@hughescr/stryker-bun-runner 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/coverage/preload-logic.js +1 -34
- package/dist/index.d.ts +34 -0
- package/dist/index.js +4444 -212
- package/dist/templates/coverage-preload.ts +83 -22
- package/package.json +12 -10
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
setActiveMutant,
|
|
13
13
|
formatCoverageData,
|
|
14
14
|
writeCoverageToFile,
|
|
15
|
-
parseWebSocketMessage,
|
|
16
|
-
createTestCounter,
|
|
17
15
|
type StrykerNamespace
|
|
18
16
|
} from '__PRELOAD_LOGIC_PATH__';
|
|
19
17
|
|
|
@@ -34,6 +32,14 @@ interface StrykerGlobal {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
// Eager modules list — placeholder replaced at generation time with a sorted JSON array of absolute
|
|
36
|
+
// paths to all source files being mutated. Importing each module here (before any test code runs,
|
|
37
|
+
// while strykerGlobal.currentTestId is undefined) ensures that all module-level top-level code is
|
|
38
|
+
// executed in the "static" coverage bucket rather than the "perTest" bucket of whichever test
|
|
39
|
+
// happened to trigger the first import. This makes coverage collection deterministic across runs.
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Placeholder replaced at generation time; value is always a valid JSON array literal
|
|
41
|
+
const EAGER_MODULES: string[] = __EAGER_MODULES__;
|
|
42
|
+
|
|
37
43
|
// Get environment variables
|
|
38
44
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
39
45
|
const config = getPreloadConfig();
|
|
@@ -49,23 +55,37 @@ const shouldCollectCoverage = shouldCollect(config);
|
|
|
49
55
|
// ============================================================================
|
|
50
56
|
let ws: WebSocket | null = null;
|
|
51
57
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
58
|
+
// Per-file test counters for per-test coverage tracking.
|
|
59
|
+
//
|
|
60
|
+
// Bun runs multiple test files sequentially inside the same worker process,
|
|
61
|
+
// sharing a single preload module instance. A module-level counter would
|
|
62
|
+
// increment globally across all files, making position N for file B mean
|
|
63
|
+
// "the (N)th test of ALL files combined" rather than "the (N)th test of B".
|
|
64
|
+
// The coverage mapper expects per-file counters that restart at 1 for each
|
|
65
|
+
// file, so we keep a Map<filePrefix, count> and reset per-file naturally.
|
|
66
|
+
//
|
|
67
|
+
// Bun.main is read DYNAMICALLY inside beforeEach (not at module init time)
|
|
68
|
+
// because it changes to reflect the currently-executing test file.
|
|
69
|
+
const perFileCounters = new Map<string, number>();
|
|
70
|
+
|
|
71
|
+
// Helper: extract a stable relative file prefix from a Bun.main absolute path.
|
|
72
|
+
// Strips the Stryker sandbox prefix so keys are portable across runs.
|
|
73
|
+
function extractFilePrefix(bunMain: string): string {
|
|
74
|
+
if(!bunMain) {
|
|
75
|
+
return 'unknown';
|
|
76
|
+
}
|
|
77
|
+
// Stryker disable next-line Regex: sandbox path extraction pattern
|
|
78
|
+
const sandboxMatch = /\.stryker-tmp\/sandbox-[^/]+\/(.+)$/.exec(bunMain);
|
|
79
|
+
return sandboxMatch ? sandboxMatch[1] : bunMain.replace(/^.*\//, '');
|
|
80
|
+
}
|
|
55
81
|
|
|
56
82
|
if(syncPort && shouldCollectCoverage) {
|
|
57
83
|
try {
|
|
58
84
|
ws = new WebSocket(`ws://localhost:${syncPort}/sync`);
|
|
59
85
|
|
|
60
|
-
ws.onmessage = (
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
const parsedMessage = parseWebSocketMessage(data);
|
|
64
|
-
|
|
65
|
-
if(parsedMessage === 'ready') {
|
|
66
|
-
// Initial ready signal - tests can start
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
86
|
+
ws.onmessage = (_event) => {
|
|
87
|
+
// Messages from the sync server are only 'ready' signals.
|
|
88
|
+
// No per-test relay needed — coverage keys are file-prefixed counters.
|
|
69
89
|
};
|
|
70
90
|
|
|
71
91
|
// Wait for ready signal
|
|
@@ -148,7 +168,29 @@ if(activeMutant) {
|
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
// ============================================================================
|
|
151
|
-
// Section 3:
|
|
171
|
+
// Section 3: Eager Module Imports (deterministic static coverage)
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Force all src modules to execute their top-level code during preload,
|
|
174
|
+
// while strykerGlobal.currentTestId is undefined. Module-level mutants
|
|
175
|
+
// then deterministically record to the `static` bucket instead of the
|
|
176
|
+
// `perTest` entry of whichever test happened to trigger the import first.
|
|
177
|
+
//
|
|
178
|
+
// This block is skipped during mutant runs (shouldCollectCoverage is false)
|
|
179
|
+
// so mutant runs do not pay the startup cost of importing every source file.
|
|
180
|
+
if(shouldCollectCoverage) {
|
|
181
|
+
for(const modPath of EAGER_MODULES) {
|
|
182
|
+
try {
|
|
183
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential eager imports are intentional; parallel import would race on module-level side effects
|
|
184
|
+
await import(modPath);
|
|
185
|
+
} catch(err) {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- err.message may exist at runtime
|
|
187
|
+
console.warn(`[Stryker] Eager import failed for ${modPath}:`, err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Section 4: Coverage Writing Logic
|
|
152
194
|
// ============================================================================
|
|
153
195
|
|
|
154
196
|
// Shared coverage writing logic
|
|
@@ -157,8 +199,10 @@ const writeCoverageData = () => {
|
|
|
157
199
|
return;
|
|
158
200
|
}
|
|
159
201
|
|
|
202
|
+
// counterToName is not populated (test names are resolved by coverage-mapper
|
|
203
|
+
// from the inspector data, not stored here), so pass an empty Map.
|
|
160
204
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- Placeholder import replaced at runtime
|
|
161
|
-
const data = formatCoverageData(strykerGlobal.mutantCoverage,
|
|
205
|
+
const data = formatCoverageData(strykerGlobal.mutantCoverage, new Map<string, string>());
|
|
162
206
|
|
|
163
207
|
try {
|
|
164
208
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
@@ -169,17 +213,34 @@ const writeCoverageData = () => {
|
|
|
169
213
|
};
|
|
170
214
|
|
|
171
215
|
// ============================================================================
|
|
172
|
-
// Section
|
|
216
|
+
// Section 5: Test Hooks (for per-test coverage tracking)
|
|
173
217
|
// ============================================================================
|
|
174
218
|
if(shouldCollectCoverage) {
|
|
175
219
|
beforeEach(() => {
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
//
|
|
220
|
+
// Assign a stable, per-file test ID by combining the normalized test
|
|
221
|
+
// file path with a per-file counter.
|
|
222
|
+
//
|
|
223
|
+
// Key design constraints:
|
|
224
|
+
// 1. Bun runs multiple test files sequentially in one worker process,
|
|
225
|
+
// so the preload module is initialized ONCE for the whole run.
|
|
226
|
+
// 2. However, Bun.main IS updated to the currently-running test file
|
|
227
|
+
// by the time each beforeEach fires.
|
|
228
|
+
// 3. The coverage-mapper expects counters to restart at 1 per file
|
|
229
|
+
// (e.g. "tests/foo.test.ts@@test-1", "tests/bar.test.ts@@test-1"),
|
|
230
|
+
// so we track a separate counter per file prefix.
|
|
231
|
+
//
|
|
232
|
+
// @ts-expect-error -- Bun global is available at runtime but not in TS typings
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Bun global accessed at runtime
|
|
234
|
+
const bunMain = String((globalThis as unknown as { Bun?: { main?: string } }).Bun?.main ?? '');
|
|
235
|
+
const filePrefix = extractFilePrefix(bunMain);
|
|
236
|
+
const prevCount = perFileCounters.get(filePrefix) ?? 0;
|
|
237
|
+
const nextCount = prevCount + 1;
|
|
238
|
+
perFileCounters.set(filePrefix, nextCount);
|
|
239
|
+
const testId = `${filePrefix}@@test-${nextCount}`;
|
|
179
240
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
|
|
180
|
-
strykerGlobal.currentTestId =
|
|
241
|
+
strykerGlobal.currentTestId = testId;
|
|
181
242
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
|
|
182
|
-
mutantCoverage.perTest[
|
|
243
|
+
mutantCoverage.perTest[testId] ??= {};
|
|
183
244
|
});
|
|
184
245
|
|
|
185
246
|
afterEach(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hughescr/stryker-bun-runner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Stryker test runner plugin for Bun with perTest coverage support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stryker",
|
|
@@ -41,21 +41,23 @@
|
|
|
41
41
|
"postversion": "git commit -m \"Bump package version to $npm_package_version\" package.json; git flow release start $npm_package_version; git flow release finish -m $npm_package_version $npm_package_version; git checkout develop; git merge main"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@stryker-mutator/api": "9.
|
|
45
|
-
"
|
|
44
|
+
"@stryker-mutator/api": "9.6.1",
|
|
45
|
+
"smol-toml": "1.6.1",
|
|
46
|
+
"tinyglobby": "0.2.16",
|
|
47
|
+
"ws": "8.20.0"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
|
-
"@hughescr/eslint-config-default": "
|
|
49
|
-
"@stryker-mutator/core": "9.
|
|
50
|
-
"@stryker-mutator/typescript-checker": "9.
|
|
51
|
-
"@types/bun": "1.3.
|
|
52
|
-
"@types/node": "25.
|
|
50
|
+
"@hughescr/eslint-config-default": "5.1.0",
|
|
51
|
+
"@stryker-mutator/core": "9.6.1",
|
|
52
|
+
"@stryker-mutator/typescript-checker": "9.6.1",
|
|
53
|
+
"@types/bun": "1.3.13",
|
|
54
|
+
"@types/node": "25.6.0",
|
|
53
55
|
"@types/ws": "8.18.1",
|
|
54
56
|
"dts-bundle-generator": "9.5.1",
|
|
55
|
-
"eslint": "
|
|
57
|
+
"eslint": "10.2.1",
|
|
56
58
|
"eslint-formatter-overview": "2.0.0",
|
|
57
59
|
"eslint-formatter-unix": "9.0.1",
|
|
58
|
-
"typescript": "
|
|
60
|
+
"typescript": "6.0.3"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
61
63
|
"@stryker-mutator/core": "^9.0.0"
|