@hughescr/stryker-bun-runner 1.0.0-beta1
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/LICENSE.md +77 -0
- package/README.md +110 -0
- package/dist/coverage/preload-logic.js +84 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +4019 -0
- package/dist/templates/coverage-preload.ts +211 -0
- package/package.json +66 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage preload script
|
|
3
|
+
* This script is loaded before tests run to collect mutation coverage data
|
|
4
|
+
* Note: This is a template file with a placeholder import that gets replaced at runtime
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { beforeEach, afterEach, afterAll } from 'bun:test';
|
|
8
|
+
import {
|
|
9
|
+
getPreloadConfig,
|
|
10
|
+
shouldCollectCoverage as shouldCollect,
|
|
11
|
+
initializeStrykerNamespace,
|
|
12
|
+
setActiveMutant,
|
|
13
|
+
formatCoverageData,
|
|
14
|
+
writeCoverageToFile,
|
|
15
|
+
parseWebSocketMessage,
|
|
16
|
+
createTestCounter,
|
|
17
|
+
type StrykerNamespace
|
|
18
|
+
} from '__PRELOAD_LOGIC_PATH__';
|
|
19
|
+
|
|
20
|
+
interface StrykerGlobal {
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
__stryker__?: StrykerNamespace
|
|
23
|
+
__mutantCoverage__?: {
|
|
24
|
+
'static': Record<string, number>
|
|
25
|
+
perTest: Record<string, Record<string, number>>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get environment variables
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
31
|
+
const config = getPreloadConfig();
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Config from placeholder import
|
|
33
|
+
const { syncPort, coverageFile, activeMutant } = config;
|
|
34
|
+
|
|
35
|
+
// Skip coverage collection during mutant runs (only need pass/fail)
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
37
|
+
const shouldCollectCoverage = shouldCollect(config);
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Section 1: WebSocket Sync (receive test start events)
|
|
41
|
+
// ============================================================================
|
|
42
|
+
let ws: WebSocket | null = null;
|
|
43
|
+
|
|
44
|
+
// Track test counter and WebSocket-provided names (only needed when collecting coverage)
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
46
|
+
const testCounter = createTestCounter();
|
|
47
|
+
let pendingTestName: string | undefined;
|
|
48
|
+
|
|
49
|
+
if(shouldCollectCoverage) {
|
|
50
|
+
// Coverage collection will use these variables
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if(syncPort && shouldCollectCoverage) {
|
|
54
|
+
try {
|
|
55
|
+
ws = new WebSocket(`ws://localhost:${syncPort}/sync`);
|
|
56
|
+
|
|
57
|
+
ws.onmessage = (event) => {
|
|
58
|
+
const data = String(event.data);
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
60
|
+
const parsedMessage = parseWebSocketMessage(data);
|
|
61
|
+
|
|
62
|
+
if(parsedMessage === 'ready') {
|
|
63
|
+
// Initial ready signal - tests can start
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Parsed message has dynamic type
|
|
68
|
+
if(parsedMessage && typeof parsedMessage === 'object' && parsedMessage.type === 'testStart') {
|
|
69
|
+
// Store the pending test name to be picked up by beforeEach
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- Parsed message has dynamic type
|
|
71
|
+
pendingTestName = parsedMessage.name;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Wait for ready signal
|
|
76
|
+
await new Promise<void>((resolve) => {
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
console.warn('[Stryker] Timeout waiting for ready signal');
|
|
79
|
+
resolve();
|
|
80
|
+
}, 5000);
|
|
81
|
+
|
|
82
|
+
const wsInstance = ws;
|
|
83
|
+
if(!wsInstance) {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
resolve();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const originalOnMessage = wsInstance.onmessage;
|
|
90
|
+
wsInstance.onmessage = (event) => {
|
|
91
|
+
if(event.data === 'ready') {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
if(originalOnMessage) {
|
|
96
|
+
originalOnMessage.call(wsInstance, event);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
wsInstance.onerror = () => {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
console.warn('[Stryker] Failed to connect to sync server');
|
|
103
|
+
resolve();
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn('[Stryker] Error during synchronization:', error);
|
|
108
|
+
}
|
|
109
|
+
} else if(syncPort && !shouldCollectCoverage) {
|
|
110
|
+
// No coverage collection, just wait for ready signal
|
|
111
|
+
try {
|
|
112
|
+
const ws = new WebSocket(`ws://localhost:${syncPort}/sync`);
|
|
113
|
+
await new Promise<void>((resolve) => {
|
|
114
|
+
const timeout = setTimeout(() => {
|
|
115
|
+
ws.close();
|
|
116
|
+
console.warn('[Stryker Sync] Timeout waiting for ready signal, proceeding anyway');
|
|
117
|
+
resolve();
|
|
118
|
+
}, 5000);
|
|
119
|
+
|
|
120
|
+
ws.onmessage = (event) => {
|
|
121
|
+
if(event.data === 'ready') {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
ws.close();
|
|
124
|
+
resolve();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
ws.onerror = () => {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
console.warn('[Stryker Sync] Failed to connect to sync server, proceeding anyway');
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.warn('[Stryker Sync] Error during synchronization, proceeding anyway:', error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Section 2: Initialize Stryker Namespace
|
|
141
|
+
// ============================================================================
|
|
142
|
+
const g = globalThis as unknown as StrykerGlobal;
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
144
|
+
const strykerGlobal = initializeStrykerNamespace(g as Record<string, unknown>);
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
|
|
146
|
+
const mutantCoverage = strykerGlobal.mutantCoverage!;
|
|
147
|
+
|
|
148
|
+
// Set active mutant for mutant runs
|
|
149
|
+
if(activeMutant) {
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
151
|
+
setActiveMutant(strykerGlobal, activeMutant);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Section 3: Coverage Writing Logic
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
// Shared coverage writing logic
|
|
159
|
+
const writeCoverageData = () => {
|
|
160
|
+
if(!shouldCollectCoverage || !coverageFile) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 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
|
|
165
|
+
const data = formatCoverageData(strykerGlobal.mutantCoverage, testCounter.getCounterToNameMap());
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
|
|
169
|
+
writeCoverageToFile(coverageFile, data);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[Stryker Coverage] Failed to write coverage:', error);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Section 4: Test Hooks (for per-test coverage tracking)
|
|
177
|
+
// ============================================================================
|
|
178
|
+
if(shouldCollectCoverage) {
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- TestCounter from placeholder import
|
|
181
|
+
const counterId = testCounter.increment();
|
|
182
|
+
|
|
183
|
+
// If we have a pending test name from WebSocket, use it
|
|
184
|
+
if(pendingTestName) {
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- TestCounter from placeholder import
|
|
186
|
+
testCounter.setName(counterId, pendingTestName);
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
|
|
188
|
+
strykerGlobal.currentTestId = pendingTestName;
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
|
|
190
|
+
mutantCoverage.perTest[pendingTestName] ??= {};
|
|
191
|
+
pendingTestName = undefined;
|
|
192
|
+
} else {
|
|
193
|
+
// Fallback to counter - will be remapped later
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
|
|
195
|
+
strykerGlobal.currentTestId = counterId;
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
|
|
197
|
+
mutantCoverage.perTest[counterId] ??= {};
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
// Clear currentTestId so any subsequent code records to static
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
|
|
204
|
+
strykerGlobal.currentTestId = undefined;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
afterAll(() => {
|
|
208
|
+
ws?.close();
|
|
209
|
+
writeCoverageData();
|
|
210
|
+
});
|
|
211
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hughescr/stryker-bun-runner",
|
|
3
|
+
"version": "1.0.0-beta1",
|
|
4
|
+
"description": "Stryker test runner plugin for Bun with perTest coverage support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stryker",
|
|
7
|
+
"stryker-plugin",
|
|
8
|
+
"bun",
|
|
9
|
+
"test-runner",
|
|
10
|
+
"mutation-testing"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/hughescr/stryker-bun-runner#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/hughescr/stryker-bun-runner/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/hughescr/stryker-bun-runner.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"author": "hughescr",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm && bun build src/coverage/preload-logic.ts --outdir dist/coverage --target bun --format esm && dts-bundle-generator --config dts-bundle-generator.config.json && cp -r src/templates dist/",
|
|
36
|
+
"lint": "eslint --cache --format unix .",
|
|
37
|
+
"mutate": "stryker run",
|
|
38
|
+
"prepublishOnly": "bun run build",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@stryker-mutator/api": "9.4.0",
|
|
44
|
+
"ws": "8.19.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@hughescr/eslint-config-default": "4.0.1",
|
|
48
|
+
"@stryker-mutator/core": "9.4.0",
|
|
49
|
+
"@stryker-mutator/typescript-checker": "9.4.0",
|
|
50
|
+
"@types/bun": "1.3.6",
|
|
51
|
+
"@types/node": "25.0.9",
|
|
52
|
+
"@types/ws": "8.18.1",
|
|
53
|
+
"dts-bundle-generator": "9.5.1",
|
|
54
|
+
"eslint": "9.39.2",
|
|
55
|
+
"eslint-formatter-overview": "2.0.0",
|
|
56
|
+
"eslint-formatter-unix": "9.0.1",
|
|
57
|
+
"eslint-plugin-package-json": "0.88.1",
|
|
58
|
+
"typescript": "5.9.3"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@stryker-mutator/core": "^9.0.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"bun": ">1.3.6"
|
|
65
|
+
}
|
|
66
|
+
}
|