UNPKG

10.8 kBJavaScriptView Raw
1import path from 'path';
2import { createRequire } from 'module';
3import { INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core';
4import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
5import { toMutantRunResult, DryRunStatus, TestStatus, determineHitLimitReached, } from '@stryker-mutator/api/test-runner';
6import { escapeRegExp, notEmpty, requireResolve } from '@stryker-mutator/util';
7import { jestTestAdapterFactory } from './jest-test-adapters/index.js';
8import { withCoverageAnalysis, withHitLimit } from './jest-plugins/index.js';
9import { pluginTokens } from './plugin-di.js';
10import { configLoaderFactory } from './config-loaders/index.js';
11import { JEST_OVERRIDE_OPTIONS } from './jest-override-options.js';
12import { determineResolveFromDirectory, JestConfigWrapper, JestWrapper, verifyAllTestFilesHaveCoverage } from './utils/index.js';
13import { state } from './jest-plugins/messaging.cjs';
14export function createJestTestRunnerFactory(namespace = INSTRUMENTER_CONSTANTS.NAMESPACE) {
15 jestTestRunnerFactory.inject = tokens(commonTokens.injector);
16 function jestTestRunnerFactory(injector) {
17 return injector
18 .provideValue(pluginTokens.resolve, createRequire(process.cwd()).resolve)
19 .provideFactory(pluginTokens.resolveFromDirectory, determineResolveFromDirectory)
20 .provideValue(pluginTokens.requireFromCwd, requireResolve)
21 .provideValue(pluginTokens.processEnv, process.env)
22 .provideClass(pluginTokens.jestWrapper, JestWrapper)
23 .provideClass(pluginTokens.jestConfigWrapper, JestConfigWrapper)
24 .provideFactory(pluginTokens.jestTestAdapter, jestTestAdapterFactory)
25 .provideFactory(pluginTokens.configLoader, configLoaderFactory)
26 .provideValue(pluginTokens.globalNamespace, namespace)
27 .injectClass(JestTestRunner);
28 }
29 return jestTestRunnerFactory;
30}
31export const jestTestRunnerFactory = createJestTestRunnerFactory();
32export class JestTestRunner {
33 log;
34 jestTestAdapter;
35 configLoader;
36 jestWrapper;
37 globalNamespace;
38 jestConfig;
39 jestOptions;
40 enableFindRelatedTests;
41 static inject = tokens(commonTokens.logger, commonTokens.options, pluginTokens.jestTestAdapter, pluginTokens.configLoader, pluginTokens.jestWrapper, pluginTokens.globalNamespace);
42 constructor(log, options, jestTestAdapter, configLoader, jestWrapper, globalNamespace) {
43 this.log = log;
44 this.jestTestAdapter = jestTestAdapter;
45 this.configLoader = configLoader;
46 this.jestWrapper = jestWrapper;
47 this.globalNamespace = globalNamespace;
48 this.jestOptions = options.jest;
49 // Get enableFindRelatedTests from stryker jest options or default to true
50 this.enableFindRelatedTests = this.jestOptions.enableFindRelatedTests;
51 if (this.enableFindRelatedTests) {
52 this.log.debug('Running jest with --findRelatedTests flag. Set jest.enableFindRelatedTests to false to run all tests on every mutant.');
53 }
54 else {
55 this.log.debug('Running jest without --findRelatedTests flag. Set jest.enableFindRelatedTests to true to run only relevant tests on every mutant.');
56 }
57 }
58 async init() {
59 const configFromFile = await this.configLoader.loadConfig();
60 this.jestConfig = this.mergeConfigSettings(configFromFile, this.jestOptions || {});
61 }
62 capabilities() {
63 return { reloadEnvironment: true };
64 }
65 async dryRun({ coverageAnalysis, files }) {
66 state.coverageAnalysis = coverageAnalysis;
67 const fileNamesUnderTest = this.enableFindRelatedTests ? files : undefined;
68 const { dryRunResult, jestResult } = await this.run({
69 fileNamesUnderTest,
70 jestConfig: this.configForDryRun(fileNamesUnderTest, coverageAnalysis, this.jestWrapper),
71 testLocationInResults: true,
72 });
73 if (dryRunResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') {
74 const errorMessage = verifyAllTestFilesHaveCoverage(jestResult, state.testFilesWithStrykerEnvironment);
75 if (errorMessage) {
76 return {
77 status: DryRunStatus.Error,
78 errorMessage,
79 };
80 }
81 else {
82 dryRunResult.mutantCoverage = state.instrumenterContext.mutantCoverage;
83 }
84 }
85 return dryRunResult;
86 }
87 async mutantRun({ activeMutant, sandboxFileName, testFilter, disableBail, hitLimit }) {
88 const fileNameUnderTest = this.enableFindRelatedTests ? sandboxFileName : undefined;
89 state.coverageAnalysis = 'off';
90 let testNamePattern;
91 if (testFilter) {
92 testNamePattern = testFilter.map((testId) => `(${escapeRegExp(testId)})`).join('|');
93 }
94 state.instrumenterContext.hitLimit = hitLimit;
95 state.instrumenterContext.hitCount = hitLimit ? 0 : undefined;
96 try {
97 // Use process.env to set the active mutant.
98 // We could use `state.strykerStatic.activeMutant`, but that only works with the `StrykerEnvironment` mixin, which is optional
99 process.env[INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE] = activeMutant.id.toString();
100 const { dryRunResult } = await this.run({
101 fileNamesUnderTest: fileNameUnderTest ? [fileNameUnderTest] : undefined,
102 jestConfig: this.configForMutantRun(fileNameUnderTest, hitLimit, this.jestWrapper),
103 testNamePattern,
104 });
105 return toMutantRunResult(dryRunResult, disableBail);
106 }
107 finally {
108 delete process.env[INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE];
109 delete state.instrumenterContext.activeMutant;
110 }
111 }
112 configForDryRun(fileNamesUnderTest, coverageAnalysis, jestWrapper) {
113 return withCoverageAnalysis(this.configWithRoots(fileNamesUnderTest), coverageAnalysis, jestWrapper);
114 }
115 configForMutantRun(fileNameUnderTest, hitLimit, jestWrapper) {
116 return withHitLimit(this.configWithRoots(fileNameUnderTest ? [fileNameUnderTest] : undefined), hitLimit, jestWrapper);
117 }
118 configWithRoots(fileNamesUnderTest) {
119 let config;
120 if (fileNamesUnderTest && this.jestConfig.roots) {
121 // Make sure the file under test lives inside one of the roots
122 config = {
123 ...this.jestConfig,
124 roots: [...this.jestConfig.roots, ...new Set(fileNamesUnderTest.map((file) => path.dirname(file)))],
125 };
126 }
127 else {
128 config = this.jestConfig;
129 }
130 return config;
131 }
132 async run(settings) {
133 this.setEnv();
134 if (this.log.isTraceEnabled()) {
135 this.log.trace('Invoking Jest with config %s', JSON.stringify(settings));
136 }
137 const { results } = await this.jestTestAdapter.run(settings);
138 return { dryRunResult: this.collectRunResult(results), jestResult: results };
139 }
140 collectRunResult(results) {
141 const timeoutResult = determineHitLimitReached(state.instrumenterContext.hitCount, state.instrumenterContext.hitLimit);
142 if (timeoutResult) {
143 return timeoutResult;
144 }
145 if (results.numRuntimeErrorTestSuites) {
146 const errorMessage = results.testResults
147 .map((testSuite) => this.collectSerializableErrorText(testSuite.testExecError))
148 .filter(notEmpty)
149 .join(', ');
150 return {
151 status: DryRunStatus.Error,
152 errorMessage,
153 };
154 }
155 else {
156 return {
157 status: DryRunStatus.Complete,
158 tests: this.processTestResults(results.testResults),
159 };
160 }
161 }
162 collectSerializableErrorText(error) {
163 return error && `${error.code && `${error.code} `}${error.message} ${error.stack}`;
164 }
165 setEnv() {
166 // Force colors off: https://github.com/chalk/supports-color#info
167 process.env.FORCE_COLOR = '0';
168 // Set node environment for issues like these: https://github.com/stryker-mutator/stryker-js/issues/3580
169 process.env.NODE_ENV = 'test';
170 }
171 processTestResults(suiteResults) {
172 const testResults = [];
173 for (const suiteResult of suiteResults) {
174 for (const testResult of suiteResult.testResults) {
175 const result = {
176 id: testResult.fullName,
177 name: testResult.fullName,
178 timeSpentMs: testResult.duration ?? 0,
179 fileName: suiteResult.testFilePath,
180 startPosition: testResult.location
181 ? {
182 // Stryker works 0-based internally, jest works 1-based: https://jestjs.io/docs/cli#--testlocationinresults
183 line: testResult.location.line - 1,
184 column: testResult.location.column,
185 }
186 : undefined,
187 };
188 switch (testResult.status) {
189 case 'passed':
190 testResults.push({
191 status: TestStatus.Success,
192 ...result,
193 });
194 break;
195 case 'failed':
196 testResults.push({
197 status: TestStatus.Failed,
198 failureMessage: testResult.failureMessages.join(', '),
199 ...result,
200 });
201 break;
202 default:
203 testResults.push({
204 status: TestStatus.Skipped,
205 ...result,
206 });
207 break;
208 }
209 }
210 }
211 return testResults;
212 }
213 mergeConfigSettings(configFromFile, options) {
214 const config = (options.config ?? {});
215 const stringify = (obj) => JSON.stringify(obj, null, 2);
216 this.log.debug(`Merging file-based config ${stringify(configFromFile)}
217 with custom config ${stringify(config)}
218 and default (internal) stryker config ${stringify(JEST_OVERRIDE_OPTIONS)}`);
219 const mergedConfig = {
220 ...configFromFile,
221 ...config,
222 ...JEST_OVERRIDE_OPTIONS,
223 };
224 mergedConfig.globals = {
225 ...mergedConfig.globals,
226 __strykerGlobalNamespace__: this.globalNamespace,
227 };
228 return mergedConfig;
229 }
230}
231//# sourceMappingURL=jest-test-runner.js.map
\No newline at end of file