UNPKG

6.79 kBPlain TextView Raw
1import { InstrumenterContext, INSTRUMENTER_CONSTANTS, StrykerOptions } from '@stryker-mutator/api/core';
2import { Logger } from '@stryker-mutator/api/logging';
3import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
4import { I, escapeRegExp } from '@stryker-mutator/util';
5
6import {
7 TestRunner,
8 DryRunResult,
9 DryRunOptions,
10 MutantRunOptions,
11 MutantRunResult,
12 DryRunStatus,
13 toMutantRunResult,
14 CompleteDryRunResult,
15 determineHitLimitReached,
16 TestRunnerCapabilities,
17 MutantActivation,
18} from '@stryker-mutator/api/test-runner';
19
20import { Context, RootHookObject, Suite } from 'mocha';
21
22import { StrykerMochaReporter } from './stryker-mocha-reporter.js';
23import { MochaRunnerWithStrykerOptions } from './mocha-runner-with-stryker-options.js';
24import * as pluginTokens from './plugin-tokens.js';
25import { MochaOptionsLoader } from './mocha-options-loader.js';
26import { MochaAdapter } from './mocha-adapter.js';
27
28export class MochaTestRunner implements TestRunner {
29 private mocha!: Mocha;
30 private readonly instrumenterContext: InstrumenterContext;
31 private originalGrep?: string;
32 public beforeEach?: (context: Context) => void;
33
34 public static inject = tokens(
35 commonTokens.logger,
36 commonTokens.options,
37 pluginTokens.loader,
38 pluginTokens.mochaAdapter,
39 pluginTokens.globalNamespace
40 );
41 private loadedEnv = false;
42 constructor(
43 private readonly log: Logger,
44 private readonly options: StrykerOptions,
45 private readonly loader: I<MochaOptionsLoader>,
46 private readonly mochaAdapter: I<MochaAdapter>,
47 globalNamespace: typeof INSTRUMENTER_CONSTANTS.NAMESPACE | '__stryker2__'
48 ) {
49 StrykerMochaReporter.log = log;
50 this.instrumenterContext = global[globalNamespace] ?? (global[globalNamespace] = {});
51 }
52
53 public async capabilities(): Promise<TestRunnerCapabilities> {
54 return {
55 // Mocha directly uses `import`, so reloading files once they are loaded is impossible
56 reloadEnvironment: false,
57 };
58 }
59
60 public async init(): Promise<void> {
61 const mochaOptions = this.loader.load(this.options as MochaRunnerWithStrykerOptions);
62 const testFileNames = this.mochaAdapter.collectFiles(mochaOptions);
63 let rootHooks: RootHookObject | undefined;
64 if (mochaOptions.require) {
65 if (mochaOptions.require.includes('esm')) {
66 throw new Error(
67 'Config option "mochaOptions.require" does not support "esm", please use `"testRunnerNodeArgs": ["--require", "esm"]` instead. See https://github.com/stryker-mutator/stryker-js/issues/3014 for more information.'
68 );
69 }
70 rootHooks = await this.mochaAdapter.handleRequires(mochaOptions.require);
71 }
72 this.mocha = this.mochaAdapter.create({
73 reporter: StrykerMochaReporter as any,
74 timeout: 0,
75 rootHooks,
76 });
77 this.mocha.cleanReferencesAfterRun(false);
78 testFileNames.forEach((fileName) => this.mocha.addFile(fileName));
79
80 this.setIfDefined(mochaOptions['async-only'], (asyncOnly) => asyncOnly && this.mocha.asyncOnly());
81 this.setIfDefined(mochaOptions.ui, this.mocha.ui);
82 this.setIfDefined(mochaOptions.grep, this.mocha.grep);
83 this.originalGrep = mochaOptions.grep;
84
85 // Bind beforeEach, so we can use that for per code coverage in dry run
86 const self = this;
87 this.mocha.suite.beforeEach(function (this: Context) {
88 self.beforeEach?.(this);
89 });
90 }
91
92 private setIfDefined<T>(value: T | undefined, operation: (input: T) => void) {
93 if (typeof value !== 'undefined') {
94 operation.apply(this.mocha, [value]);
95 }
96 }
97
98 public async dryRun({ coverageAnalysis, disableBail }: DryRunOptions): Promise<DryRunResult> {
99 if (coverageAnalysis === 'perTest') {
100 this.beforeEach = (context) => {
101 this.instrumenterContext.currentTestId = context.currentTest?.fullTitle();
102 };
103 }
104 const runResult = await this.run(disableBail);
105 if (runResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') {
106 runResult.mutantCoverage = this.instrumenterContext.mutantCoverage;
107 }
108 delete this.beforeEach;
109 return runResult;
110 }
111
112 public async mutantRun({ activeMutant, testFilter, disableBail, hitLimit, mutantActivation }: MutantRunOptions): Promise<MutantRunResult> {
113 this.instrumenterContext.hitLimit = hitLimit;
114 this.instrumenterContext.hitCount = hitLimit ? 0 : undefined;
115 if (testFilter) {
116 const metaRegExp = testFilter.map((testId) => `(${escapeRegExp(testId)})`).join('|');
117 const regex = new RegExp(metaRegExp);
118 this.mocha.grep(regex);
119 } else {
120 this.setIfDefined(this.originalGrep, this.mocha.grep);
121 }
122 const dryRunResult = await this.run(disableBail, activeMutant.id, mutantActivation);
123 return toMutantRunResult(dryRunResult);
124 }
125
126 public async run(disableBail: boolean, activeMutantId?: string, mutantActivation?: MutantActivation): Promise<DryRunResult> {
127 setBail(!disableBail, this.mocha.suite);
128 try {
129 if (!this.loadedEnv) {
130 this.instrumenterContext.activeMutant = mutantActivation === 'static' ? activeMutantId : undefined;
131 // Loading files Async is needed to support native esm modules
132 // See https://mochajs.org/api/mocha#loadFilesAsync
133 await this.mocha.loadFilesAsync();
134 this.loadedEnv = true;
135 }
136 this.instrumenterContext.activeMutant = activeMutantId;
137 await this.runMocha();
138 const reporter = StrykerMochaReporter.currentInstance;
139 if (reporter) {
140 const timeoutResult = determineHitLimitReached(this.instrumenterContext.hitCount, this.instrumenterContext.hitLimit);
141 if (timeoutResult) {
142 return timeoutResult;
143 }
144 const result: CompleteDryRunResult = {
145 status: DryRunStatus.Complete,
146 tests: reporter.tests,
147 };
148 return result;
149 } else {
150 const errorMessage = `Mocha didn't instantiate the ${StrykerMochaReporter.name} correctly. Test result cannot be reported.`;
151 this.log.error(errorMessage);
152 return {
153 status: DryRunStatus.Error,
154 errorMessage,
155 };
156 }
157 } catch (errorMessage: any) {
158 return {
159 errorMessage,
160 status: DryRunStatus.Error,
161 };
162 }
163
164 function setBail(bail: boolean, suite: Suite) {
165 suite.bail(bail);
166 suite.suites.forEach((childSuite) => setBail(bail, childSuite));
167 }
168 }
169
170 public async dispose(): Promise<void> {
171 try {
172 this.mocha?.dispose();
173 } catch (err: any) {
174 if (err?.code !== 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING') {
175 // Oops, didn't mean to catch this one
176 throw err;
177 }
178 }
179 }
180
181 private async runMocha(): Promise<void> {
182 return new Promise<void>((res) => {
183 this.mocha.run(() => res());
184 });
185 }
186}
187
\No newline at end of file