Skip to content

Commit 28cab95

Browse files
committed
Rewrite vitest-runner to no longer rely on file communcation.
See vitest-dev/vitest#7957
1 parent 14bf825 commit 28cab95

12 files changed

+149
-248
lines changed

e2e/test/coverage-analysis/vitest.browser.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import config from './vitest.config.js';
22

33
config.test.browser = {
44
enabled: true,
5-
name: 'chromium',
5+
instances: [{ browser: 'chromium' }],
66
provider: 'playwright',
77
headless: true,
88
};

packages/vitest-runner/src/file-communicator.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { MutantCoverage } from '@stryker-mutator/api/core';
2+
import { MutantActivation } from '@stryker-mutator/api/test-runner';
3+
import { beforeEach, afterAll, beforeAll, afterEach, inject } from 'vitest';
4+
import { toRawTestId } from './test-helpers.js';
5+
6+
const globalNamespace = inject('globalNamespace');
7+
const mutantActivation = inject('mutantActivation');
8+
const mode = inject('mode');
9+
10+
const ns = globalThis[globalNamespace] || (globalThis[globalNamespace] = {});
11+
ns.hitLimit = inject('hitLimit');
12+
debugger;
13+
14+
if (mode === 'mutant') {
15+
beforeAll(() => {
16+
ns.hitCount = 0;
17+
});
18+
19+
if (mutantActivation === 'static') {
20+
ns.activeMutant = inject('activeMutant');
21+
} else {
22+
beforeAll(() => {
23+
ns.activeMutant = inject('activeMutant');
24+
});
25+
}
26+
afterAll((suite) => {
27+
suite.meta.hitCount = ns.hitCount;
28+
});
29+
} else {
30+
ns.activeMutant = undefined;
31+
32+
beforeEach((test) => {
33+
ns.currentTestId = toRawTestId(test.task);
34+
});
35+
36+
afterEach(() => {
37+
ns.currentTestId = undefined;
38+
});
39+
40+
afterAll((suite) => {
41+
suite.meta.mutantCoverage = ns.mutantCoverage;
42+
});
43+
}
44+
45+
declare module 'vitest' {
46+
interface ProvidedContext {
47+
globalNamespace: '__stryker__' | '__stryker2__';
48+
hitLimit: number | undefined;
49+
mutantActivation: MutantActivation;
50+
activeMutant: string | undefined;
51+
mode: 'mutant' | 'dry-run';
52+
}
53+
interface TaskMeta {
54+
hitCount: number | undefined;
55+
mutantCoverage: MutantCoverage | undefined;
56+
}
57+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { RunnerTestCase, RunnerTestSuite } from 'vitest';
2+
3+
// Don't merge this file into 'vitest-helpers.ts'!
4+
// This file is used from the testing environment (via stryker-setup.js) and thus could be loaded into the browser (when using vitest with browser mode).
5+
// Thus we should avoid unnecessary dependencies in this file.
6+
7+
export function collectTestName({ name, suite }: { name: string; suite?: RunnerTestSuite }): string {
8+
const nameParts = [name];
9+
let currentSuite = suite;
10+
while (currentSuite) {
11+
nameParts.unshift(currentSuite.name);
12+
currentSuite = currentSuite.suite;
13+
}
14+
return nameParts.join(' ').trim();
15+
}
16+
17+
export function toRawTestId(test: RunnerTestCase): string {
18+
return `${test.file?.filepath ?? 'unknown.js'}#${collectTestName(test)}`;
19+
}

packages/vitest-runner/src/vitest-helpers.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import path from 'path';
22

33
import { BaseTestResult, TestResult, TestStatus } from '@stryker-mutator/api/test-runner';
4-
import type { RunMode, Suite, TaskState, Test, ResolvedConfig } from 'vitest';
4+
import type { RunMode, RunnerTestSuite, TaskState, RunnerTestCase } from 'vitest';
55
import { MutantCoverage } from '@stryker-mutator/api/core';
6+
import { collectTestName, toRawTestId } from './test-helpers.js';
67

78
function convertTaskStateToTestStatus(taskState: TaskState | undefined, testMode: RunMode): TestStatus {
89
if (testMode === 'skip') {
@@ -21,7 +22,7 @@ function convertTaskStateToTestStatus(taskState: TaskState | undefined, testMode
2122
return TestStatus.Failed;
2223
}
2324

24-
export function convertTestToTestResult(test: Test): TestResult {
25+
export function convertTestToTestResult(test: RunnerTestCase): TestResult {
2526
const status = convertTaskStateToTestStatus(test.result?.state, test.mode);
2627
const baseTestResult: BaseTestResult = {
2728
id: normalizeTestId(toRawTestId(test)),
@@ -62,7 +63,7 @@ export function normalizeCoverage(rawCoverage: MutantCoverage): MutantCoverage {
6263
};
6364
}
6465

65-
export function collectTestsFromSuite(suite: Suite): Test[] {
66+
export function collectTestsFromSuite(suite: RunnerTestSuite): RunnerTestCase[] {
6667
return suite.tasks.flatMap((task) => {
6768
if (task.type === 'suite') {
6869
return collectTestsFromSuite(task);
@@ -73,40 +74,3 @@ export function collectTestsFromSuite(suite: Suite): Test[] {
7374
}
7475
});
7576
}
76-
77-
export function addToInlineDeps(config: ResolvedConfig, matcher: RegExp): void {
78-
switch (typeof config.deps?.inline) {
79-
case 'undefined':
80-
config.deps = { inline: [matcher] };
81-
break;
82-
case 'object':
83-
config.deps.inline.push(matcher);
84-
break;
85-
case 'boolean':
86-
if (!config.deps.inline) {
87-
config.deps.inline = [matcher];
88-
}
89-
break;
90-
default:
91-
config.deps.inline satisfies never;
92-
}
93-
}
94-
95-
// Stryker disable all: the function toTestId will be stringified at runtime which will cause problems when mutated.
96-
97-
// Note: this function is used in code and copied to the mutated environment so the naming convention will always be the same.
98-
// It can not use external resource because those will not be available in the mutated environment.
99-
export function collectTestName({ name, suite }: { name: string; suite?: Suite }): string {
100-
const nameParts = [name];
101-
let currentSuite = suite;
102-
while (currentSuite) {
103-
nameParts.unshift(currentSuite.name);
104-
currentSuite = currentSuite.suite;
105-
}
106-
return nameParts.join(' ').trim();
107-
}
108-
109-
export function toRawTestId(test: Test): string {
110-
return `${test.file?.filepath ?? 'unknown.js'}#${collectTestName(test)}`;
111-
}
112-
// Stryker restore all

packages/vitest-runner/src/vitest-test-runner.ts

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,26 @@ import {
1515
import { escapeRegExp, notEmpty } from '@stryker-mutator/util';
1616

1717
import { vitestWrapper, Vitest } from './vitest-wrapper.js';
18-
import { convertTestToTestResult, fromTestId, collectTestsFromSuite, addToInlineDeps, normalizeCoverage } from './vitest-helpers.js';
19-
import { FileCommunicator } from './file-communicator.js';
18+
import { convertTestToTestResult, fromTestId, collectTestsFromSuite, normalizeCoverage } from './vitest-helpers.js';
2019
import { VitestRunnerOptionsWithStrykerOptions } from './vitest-runner-options-with-stryker-options.js';
20+
import { fileURLToPath } from 'url';
2121

2222
type StrykerNamespace = '__stryker__' | '__stryker2__';
23+
const STRYKER_SETUP = fileURLToPath(new URL('./stryker-setup.js', import.meta.url));
2324

2425
export class VitestTestRunner implements TestRunner {
2526
public static inject = [commonTokens.options, commonTokens.logger, 'globalNamespace'] as const;
2627
private ctx?: Vitest;
27-
private readonly fileCommunicator: FileCommunicator;
28+
// private readonly fileCommunicator: FileCommunicator;
2829
private readonly options: VitestRunnerOptionsWithStrykerOptions;
2930

3031
constructor(
3132
options: StrykerOptions,
3233
private readonly log: Logger,
33-
globalNamespace: StrykerNamespace,
34+
private globalNamespace: StrykerNamespace,
3435
) {
3536
this.options = options as VitestRunnerOptionsWithStrykerOptions;
36-
this.fileCommunicator = new FileCommunicator(globalNamespace);
37+
// this.fileCommunicator = new FileCommunicator(globalNamespace);
3738
}
3839

3940
public capabilities(): TestRunnerCapabilities {
@@ -62,26 +63,22 @@ export class VitestTestRunner implements TestRunner {
6263
bail: this.options.disableBail ? 0 : 1,
6364
onConsoleLog: () => false,
6465
});
65-
66-
// The vitest setup file needs to be inlined
67-
// See https://github.com/vitest-dev/vitest/issues/3403#issuecomment-1554057966
68-
const vitestSetupMatcher = new RegExp(escapeRegExp(this.fileCommunicator.vitestSetup));
69-
addToInlineDeps(this.ctx.config, vitestSetupMatcher);
66+
this.ctx.provide('globalNamespace', this.globalNamespace);
7067
this.ctx.config.browser.screenshotFailures = false;
7168
this.ctx.projects.forEach((project) => {
72-
project.config.setupFiles = [this.fileCommunicator.vitestSetup, ...project.config.setupFiles];
69+
project.config.setupFiles = [STRYKER_SETUP, ...project.config.setupFiles];
7370
project.config.browser.screenshotFailures = false;
74-
addToInlineDeps(project.config, vitestSetupMatcher);
7571
});
7672
if (this.log.isDebugEnabled()) {
7773
this.log.debug(`vitest final config: ${JSON.stringify(this.ctx.config, null, 2)}`);
7874
}
7975
}
8076

8177
public async dryRun(): Promise<DryRunResult> {
82-
await this.fileCommunicator.setDryRun();
78+
this.ctx!.provide('mode', 'dry-run');
79+
8380
const testResult = await this.run();
84-
const mutantCoverage: MutantCoverage = this.readMutantCoverage();
81+
const mutantCoverage = this.readMutantCoverage();
8582
if (testResult.status === DryRunStatus.Complete) {
8683
return {
8784
status: testResult.status,
@@ -93,7 +90,10 @@ export class VitestTestRunner implements TestRunner {
9390
}
9491

9592
public async mutantRun(options: MutantRunOptions): Promise<MutantRunResult> {
96-
await this.fileCommunicator.setMutantRun(options);
93+
this.ctx!.provide('mode', 'mutant');
94+
this.ctx!.provide('hitLimit', options.hitLimit);
95+
this.ctx!.provide('mutantActivation', options.mutantActivation);
96+
this.ctx!.provide('activeMutant', options.activeMutant.id);
9797
const dryRunResult = await this.run(options.testFilter);
9898
const hitCount = this.readHitCount();
9999
const timeOut = determineHitLimitReached(hitCount, options.hitLimit);
@@ -150,21 +150,6 @@ export class VitestTestRunner implements TestRunner {
150150
// Clear the state from the previous run
151151
// Note that this is kind of a hack, see https://github.com/vitest-dev/vitest/discussions/3017#discussioncomment-5901751
152152
this.ctx!.state.filesMap.clear();
153-
154-
// Since we:
155-
// 1. are reusing the same vitest instance
156-
// 2. have changed the vitest setup file contents (see FileCommunicator.setMutantRun)
157-
// 3. the vitest setup file is inlined (see VitestTestRunner.init)
158-
// 4. we're not using the vitest watch mode
159-
// We need to invalidate the module cache for the vitest setup file
160-
// See https://github.com/vitest-dev/vitest/issues/3409#issuecomment-1555884513
161-
this.ctx!.projects.forEach((project) => {
162-
const { moduleGraph } = project.server;
163-
const module = moduleGraph.getModuleById(this.fileCommunicator.vitestSetup);
164-
if (module) {
165-
moduleGraph.invalidateModule(module);
166-
}
167-
});
168153
}
169154

170155
private readHitCount() {
@@ -213,7 +198,7 @@ export class VitestTestRunner implements TestRunner {
213198
}
214199

215200
public async dispose(): Promise<void> {
216-
await this.fileCommunicator.dispose();
201+
// await this.fileCommunicator.dispose();
217202
await this.ctx?.close();
218203
}
219204
}

packages/vitest-runner/test/integration/browser-mode.it.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('VitestRunner in browser mode', () => {
2525

2626
sandbox = new TempTestDirectorySandbox('browser-project', { soft: true });
2727
await sandbox.init();
28-
sandboxFileName = path.resolve(sandbox.tmpDir, 'src/heading.component.ts');
28+
sandboxFileName = path.resolve(sandbox.tmpDir, 'src/math.component.ts');
2929
await sut.init();
3030
});
3131
afterEach(async () => {
@@ -137,9 +137,10 @@ describe('VitestRunner in browser mode', () => {
137137

138138
it('should be able to survive after killing mutant', async () => {
139139
// Arrange
140-
await sut.mutantRun(
140+
const initResult = await sut.mutantRun(
141141
factory.mutantRunOptions({ activeMutant: factory.mutant({ id: '50' }), mutantActivation: 'runtime', testFilter: [test3], sandboxFileName }),
142142
);
143+
assertions.expectKilled(initResult);
143144

144145
// Act
145146
const runResult = await sut.mutantRun(
@@ -156,6 +157,28 @@ describe('VitestRunner in browser mode', () => {
156157
expect(runResult.nrOfTests).eq(1);
157158
});
158159

160+
it('should be able to kill after survive mutant', async () => {
161+
// Arrange
162+
const initResult = await sut.mutantRun(
163+
factory.mutantRunOptions({
164+
activeMutant: factory.mutant({ id: '48' }), // Should survive
165+
sandboxFileName,
166+
mutantActivation: 'runtime',
167+
testFilter: [test2],
168+
}),
169+
);
170+
assertions.expectSurvived(initResult);
171+
172+
// Act
173+
const runResult = await sut.mutantRun(
174+
factory.mutantRunOptions({ activeMutant: factory.mutant({ id: '50' }), mutantActivation: 'runtime', testFilter: [test3], sandboxFileName }),
175+
);
176+
177+
// Assert
178+
assertions.expectKilled(runResult);
179+
expect(runResult.nrOfTests).eq(1);
180+
});
181+
159182
it('should be able to kill a static mutant', async () => {
160183
// Act
161184
const runResult = await sut.mutantRun(

0 commit comments

Comments
 (0)