UNPKG

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