1 | import { InstrumenterContext, INSTRUMENTER_CONSTANTS, StrykerOptions } from '@stryker-mutator/api/core';
|
2 | import { Logger } from '@stryker-mutator/api/logging';
|
3 | import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
|
4 | import { I, escapeRegExp } from '@stryker-mutator/util';
|
5 |
|
6 | import {
|
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 |
|
20 | import { Context, RootHookObject, Suite } from 'mocha';
|
21 |
|
22 | import { StrykerMochaReporter } from './stryker-mocha-reporter.js';
|
23 | import { MochaRunnerWithStrykerOptions } from './mocha-runner-with-stryker-options.js';
|
24 | import * as pluginTokens from './plugin-tokens.js';
|
25 | import { MochaOptionsLoader } from './mocha-options-loader.js';
|
26 | import { MochaAdapter } from './mocha-adapter.js';
|
27 |
|
28 | export 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 |
|
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 |
|
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 |
|
132 |
|
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 |
|
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 |