Skip to content

Commit 3c6e7cb

Browse files
committed
Rewrite vitest-runner to no longer rely on file communcation.
See vitest-dev/vitest#7957
1 parent f66c974 commit 3c6e7cb

12 files changed

+146
-225
lines changed

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

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

33
config.test.browser = {
44
enabled: true,
5+
instances: [{ browser: 'chromium' }],
56
provider: 'playwright',
67
headless: true,
7-
instances: [
8-
{
9-
browser: 'chromium',
10-
}
11-
]
128
};
139

1410
export default config;

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: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BaseTestResult, TestResult, TestStatus } from '@stryker-mutator/api/tes
44
import type { RunMode, TaskState } from 'vitest';
55
import { RunnerTestCase, RunnerTestSuite } from 'vitest/node';
66
import { MutantCoverage } from '@stryker-mutator/api/core';
7+
import { collectTestName, toRawTestId } from './test-helpers.js';
78

89
function convertTaskStateToTestStatus(taskState: TaskState | undefined, testMode: RunMode): TestStatus {
910
if (testMode === 'skip') {
@@ -74,22 +75,3 @@ export function collectTestsFromSuite(suite: RunnerTestSuite): RunnerTestCase[]
7475
}
7576
});
7677
}
77-
78-
// Stryker disable all: the function toTestId will be stringified at runtime which will cause problems when mutated.
79-
80-
// Note: this function is used in code and copied to the mutated environment so the naming convention will always be the same.
81-
// It can not use external resource because those will not be available in the mutated environment.
82-
export function collectTestName({ name, suite }: { name: string; suite?: RunnerTestSuite }): string {
83-
const nameParts = [name];
84-
let currentSuite = suite;
85-
while (currentSuite) {
86-
nameParts.unshift(currentSuite.name);
87-
currentSuite = currentSuite.suite;
88-
}
89-
return nameParts.join(' ').trim();
90-
}
91-
92-
export function toRawTestId(test: RunnerTestCase): string {
93-
return `${test.file?.filepath ?? 'unknown.js'}#${collectTestName(test)}`;
94-
}
95-
// Stryker restore all

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

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

1717
import { vitestWrapper, Vitest } from './vitest-wrapper.js';
1818
import { convertTestToTestResult, fromTestId, collectTestsFromSuite, normalizeCoverage } from './vitest-helpers.js';
19-
import { FileCommunicator } from './file-communicator.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,20 +63,22 @@ export class VitestTestRunner implements TestRunner {
6263
bail: this.options.disableBail ? 0 : 1,
6364
onConsoleLog: () => false,
6465
});
65-
66+
this.ctx.provide('globalNamespace', this.globalNamespace);
6667
this.ctx.config.browser.screenshotFailures = false;
6768
this.ctx.projects.forEach((project) => {
68-
project.config.setupFiles = [this.fileCommunicator.vitestSetup, ...project.config.setupFiles];
69+
project.config.setupFiles = [STRYKER_SETUP, ...project.config.setupFiles];
70+
project.config.browser.screenshotFailures = false;
6971
});
7072
if (this.log.isDebugEnabled()) {
7173
this.log.debug(`vitest final config: ${JSON.stringify(this.ctx.config, null, 2)}`);
7274
}
7375
}
7476

7577
public async dryRun(): Promise<DryRunResult> {
76-
await this.fileCommunicator.setDryRun();
78+
this.ctx!.provide('mode', 'dry-run');
79+
7780
const testResult = await this.run();
78-
const mutantCoverage: MutantCoverage = this.readMutantCoverage();
81+
const mutantCoverage = this.readMutantCoverage();
7982
if (testResult.status === DryRunStatus.Complete) {
8083
return {
8184
status: testResult.status,
@@ -87,7 +90,10 @@ export class VitestTestRunner implements TestRunner {
8790
}
8891

8992
public async mutantRun(options: MutantRunOptions): Promise<MutantRunResult> {
90-
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);
9197
const dryRunResult = await this.run(options.testFilter);
9298
const hitCount = this.readHitCount();
9399
const timeOut = determineHitLimitReached(hitCount, options.hitLimit);
@@ -141,21 +147,6 @@ export class VitestTestRunner implements TestRunner {
141147
// Clear the state from the previous run
142148
// Note that this is kind of a hack, see https://github.com/vitest-dev/vitest/discussions/3017#discussioncomment-5901751
143149
this.ctx!.state.filesMap.clear();
144-
145-
// Since we:
146-
// 1. are reusing the same vitest instance
147-
// 2. have changed the vitest setup file contents (see FileCommunicator.setMutantRun)
148-
// 3. the vitest setup file is inlined (see VitestTestRunner.init)
149-
// 4. we're not using the vitest watch mode
150-
// We need to invalidate the module cache for the vitest setup file
151-
// See https://github.com/vitest-dev/vitest/issues/3409#issuecomment-1555884513
152-
this.ctx!.projects.forEach((project) => {
153-
const { moduleGraph } = project.vite;
154-
const module = moduleGraph.getModuleById(this.fileCommunicator.vitestSetup);
155-
if (module) {
156-
moduleGraph.invalidateModule(module);
157-
}
158-
});
159150
}
160151

161152
private readHitCount() {
@@ -204,7 +195,7 @@ export class VitestTestRunner implements TestRunner {
204195
}
205196

206197
public async dispose(): Promise<void> {
207-
await this.fileCommunicator.dispose();
198+
// await this.fileCommunicator.dispose();
208199
await this.ctx?.close();
209200
}
210201
}

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)