1 | import path from 'path';
|
2 | import { createRequire } from 'module';
|
3 | import { INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core';
|
4 | import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
|
5 | import { toMutantRunResult, DryRunStatus, TestStatus, determineHitLimitReached, } from '@stryker-mutator/api/test-runner';
|
6 | import { escapeRegExp, notEmpty, requireResolve } from '@stryker-mutator/util';
|
7 | import { jestTestAdapterFactory } from './jest-test-adapters/index.js';
|
8 | import { withCoverageAnalysis, withHitLimit } from './jest-plugins/index.js';
|
9 | import { pluginTokens } from './plugin-di.js';
|
10 | import { configLoaderFactory } from './config-loaders/index.js';
|
11 | import { JEST_OVERRIDE_OPTIONS } from './jest-override-options.js';
|
12 | import { determineResolveFromDirectory, JestConfigWrapper, JestWrapper, verifyAllTestFilesHaveCoverage } from './utils/index.js';
|
13 | import { state } from './jest-plugins/messaging.cjs';
|
14 | export 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 | }
|
31 | export const jestTestRunnerFactory = createJestTestRunnerFactory();
|
32 | export 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 |
|
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 |
|
89 |
|
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 |
|
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 |
|
158 | process.env.FORCE_COLOR = '0';
|
159 |
|
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 |
|
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 | }
|
224 | JestTestRunner.inject = tokens(commonTokens.logger, commonTokens.options, pluginTokens.jestTestAdapter, pluginTokens.configLoader, pluginTokens.jestWrapper, pluginTokens.globalNamespace);
|
225 |
|
\ | No newline at end of file |