@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.
@@ -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
- // Track test counter (only needed when collecting coverage)
53
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
54
- const testCounter = createTestCounter();
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 = (event) => {
61
- const data = String(event.data);
62
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
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: Coverage Writing Logic
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, testCounter.getCounterToNameMap());
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 4: Test Hooks (for per-test coverage tracking)
216
+ // Section 5: Test Hooks (for per-test coverage tracking)
173
217
  // ============================================================================
174
218
  if(shouldCollectCoverage) {
175
219
  beforeEach(() => {
176
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- TestCounter from placeholder import
177
- const counterId = testCounter.increment();
178
- // Always use counter-based IDs - coverage-mapper remaps to full names using inspector data
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 = counterId;
241
+ strykerGlobal.currentTestId = testId;
181
242
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
182
- mutantCoverage.perTest[counterId] ??= {};
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.1.2",
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.5.1",
45
- "ws": "8.19.0"
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": "4.0.1",
49
- "@stryker-mutator/core": "9.5.1",
50
- "@stryker-mutator/typescript-checker": "9.5.1",
51
- "@types/bun": "1.3.8",
52
- "@types/node": "25.2.0",
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": "9.39.2",
57
+ "eslint": "10.2.1",
56
58
  "eslint-formatter-overview": "2.0.0",
57
59
  "eslint-formatter-unix": "9.0.1",
58
- "typescript": "5.9.3"
60
+ "typescript": "6.0.3"
59
61
  },
60
62
  "peerDependencies": {
61
63
  "@stryker-mutator/core": "^9.0.0"