oxmutate 1.0.3 → 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 +18 -9
- package/binding/oxmutate.darwin-arm64.node +0 -0
- package/binding/oxmutate.darwin-x64.node +0 -0
- package/binding/oxmutate.linux-arm64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-musl.node +0 -0
- package/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/checker.js +1 -0
- package/dist/child.js +27 -1
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.js +193 -29
- package/dist/directives.js +55 -0
- package/dist/heartbeat.js +111 -0
- package/dist/lanes.js +1 -0
- package/dist/progress.js +1 -0
- package/dist/reports.js +1 -1
- package/dist/runner.d.ts +21 -0
- package/dist/runner.js +270 -11
- package/dist/runtime.js +1 -0
- package/dist/scheduler.js +51 -12
- package/dist/spec-oracle.js +1 -0
- package/package.json +1 -1
package/dist/scheduler.js
CHANGED
|
@@ -12,6 +12,8 @@ import { fork } from 'node:child_process';
|
|
|
12
12
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { beatFile, classifyStall, CPU_WINDOW_MS, cpuShare, readBeat } from './heartbeat.js';
|
|
16
|
+
import { env, stderr } from './runtime.js';
|
|
15
17
|
const childJs = fileURLToPath(new URL('./child.js', import.meta.url));
|
|
16
18
|
/** The tail of a server's stderr kept for an error message. */
|
|
17
19
|
const STDERR_TAIL = 4000;
|
|
@@ -28,15 +30,23 @@ function inFlight(progress) {
|
|
|
28
30
|
return undefined;
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
const PERCENT = 100;
|
|
34
|
+
/** How many CPU samples a server keeps: a little over the window. */
|
|
35
|
+
const CPU_SAMPLES_KEPT = 8;
|
|
31
36
|
class Server {
|
|
32
37
|
child;
|
|
33
38
|
ready;
|
|
34
39
|
stderr = '';
|
|
40
|
+
/** Where this server's test workers beat (heartbeat.ts). */
|
|
41
|
+
beat;
|
|
42
|
+
/** The server's latest CPU readings, oldest first (child.ts sends them). */
|
|
43
|
+
cpu = [];
|
|
35
44
|
onMessage;
|
|
36
45
|
onExit;
|
|
37
46
|
exited = false;
|
|
38
|
-
constructor(configFile, cwd, execArgv) {
|
|
47
|
+
constructor(configFile, cwd, execArgv, workDir) {
|
|
39
48
|
this.child = fork(childJs, [configFile], { cwd, stdio: ['ignore', 'ignore', 'pipe', 'ipc'], execArgv });
|
|
49
|
+
this.beat = beatFile(workDir, this.child.pid);
|
|
40
50
|
this.child.stderr?.on('data', (d) => (this.stderr = (this.stderr + d.toString()).slice(-STDERR_TAIL)));
|
|
41
51
|
this.ready = new Promise((resolve) => {
|
|
42
52
|
const first = (m) => {
|
|
@@ -48,7 +58,15 @@ class Server {
|
|
|
48
58
|
this.child.on('message', first);
|
|
49
59
|
this.child.once('exit', () => resolve(false));
|
|
50
60
|
});
|
|
51
|
-
this.child.on('message', (m) =>
|
|
61
|
+
this.child.on('message', (m) => {
|
|
62
|
+
if (m.type === 'cpu') {
|
|
63
|
+
this.cpu.push({ at: m.at ?? 0, us: m.us ?? 0 });
|
|
64
|
+
if (this.cpu.length > CPU_SAMPLES_KEPT)
|
|
65
|
+
this.cpu.shift();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.onMessage?.(m);
|
|
69
|
+
});
|
|
52
70
|
this.child.once('exit', (code) => {
|
|
53
71
|
this.exited = true;
|
|
54
72
|
this.onExit?.(code);
|
|
@@ -78,9 +96,18 @@ class Server {
|
|
|
78
96
|
if (!line)
|
|
79
97
|
return;
|
|
80
98
|
const [id = '', since = '0'] = line.split(' ');
|
|
81
|
-
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (now - Number(since) > budget(id)) {
|
|
101
|
+
// Read before the kill: an idle stop is a Timeout as it stands, a busy one is re-run (heartbeat.ts).
|
|
102
|
+
const beat = readBeat(this.beat, now);
|
|
103
|
+
const share = cpuShare(this.cpu, now);
|
|
104
|
+
const stall = classifyStall({ beat, cpuShare: share });
|
|
105
|
+
if (env['OXMUTATE_DEBUG_WATCHDOG']) {
|
|
106
|
+
const cpu = share === undefined ? '?' : `${String(Math.round(share * PERCENT))}%`;
|
|
107
|
+
stderr.write(`oxmutate watchdog: stop ${id} after ${String(now - Number(since))} ms; ${stall}; beat ${Number.isFinite(beat.age) ? `${String(beat.age)} ms ago` : 'never'}${beat.inTest ? ' in a test' : ''}; cpu ${cpu} over ${String(CPU_WINDOW_MS)} ms\n`);
|
|
108
|
+
}
|
|
82
109
|
this.child.kill('SIGKILL');
|
|
83
|
-
finish({ killedFor: id, stderr: this.stderr });
|
|
110
|
+
finish({ killedFor: id, stall, stderr: this.stderr });
|
|
84
111
|
}
|
|
85
112
|
}, WATCHDOG_POLL_MS);
|
|
86
113
|
this.onMessage = (m) => {
|
|
@@ -104,13 +131,21 @@ class Server {
|
|
|
104
131
|
export function createPool(config, size, workDir, tag) {
|
|
105
132
|
const configFile = path.join(workDir, `server-${tag}.json`);
|
|
106
133
|
writeFileSync(configFile, JSON.stringify(config));
|
|
107
|
-
const stats = { spawned: 0, killed: 0, crashed: 0 };
|
|
134
|
+
const stats = { spawned: 0, killed: 0, idle: 0, crashed: 0 };
|
|
108
135
|
const spawn = () => {
|
|
109
136
|
stats.spawned++;
|
|
110
|
-
return new Server(configFile, config.root, config.nodeArgs ?? []);
|
|
137
|
+
return new Server(configFile, config.root, config.nodeArgs ?? [], workDir);
|
|
111
138
|
};
|
|
112
139
|
const servers = Array.from({ length: size }, spawn);
|
|
113
140
|
let spare = spawn();
|
|
141
|
+
const tally = (outcome) => {
|
|
142
|
+
if (outcome.killedFor)
|
|
143
|
+
stats.killed++;
|
|
144
|
+
if (outcome.stall === 'idle')
|
|
145
|
+
stats.idle++;
|
|
146
|
+
if (outcome.crashed)
|
|
147
|
+
stats.crashed++;
|
|
148
|
+
};
|
|
114
149
|
const replacement = () => {
|
|
115
150
|
const next = spare ?? spawn();
|
|
116
151
|
spare = spawn();
|
|
@@ -118,7 +153,7 @@ export function createPool(config, size, workDir, tag) {
|
|
|
118
153
|
};
|
|
119
154
|
return {
|
|
120
155
|
stats,
|
|
121
|
-
async drain(units, budget, after) {
|
|
156
|
+
async drain(units, budget, after, prepare) {
|
|
122
157
|
const queue = [...units];
|
|
123
158
|
let busy = 0;
|
|
124
159
|
const waiters = [];
|
|
@@ -126,9 +161,16 @@ export function createPool(config, size, workDir, tag) {
|
|
|
126
161
|
for (const w of waiters.splice(0))
|
|
127
162
|
w();
|
|
128
163
|
};
|
|
164
|
+
/** The next unit `prepare` keeps, or undefined when the queue is empty. */
|
|
165
|
+
const take = () => {
|
|
166
|
+
for (let unit = queue.shift(); unit; unit = queue.shift())
|
|
167
|
+
if (!prepare || prepare(unit))
|
|
168
|
+
return unit;
|
|
169
|
+
return undefined;
|
|
170
|
+
};
|
|
129
171
|
await Promise.all(servers.map(async (_, slot) => {
|
|
130
172
|
for (;;) {
|
|
131
|
-
const unit =
|
|
173
|
+
const unit = take();
|
|
132
174
|
if (!unit) {
|
|
133
175
|
if (busy === 0) {
|
|
134
176
|
signal();
|
|
@@ -145,10 +187,7 @@ export function createPool(config, size, workDir, tag) {
|
|
|
145
187
|
await server.ready;
|
|
146
188
|
}
|
|
147
189
|
const outcome = await server.run(unit, (id) => budget(unit, id));
|
|
148
|
-
|
|
149
|
-
stats.killed++;
|
|
150
|
-
if (outcome.crashed)
|
|
151
|
-
stats.crashed++;
|
|
190
|
+
tally(outcome);
|
|
152
191
|
if (outcome.killedFor || outcome.crashed) {
|
|
153
192
|
servers[slot] = replacement();
|
|
154
193
|
}
|
package/dist/spec-oracle.js
CHANGED
|
@@ -45,6 +45,7 @@ const share = (n, of) => (of ? (PERCENT * n) / of : 0);
|
|
|
45
45
|
const located = ({ id, file, line, mutatorName, replacement }) => ({ id, file, line, mutatorName, replacement });
|
|
46
46
|
const median = (xs) => {
|
|
47
47
|
const sorted = [...xs].sort((a, b) => a - b);
|
|
48
|
+
// Stryker disable next-line NumericLiteral: guarded by `sorted.length`, so the fallback never runs
|
|
48
49
|
return sorted.length ? (sorted[Math.floor(sorted.length / 2)] ?? 0) : 0;
|
|
49
50
|
};
|
|
50
51
|
const testsMiss = (m) => m.status === 'Survived' || m.status === 'NoCoverage';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Mutation testing for JavaScript and TypeScript on the oxc engine, run warm inside the project's own Vitest, with CI lanes, SARIF and an agent surface. No dependency outside the burgee family. A Stryker migration path.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|