@stryker-mutator/jest-runner
Version:
A plugin to use the jest test runner and framework in Stryker, the JavaScript mutation testing framework
231 lines • 10.8 kB
JavaScript
import path from 'path';
import { createRequire } from 'module';
import { INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { toMutantRunResult, DryRunStatus, TestStatus, determineHitLimitReached, } from '@stryker-mutator/api/test-runner';
import { escapeRegExp, notEmpty, requireResolve } from '@stryker-mutator/util';
import { jestTestAdapterFactory } from './jest-test-adapters/index.js';
import { withCoverageAnalysis, withHitLimit } from './jest-plugins/index.js';
import { pluginTokens } from './plugin-di.js';
import { configLoaderFactory } from './config-loaders/index.js';
import { JEST_OVERRIDE_OPTIONS } from './jest-override-options.js';
import { determineResolveFromDirectory, JestConfigWrapper, JestWrapper, verifyAllTestFilesHaveCoverage } from './utils/index.js';
import { state } from './jest-plugins/messaging.cjs';
export function createJestTestRunnerFactory(namespace = INSTRUMENTER_CONSTANTS.NAMESPACE) {
jestTestRunnerFactory.inject = tokens(commonTokens.injector);
function jestTestRunnerFactory(injector) {
return injector
.provideValue(pluginTokens.resolve, createRequire(process.cwd()).resolve)
.provideFactory(pluginTokens.resolveFromDirectory, determineResolveFromDirectory)
.provideValue(pluginTokens.requireFromCwd, requireResolve)
.provideValue(pluginTokens.processEnv, process.env)
.provideClass(pluginTokens.jestWrapper, JestWrapper)
.provideClass(pluginTokens.jestConfigWrapper, JestConfigWrapper)
.provideFactory(pluginTokens.jestTestAdapter, jestTestAdapterFactory)
.provideFactory(pluginTokens.configLoader, configLoaderFactory)
.provideValue(pluginTokens.globalNamespace, namespace)
.injectClass(JestTestRunner);
}
return jestTestRunnerFactory;
}
export const jestTestRunnerFactory = createJestTestRunnerFactory();
export class JestTestRunner {
log;
jestTestAdapter;
configLoader;
jestWrapper;
globalNamespace;
jestConfig;
jestOptions;
enableFindRelatedTests;
static inject = tokens(commonTokens.logger, commonTokens.options, pluginTokens.jestTestAdapter, pluginTokens.configLoader, pluginTokens.jestWrapper, pluginTokens.globalNamespace);
constructor(log, options, jestTestAdapter, configLoader, jestWrapper, globalNamespace) {
this.log = log;
this.jestTestAdapter = jestTestAdapter;
this.configLoader = configLoader;
this.jestWrapper = jestWrapper;
this.globalNamespace = globalNamespace;
this.jestOptions = options.jest;
// Get enableFindRelatedTests from stryker jest options or default to true
this.enableFindRelatedTests = this.jestOptions.enableFindRelatedTests;
if (this.enableFindRelatedTests) {
this.log.debug('Running jest with --findRelatedTests flag. Set jest.enableFindRelatedTests to false to run all tests on every mutant.');
}
else {
this.log.debug('Running jest without --findRelatedTests flag. Set jest.enableFindRelatedTests to true to run only relevant tests on every mutant.');
}
}
async init() {
const configFromFile = await this.configLoader.loadConfig();
this.jestConfig = this.mergeConfigSettings(configFromFile, this.jestOptions || {});
}
capabilities() {
return { reloadEnvironment: true };
}
async dryRun({ coverageAnalysis, files }) {
state.coverageAnalysis = coverageAnalysis;
const fileNamesUnderTest = this.enableFindRelatedTests ? files : undefined;
const { dryRunResult, jestResult } = await this.run({
fileNamesUnderTest,
jestConfig: this.configForDryRun(fileNamesUnderTest, coverageAnalysis, this.jestWrapper),
testLocationInResults: true,
});
if (dryRunResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') {
const errorMessage = verifyAllTestFilesHaveCoverage(jestResult, state.testFilesWithStrykerEnvironment);
if (errorMessage) {
return {
status: DryRunStatus.Error,
errorMessage,
};
}
else {
dryRunResult.mutantCoverage = state.instrumenterContext.mutantCoverage;
}
}
return dryRunResult;
}
async mutantRun({ activeMutant, sandboxFileName, testFilter, disableBail, hitLimit }) {
const fileNameUnderTest = this.enableFindRelatedTests ? sandboxFileName : undefined;
state.coverageAnalysis = 'off';
let testNamePattern;
if (testFilter) {
testNamePattern = testFilter.map((testId) => `(${escapeRegExp(testId)})`).join('|');
}
state.instrumenterContext.hitLimit = hitLimit;
state.instrumenterContext.hitCount = hitLimit ? 0 : undefined;
try {
// Use process.env to set the active mutant.
// We could use `state.strykerStatic.activeMutant`, but that only works with the `StrykerEnvironment` mixin, which is optional
process.env[INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE] = activeMutant.id.toString();
const { dryRunResult } = await this.run({
fileNamesUnderTest: fileNameUnderTest ? [fileNameUnderTest] : undefined,
jestConfig: this.configForMutantRun(fileNameUnderTest, hitLimit, this.jestWrapper),
testNamePattern,
});
return toMutantRunResult(dryRunResult, disableBail);
}
finally {
delete process.env[INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE];
delete state.instrumenterContext.activeMutant;
}
}
configForDryRun(fileNamesUnderTest, coverageAnalysis, jestWrapper) {
return withCoverageAnalysis(this.configWithRoots(fileNamesUnderTest), coverageAnalysis, jestWrapper);
}
configForMutantRun(fileNameUnderTest, hitLimit, jestWrapper) {
return withHitLimit(this.configWithRoots(fileNameUnderTest ? [fileNameUnderTest] : undefined), hitLimit, jestWrapper);
}
configWithRoots(fileNamesUnderTest) {
let config;
if (fileNamesUnderTest && this.jestConfig.roots) {
// Make sure the file under test lives inside one of the roots
config = {
...this.jestConfig,
roots: [...this.jestConfig.roots, ...new Set(fileNamesUnderTest.map((file) => path.dirname(file)))],
};
}
else {
config = this.jestConfig;
}
return config;
}
async run(settings) {
this.setEnv();
if (this.log.isTraceEnabled()) {
this.log.trace('Invoking Jest with config %s', JSON.stringify(settings));
}
const { results } = await this.jestTestAdapter.run(settings);
return { dryRunResult: this.collectRunResult(results), jestResult: results };
}
collectRunResult(results) {
const timeoutResult = determineHitLimitReached(state.instrumenterContext.hitCount, state.instrumenterContext.hitLimit);
if (timeoutResult) {
return timeoutResult;
}
if (results.numRuntimeErrorTestSuites) {
const errorMessage = results.testResults
.map((testSuite) => this.collectSerializableErrorText(testSuite.testExecError))
.filter(notEmpty)
.join(', ');
return {
status: DryRunStatus.Error,
errorMessage,
};
}
else {
return {
status: DryRunStatus.Complete,
tests: this.processTestResults(results.testResults),
};
}
}
collectSerializableErrorText(error) {
return error && `${error.code && `${error.code} `}${error.message} ${error.stack}`;
}
setEnv() {
// Force colors off: https://github.com/chalk/supports-color#info
process.env.FORCE_COLOR = '0';
// Set node environment for issues like these: https://github.com/stryker-mutator/stryker-js/issues/3580
process.env.NODE_ENV = 'test';
}
processTestResults(suiteResults) {
const testResults = [];
for (const suiteResult of suiteResults) {
for (const testResult of suiteResult.testResults) {
const result = {
id: testResult.fullName,
name: testResult.fullName,
timeSpentMs: testResult.duration ?? 0,
fileName: suiteResult.testFilePath,
startPosition: testResult.location
? {
// Stryker works 0-based internally, jest works 1-based: https://jestjs.io/docs/cli#--testlocationinresults
line: testResult.location.line - 1,
column: testResult.location.column,
}
: undefined,
};
switch (testResult.status) {
case 'passed':
testResults.push({
status: TestStatus.Success,
...result,
});
break;
case 'failed':
testResults.push({
status: TestStatus.Failed,
failureMessage: testResult.failureMessages.join(', '),
...result,
});
break;
default:
testResults.push({
status: TestStatus.Skipped,
...result,
});
break;
}
}
}
return testResults;
}
mergeConfigSettings(configFromFile, options) {
const config = (options.config ?? {});
const stringify = (obj) => JSON.stringify(obj, null, 2);
this.log.debug(`Merging file-based config ${stringify(configFromFile)}
with custom config ${stringify(config)}
and default (internal) stryker config ${stringify(JEST_OVERRIDE_OPTIONS)}`);
const mergedConfig = {
...configFromFile,
...config,
...JEST_OVERRIDE_OPTIONS,
};
mergedConfig.globals = {
...mergedConfig.globals,
__strykerGlobalNamespace__: this.globalNamespace,
};
return mergedConfig;
}
}
//# sourceMappingURL=jest-test-runner.js.map