@stryker-mutator/karma-runner
Version:
A plugin to use the karma test runner in Stryker, the JavaScript mutation testing framework
242 lines (215 loc) • 7.58 kB
text/typescript
import path from 'path';
import { fileURLToPath } from 'url';
import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging';
import type {
Config,
ConfigOptions,
ClientOptions,
InlinePluginType,
} from 'karma';
import { noopLogger, requireResolve } from '@stryker-mutator/util';
import { StrykerReporter, strykerReporterFactory } from './stryker-reporter.js';
import {
TestHooksMiddleware,
TEST_HOOKS_FILE_NAME,
} from './test-hooks-middleware.js';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export const strykerKarmaConfigPath = path.resolve(
path.dirname(filename),
'..' /* karma-plugins */,
'..' /* src */,
'..' /* dist */,
'stryker-karma.conf.cjs',
);
function setDefaultOptions(config: Config) {
config.set({
browsers: ['ChromeHeadless'],
frameworks: ['jasmine'],
});
}
async function setUserKarmaConfigFile(
config: Config,
log: Logger,
requireFromCwd: typeof requireResolve,
) {
if (globalSettings.karmaConfigFile) {
const configFileName = path.resolve(globalSettings.karmaConfigFile);
log.debug('Importing config from "%s"', configFileName);
try {
const userConfig = requireFromCwd(configFileName);
if (typeof userConfig !== 'function') {
throw new TypeError(
`Karma config file "${configFileName}" should export a function! Found: ${typeof userConfig}`,
);
}
await userConfig(config);
config.configFile = configFileName; // override config to ensure karma is as user-like as possible
} catch (error: any) {
if (error.code === 'MODULE_NOT_FOUND') {
log.error(
`Unable to find karma config at "${globalSettings.karmaConfigFile}" (tried to load from ${configFileName}). Please check your stryker config. You might need to make sure the file is included in the sandbox directory.`,
);
} else {
throw error; // oops
}
}
}
}
/**
* Sets configuration that is needed to control the karma life cycle. Namely it shouldn't watch files and not quit after first test run.
* @param config The config to use
*/
function setLifeCycleOptions(config: Config) {
config.set({
// No auto watch, stryker will inform us when we need to test
autoWatch: false,
// Override browserNoActivityTimeout. Default value 10000 might not enough to send perTest coverage results
browserNoActivityTimeout: 1000000,
// Never detach, always run in this same process (is already a separate process)
detached: false,
// Don't stop after first run
singleRun: false,
});
}
/**
* Sets configuration that is needed to control client scripts in karma.
* @param config The config to use
* @see https://github.com/stryker-mutator/stryker-js/issues/2049
*/
function setClientOptions(config: Config) {
// Disable clearContext because of issue #2049 (race condition in Karma)
// Enabling clearContext (default true) will load "about:blank" in the iFrame after a test run.
// As far as I can see clearing the context only has a visible effect (you don't see the result of the last test).
// If this is true, disabling it is safe to do and solves the race condition issue.
const clientOptions: Partial<ClientOptions> = { clearContext: false };
// Disable randomized tests with using jasmine. Stryker doesn't play nice with a random test order, since spec id's tent to move around
// Also set failFast, so that we're not waiting on more than 1 failed test
if (config.frameworks?.includes('jasmine')) {
(clientOptions as any).jasmine = {
random: false,
// Jasmine@<4
failFast: !globalSettings.disableBail,
oneFailurePerSpec: !globalSettings.disableBail,
// Jasmine@4
stopOnSpecFailure: !globalSettings.disableBail,
stopSpecOnExpectationFailure: !globalSettings.disableBail,
};
}
if (config.frameworks?.includes('mocha')) {
(clientOptions as any).mocha = { bail: !globalSettings.disableBail };
}
config.set({ client: clientOptions });
}
function setUserKarmaConfig(config: Config) {
if (globalSettings.karmaConfig) {
config.set(globalSettings.karmaConfig);
}
}
function setBasePath(config: Config) {
if (!config.basePath) {
// We need to set the base path, so karma won't use this file to base everything of
if (globalSettings.karmaConfigFile) {
config.basePath = path.resolve(
path.dirname(globalSettings.karmaConfigFile),
);
} else {
config.basePath = process.cwd();
}
}
}
function addPlugin(
karmaConfig: ConfigOptions,
karmaPlugin: Record<string, InlinePluginType> | string,
) {
karmaConfig.plugins = karmaConfig.plugins ?? ['karma-*'];
karmaConfig.plugins.push(
typeof karmaPlugin === 'string'
? import.meta.resolve(karmaPlugin)
: karmaPlugin,
);
}
/**
* Configures the test hooks middleware.
* It adds a non-existing file to the top `files` array.
* Further more it configures a middleware that serves the file.
*/
function configureTestHooksMiddleware(config: Config) {
// Add test run middleware file
config.files = config.files ?? [];
config.files.unshift({
pattern: TEST_HOOKS_FILE_NAME,
included: true,
watched: false,
served: false,
nocache: true,
}); // Add a custom hooks file to provide hooks
const middleware: string[] =
config.beforeMiddleware ?? (config.beforeMiddleware = []);
middleware.unshift(TestHooksMiddleware.name);
TestHooksMiddleware.instance.configureTestFramework(config.frameworks);
addPlugin(config, {
[`middleware:${TestHooksMiddleware.name}`]: [
'value',
TestHooksMiddleware.instance.handler,
],
});
}
function configureStrykerMutantCoverageAdapter(config: Config) {
config.files = config.files ?? [];
config.files.unshift({
pattern: path.resolve(dirname, 'stryker-mutant-coverage-adapter.js'),
included: true,
watched: false,
served: true,
nocache: true,
type: 'module',
});
}
function configureStrykerReporter(config: Config) {
addPlugin(config, {
[`reporter:${StrykerReporter.name}`]: ['factory', strykerReporterFactory],
});
if (!config.reporters) {
config.reporters = [];
}
config.reporters.push(StrykerReporter.name);
}
interface GlobalSettings {
karmaConfig?: ConfigOptions;
karmaConfigFile?: string;
getLogger: LoggerFactoryMethod;
disableBail: boolean;
}
const globalSettings: GlobalSettings = {
getLogger() {
return noopLogger;
},
disableBail: false,
};
export async function configureKarma(
config: Config,
requireFromCwd = requireResolve,
): Promise<void> {
const log = globalSettings.getLogger(path.basename(filename));
setDefaultOptions(config);
await setUserKarmaConfigFile(config, log, requireFromCwd);
setUserKarmaConfig(config);
setBasePath(config);
setLifeCycleOptions(config);
setClientOptions(config);
configureTestHooksMiddleware(config);
configureStrykerMutantCoverageAdapter(config);
configureStrykerReporter(config);
}
/**
* Provide global settings for next configuration
* This is the only way we can pass through any values between the `KarmaTestRunner` and the stryker-karma.conf file.
* (not counting environment variables)
*/
configureKarma.setGlobals = (globals: Partial<GlobalSettings>) => {
globalSettings.karmaConfig = globals.karmaConfig;
globalSettings.karmaConfigFile = globals.karmaConfigFile;
globalSettings.getLogger = globals.getLogger ?? (() => noopLogger);
globalSettings.disableBail = globals.disableBail ?? false;
};