UNPKG

@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
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