oxmutate 1.2.0 → 1.2.2
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/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/bin.js +0 -1
- package/dist/checker.js +1 -32
- package/dist/child.js +4 -46
- package/dist/cli.js +0 -10
- package/dist/command-runner.js +0 -27
- package/dist/config.js +0 -29
- package/dist/coordinator.js +5 -129
- package/dist/directives.js +0 -17
- package/dist/files.js +0 -10
- package/dist/frozen.js +0 -15
- package/dist/heartbeat.js +1 -50
- package/dist/index.js +0 -4
- package/dist/lanes.js +1 -30
- package/dist/native.js +0 -13
- package/dist/platform.js +0 -14
- package/dist/progress.js +0 -9
- package/dist/reports.js +0 -28
- package/dist/resolve.js +0 -10
- package/dist/runner.js +6 -115
- package/dist/runtime.js +0 -16
- package/dist/scheduler.js +0 -20
- package/dist/spec-oracle.js +0 -23
- package/dist/spec.js +0 -28
- package/dist/vite-plugin.js +0 -5
- package/dist/vite.js +0 -2
- package/package.json +2 -2
package/dist/coordinator.js
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One mutation run, end to end:
|
|
3
|
-
*
|
|
4
|
-
* instrument (native, parallel) → dry run ×2 (coverage, timings, nondeterministic tests)
|
|
5
|
-
* → incremental reuse → plan → warm units on a work-stealing server pool, under a watchdog
|
|
6
|
-
* → isolated re-runs (static mutants, canary-flagged files, unfinished work, flaky verdicts)
|
|
7
|
-
* → verdicts
|
|
8
|
-
*
|
|
9
|
-
* Every decision that can make a verdict wrong is guarded and counted in `stats`, so a run says
|
|
10
|
-
* what it had to correct, not just what it concluded.
|
|
11
|
-
*/
|
|
12
1
|
import { createHash } from 'node:crypto';
|
|
13
2
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
3
|
import { tmpdir } from 'node:os';
|
|
@@ -37,22 +26,12 @@ function readRecords(file) {
|
|
|
37
26
|
.map((l) => JSON.parse(l));
|
|
38
27
|
}
|
|
39
28
|
const sha = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
40
|
-
/**
|
|
41
|
-
* Work units of at most this many (mutant, test) executions: small enough to steal, large enough
|
|
42
|
-
* that each unit's fresh worker, which imports the test file's whole graph, is paid for few units.
|
|
43
|
-
* At 120, module loading was most of the CPU on burgee/controlroom (React/Ink in every worker); 400
|
|
44
|
-
* cut units 172 -> 93 and wall 44 -> 39 s (paratext 39 -> 35 s), and 1000 gained nothing more
|
|
45
|
-
* (2026-10-30). Smaller stays better for a pollution restart, which re-runs the rest of a unit.
|
|
46
|
-
*/
|
|
47
29
|
const UNIT_PAIRS = 400;
|
|
48
|
-
/** How many times its isolated budget a busy stop gets on its re-run. */
|
|
49
30
|
const RERUN_SLACK = 3;
|
|
50
31
|
const MS_PER_MINUTE = 60_000;
|
|
51
32
|
const PERCENT = 100;
|
|
52
|
-
/** Every test that reaches a mutant: the score's, then a spec oracle's replay tests. */
|
|
53
33
|
const allCovering = (m) => [...m.coveredBy, ...(m.specCoveredBy ?? [])];
|
|
54
34
|
const percent = (n, of) => (of ? (PERCENT * n) / of : PERCENT);
|
|
55
|
-
/** Stryker's score: detected over everything valid and not ignored; `covered` leaves out no-coverage. */
|
|
56
35
|
function scoreOf(mutants) {
|
|
57
36
|
const scored = mutants.filter((m) => !['Ignored', 'CompileError', 'RuntimeError'].includes(m.status));
|
|
58
37
|
const detected = scored.filter((m) => m.status === 'Killed' || m.status === 'Timeout').length;
|
|
@@ -73,8 +52,6 @@ export async function run(options) {
|
|
|
73
52
|
const progress = startProgress(options.reporters, stderr);
|
|
74
53
|
try {
|
|
75
54
|
progress.phase('instrumenting');
|
|
76
|
-
// ── 1. Instrument ─────────────────────────────────────────────────────────────────────────
|
|
77
|
-
// Stryker's `file:line-line` entries select files by their glob and mutants by their range.
|
|
78
55
|
const specs = options.mutate.map(parseMutateSpec);
|
|
79
56
|
const files = matchFiles(root, specs.map((x) => x.glob));
|
|
80
57
|
const rangesFor = (file) => {
|
|
@@ -127,16 +104,13 @@ export async function run(options) {
|
|
|
127
104
|
stats.files = files.length;
|
|
128
105
|
stats.mutants = mutants.length;
|
|
129
106
|
const mapFile = path.join(work, 'instrumented.json');
|
|
130
|
-
// The frozen oracle's base-ref test files ride along in the same map (frozen.ts).
|
|
131
107
|
writeFileSync(mapFile, JSON.stringify({ ...codeMap, ...options.testOverrides }));
|
|
132
|
-
// ── 1b. The build, once, before anything runs (Stryker's `buildCommand`) ─────────────────
|
|
133
108
|
if (options.buildCommand) {
|
|
134
109
|
const [command = '', ...args] = options.buildCommand.trim().split(/\s+/);
|
|
135
110
|
const built = await spawn(command, args, { cwd: root, runtime: ambientRuntime() });
|
|
136
111
|
if (!built.ok)
|
|
137
112
|
throw new InstrumentFault(`buildCommand \`${options.buildCommand}\` exited ${String(built.code)}: ${(built.stderr || built.stdout).trim().slice(-1500)}. fix: make the build pass on its own first`);
|
|
138
113
|
}
|
|
139
|
-
// ── 1c. The command runner: any test command, one process per mutant (command-runner.ts) ──
|
|
140
114
|
if (options.commandRunner) {
|
|
141
115
|
const m0 = performance.now();
|
|
142
116
|
try {
|
|
@@ -151,8 +125,6 @@ export async function run(options) {
|
|
|
151
125
|
stats.wallMs = performance.now() - wall0;
|
|
152
126
|
return { root, options, sources, mutants, tests: [], stats, score: scoreOf(mutants) };
|
|
153
127
|
}
|
|
154
|
-
// ── 2. Dry run, twice (the second only to find nondeterministic tests) ────────────────────
|
|
155
|
-
// Spec oracles' traces, generated once; servers inherit where they are (spec.ts reads it).
|
|
156
128
|
const traceDirs = {};
|
|
157
129
|
for (const oracle of options.specOracles)
|
|
158
130
|
traceDirs[oracle.name] = await tracesFor(oracle, root, work);
|
|
@@ -198,8 +170,6 @@ export async function run(options) {
|
|
|
198
170
|
throw new InstrumentFault(`${String(failing.length)} test(s) fail with no mutant active, so no verdict can be trusted: ${failing.slice(0, 5).map((t) => t.name).join('; ')}. fix: make the suite pass first`);
|
|
199
171
|
if (dry.tests.length === 0 && !options.allowEmpty)
|
|
200
172
|
throw new InstrumentFault('the dry run ran no tests. fix: check the Vitest config the run used, or set allowEmpty');
|
|
201
|
-
// The second dry run only finds nondeterministic tests, which matter at step 7: it runs beside
|
|
202
|
-
// the warm phase instead of before it (6 s of a 107 s burgee/controlroom run, 2026-10-27).
|
|
203
173
|
const findFlakyTests = async () => {
|
|
204
174
|
const flaky = new Set();
|
|
205
175
|
if (options.flakyReruns > 0) {
|
|
@@ -216,11 +186,10 @@ export async function run(options) {
|
|
|
216
186
|
return flaky;
|
|
217
187
|
};
|
|
218
188
|
const flakyCheck = findFlakyTests();
|
|
219
|
-
flakyCheck.catch(() => undefined);
|
|
189
|
+
flakyCheck.catch(() => undefined);
|
|
220
190
|
const testInfo = new Map(dry.tests.map((t) => [t.id, t]));
|
|
221
191
|
const coveringTests = new Map();
|
|
222
192
|
const hits = new Map();
|
|
223
|
-
/** Static mutant → the test files whose collection (imports, beforeAll) reached it. */
|
|
224
193
|
const staticFiles = new Map();
|
|
225
194
|
for (const [testId, perMutant] of Object.entries(dry.perTest)) {
|
|
226
195
|
if (testId.startsWith(FILE_KEY)) {
|
|
@@ -239,8 +208,6 @@ export async function run(options) {
|
|
|
239
208
|
}
|
|
240
209
|
}
|
|
241
210
|
const staticIds = new Set([...Object.keys(dry.static), ...staticFiles.keys()]);
|
|
242
|
-
// A spec oracle's replay tests, by test id → oracle name. They are not tests of the score:
|
|
243
|
-
// their kills are reported as the spec's, on a track of their own.
|
|
244
211
|
const specOf = new Map();
|
|
245
212
|
for (const oracle of options.specOracles) {
|
|
246
213
|
const replay = new Set(matchFiles(root, oracle.replay).map((f) => path.join(root, f)));
|
|
@@ -277,7 +244,6 @@ export async function run(options) {
|
|
|
277
244
|
const tests = dry.tests.map(({ id, file, name, ms }) => ({ id, file, name, ms }));
|
|
278
245
|
return { root, options, sources, mutants, tests, stats, score: { detected: 0, undetected: 0, total: 0, mutationScore: 0, covered: 0 }, dryRunOnly: true };
|
|
279
246
|
}
|
|
280
|
-
// ── 3. Incremental reuse ──────────────────────────────────────────────────────────────────
|
|
281
247
|
const fileHashes = new Map();
|
|
282
248
|
const fileHash = (abs) => {
|
|
283
249
|
let h = fileHashes.get(abs);
|
|
@@ -301,21 +267,12 @@ export async function run(options) {
|
|
|
301
267
|
stats.reused++;
|
|
302
268
|
}
|
|
303
269
|
}
|
|
304
|
-
// ── 4. Warm units on the server pool ──────────────────────────────────────────────────────
|
|
305
|
-
// Static mutants, hybrids included, never run warm: a module evaluated without the mutant
|
|
306
|
-
// cannot stand in for one evaluated with it. Tried 2026-10-27: warm kills of hybrids gave 3
|
|
307
|
-
// false kills on burgee/paratext (an import-time registration the tests then read).
|
|
308
270
|
const toRun = mutants.filter((m) => !m.reused && m.status !== 'Ignored' && !m.static && allCovering(m).length > 0);
|
|
309
271
|
const byId = new Map(mutants.map((m) => [m.id, m]));
|
|
310
|
-
/** The switch-taken count past which a run is a runaway: 100x what the dry run saw, at least 1000. */
|
|
311
272
|
const hitLimitOf = (id) => Math.max(1000, (hits.get(id) ?? 1) * 100);
|
|
312
273
|
const hitLimits = Object.fromEntries(toRun.map((m) => [m.id, hitLimitOf(m.id)]));
|
|
313
274
|
const maxTestMs = (id) => { const m = byId.get(id); return Math.max(0, ...(m ? allCovering(m) : []).map((t) => testInfo.get(t)?.ms ?? 0)); };
|
|
314
|
-
// The watchdog is for runaways, which loop in fast code; a slow test under a loaded machine is
|
|
315
|
-
// not one. 3x the covering test's dry-run time, not Stryker's 1.5x: at 1.5x, subprocess-heavy
|
|
316
|
-
// burgee/paratext tests were stopped as false timeouts (2026-10-27).
|
|
317
275
|
const warmBudget = (id) => options.timeoutMS + Math.max(options.timeoutFactor, 3) * maxTestMs(id) + 250;
|
|
318
|
-
// Files a mutant's covering tests live in, cheapest first (by the covering tests' time there).
|
|
319
276
|
const fileOrder = new Map();
|
|
320
277
|
for (const m of toRun) {
|
|
321
278
|
const cost = new Map();
|
|
@@ -335,7 +292,6 @@ export async function run(options) {
|
|
|
335
292
|
stats.units++;
|
|
336
293
|
return unit;
|
|
337
294
|
};
|
|
338
|
-
/** Split a file's plan into units of about UNIT_PAIRS executions, by mutant. */
|
|
339
295
|
const unitsFor = (file, plan) => {
|
|
340
296
|
const ids = [...new Set(Object.values(plan).flat())];
|
|
341
297
|
const pairs = Object.values(plan).reduce((sum, l) => sum + l.length, 0);
|
|
@@ -345,7 +301,6 @@ export async function run(options) {
|
|
|
345
301
|
return makeUnit(file, Object.fromEntries(Object.entries(plan).map(([t, l]) => [t, l.filter((id) => mine.has(id))]).filter(([, l]) => l.length > 0)));
|
|
346
302
|
});
|
|
347
303
|
};
|
|
348
|
-
/** Units for these mutants, each planned only in the files `pick` selects for it. */
|
|
349
304
|
const unitsForRound = (mutantsHere, pick) => {
|
|
350
305
|
const perFile = new Map();
|
|
351
306
|
for (const m of mutantsHere) {
|
|
@@ -363,22 +318,16 @@ export async function run(options) {
|
|
|
363
318
|
}
|
|
364
319
|
}
|
|
365
320
|
const units = [...perFile].flatMap(([file, tests]) => unitsFor(file, Object.fromEntries(tests)));
|
|
366
|
-
// Sizing and ordering units by dry-run time instead of pair count (longest first) was tried:
|
|
367
|
-
// 30% more units and ~10% more CPU on burgee/controlroom, and no wall-clock gain that this
|
|
368
|
-
// machine's load (average 21 to 76 across the A/B) could show (2026-10-27).
|
|
369
321
|
const weight = (u) => Object.values(planUnits.get(u.results)?.plan ?? {}).reduce((sum, l) => sum + l.length, 0);
|
|
370
322
|
return units.sort((a, b) => weight(b) - weight(a));
|
|
371
323
|
};
|
|
372
324
|
const records = [];
|
|
373
325
|
const runaways = new Set();
|
|
374
326
|
const crashedIds = new Set();
|
|
375
|
-
// Dependencies inlined here too, so the static phase (5b) reuses these servers instead of starting its own.
|
|
376
327
|
const pool = createPool({ root, vitestConfig: options.vitestConfig, workers: 1, instrumentedMap: mapFile, nodeArgs: options.testRunnerNodeArgs, inlineDeps: true }, options.concurrency, work, 'mutant');
|
|
377
328
|
pools.push(pool);
|
|
378
|
-
/** Mutants decided on one verdict track: the tests', or a spec oracle's replay tests'. */
|
|
379
329
|
const killedSoFar = (spec) => new Set([...runaways, ...crashedIds, ...records.filter((r) => r.type === 'mutant' && r.status !== 'Survived' && Boolean(r.spec) === spec).map((r) => r.id ?? '')]);
|
|
380
330
|
const afterUnit = (unit, outcome) => {
|
|
381
|
-
// Verdicts a failed canary cannot vouch for are dropped; their pairs are re-queued below.
|
|
382
331
|
const raw = readRecords(unit.results);
|
|
383
332
|
const suspect = new Set(raw.filter((r) => r.type === 'polluted').flatMap((r) => (r.suspects ?? []).map((id) => `${id}|${r.test ?? ''}`)));
|
|
384
333
|
const recs = raw.filter((r) => !(r.type === 'mutant' && suspect.has(`${r.id ?? ''}|${r.test ?? ''}`)));
|
|
@@ -393,7 +342,6 @@ export async function run(options) {
|
|
|
393
342
|
runaways.add(outcome.killedFor);
|
|
394
343
|
else if (bad)
|
|
395
344
|
crashedIds.add(bad);
|
|
396
|
-
// The rest of the unit: pairs not yet run, mutants not yet decided, into a fresh context.
|
|
397
345
|
const ran = new Set(recs.filter((r) => r.type === 'mutant').map((r) => `${r.id ?? ''}|${r.test ?? ''}`));
|
|
398
346
|
const decided = { tests: killedSoFar(false), spec: killedSoFar(true) };
|
|
399
347
|
const { file, plan, depth } = planUnits.get(unit.results);
|
|
@@ -406,8 +354,6 @@ export async function run(options) {
|
|
|
406
354
|
}
|
|
407
355
|
if (Object.keys(rest).length === 0)
|
|
408
356
|
return [];
|
|
409
|
-
// A unit that made no progress, or that keeps being polluted, is left to the isolated
|
|
410
|
-
// re-run rather than retried for ever.
|
|
411
357
|
if (ran.size === 0 || depth >= 40) {
|
|
412
358
|
for (const id of new Set(Object.values(rest).flat()))
|
|
413
359
|
unfinished.add(id);
|
|
@@ -423,13 +369,6 @@ export async function run(options) {
|
|
|
423
369
|
const killedCount = () => `${String(killedSoFar(false).size)} of ${String(toRun.length)} mutants killed so far`;
|
|
424
370
|
progress.phase('warm runs', killedCount);
|
|
425
371
|
const m0 = performance.now();
|
|
426
|
-
// One round, every covering file at once. A two-round plan (cheapest file first, survivors
|
|
427
|
-
// to the rest) cut executions 23% on burgee/paratext but took 2.9x longer: the second round
|
|
428
|
-
// waits for the first to drain, and the expensive files then run as a tail (2026-10-27).
|
|
429
|
-
// Kills are filtered out at dispatch, not when a unit is built: most units wait in the queue while
|
|
430
|
-
// others run, and a mutant another unit has killed (vouched for by its canary) needs no more of its
|
|
431
|
-
// covering tests. 1,022 of 6,550 warm executions on burgee/paratext ran after the mutant's kill
|
|
432
|
-
// (2026-10-30). Verdicts are unchanged: a Killed mutant needs no other pair (dispatch.qnt).
|
|
433
372
|
const beforeUnit = (unit) => {
|
|
434
373
|
const entry = planUnits.get(unit.results);
|
|
435
374
|
if (!entry)
|
|
@@ -450,7 +389,6 @@ export async function run(options) {
|
|
|
450
389
|
};
|
|
451
390
|
await pool.drain(unitsForRound(toRun, (m) => fileOrder.get(m.id) ?? []), (_, id) => warmBudget(id), afterUnit, beforeUnit);
|
|
452
391
|
stats.mutationMs = performance.now() - m0;
|
|
453
|
-
// ── 5. Verdicts from the warm run ─────────────────────────────────────────────────────────
|
|
454
392
|
const byMutant = new Map();
|
|
455
393
|
for (const r of records)
|
|
456
394
|
if (r.type === 'mutant' && r.id)
|
|
@@ -465,10 +403,10 @@ export async function run(options) {
|
|
|
465
403
|
if (m.specCoveredBy)
|
|
466
404
|
m.spec = specVerdict(m, all.filter((r) => r.spec));
|
|
467
405
|
if (m.coveredBy.length === 0 && !runaways.has(m.id) && !crashedIds.has(m.id)) {
|
|
468
|
-
continue;
|
|
406
|
+
continue;
|
|
469
407
|
}
|
|
470
408
|
else if (runaways.has(m.id)) {
|
|
471
|
-
isolatedQueue.push(m);
|
|
409
|
+
isolatedQueue.push(m);
|
|
472
410
|
}
|
|
473
411
|
else if (crashedIds.has(m.id)) {
|
|
474
412
|
m.status = 'RuntimeError';
|
|
@@ -484,22 +422,15 @@ export async function run(options) {
|
|
|
484
422
|
m.testsCompleted = recs.length;
|
|
485
423
|
}
|
|
486
424
|
else {
|
|
487
|
-
isolatedQueue.push(m);
|
|
425
|
+
isolatedQueue.push(m);
|
|
488
426
|
}
|
|
489
427
|
}
|
|
490
428
|
const staticMutants = mutants.filter((m) => m.static && m.status !== 'Ignored' && !m.reused);
|
|
491
|
-
// ── 6. Isolated re-runs: the switch armed before import, Vitest run normally ──────────────
|
|
492
429
|
const allTestFiles = [...new Set(dry.tests.map((t) => t.file))];
|
|
493
|
-
// A static mutant needs the files that load it (plus any whose tests reach it); only a hit
|
|
494
|
-
// with no file to attribute it to (a setup file outside collection) falls back to all files.
|
|
495
430
|
const fileCost = new Map();
|
|
496
431
|
for (const t of dry.tests)
|
|
497
432
|
fileCost.set(t.file, (fileCost.get(t.file) ?? 0) + t.ms);
|
|
498
433
|
const byCost = (files) => [...files].sort((a, b) => (fileCost.get(a) ?? 0) - (fileCost.get(b) ?? 0));
|
|
499
|
-
// Test file -> source file -> how many of that source's mutants its tests reach. A static mutant
|
|
500
|
-
// rarely has covering tests of its own, so its loaders were tried cheapest first, and the cheapest
|
|
501
|
-
// seldom tests the module: on burgee/controlroom each tabs.ts static mutant ran 75 tests in two
|
|
502
|
-
// files where Stryker needed 37 (2026-10-30). The file that exercises the module goes first.
|
|
503
434
|
const affinity = new Map();
|
|
504
435
|
for (const mm of mutants) {
|
|
505
436
|
for (const t of mm.coveredBy) {
|
|
@@ -518,35 +449,20 @@ export async function run(options) {
|
|
|
518
449
|
const loaders = staticFiles.get(m.id);
|
|
519
450
|
if (!loaders || m.id in dry.static)
|
|
520
451
|
return byCost(allTestFiles);
|
|
521
|
-
// Files whose tests reach the mutant first: they kill it. Ordering all loaders by cost alone
|
|
522
|
-
// ran a file that only imports it first, and 288 of 349 isolated kills on burgee/paratext
|
|
523
|
-
// came from a later file (2026-10-27).
|
|
524
452
|
const covering = byCost([...new Set(tests)]);
|
|
525
453
|
return [...covering, ...byAffinity(m.file, [...loaders].filter((f) => !covering.includes(f)))];
|
|
526
454
|
};
|
|
527
455
|
const isolatedBudget = (id) => {
|
|
528
456
|
const m = byId.get(id);
|
|
529
457
|
const fs = m ? filesFor(m) : allTestFiles;
|
|
530
|
-
// A non-static mutant's isolated run executes only its covering tests (`tests` below), so they
|
|
531
|
-
// are its budget; every test of its files made a hang wait 30-40 s on oxmutate's own suite,
|
|
532
|
-
// where its covering tests take a fraction of that (2026-10-30). A static mutant runs them all.
|
|
533
458
|
const covering = new Set(m && !m.static ? m.coveredBy : []);
|
|
534
459
|
const ms = dry.tests.filter((t) => fs.includes(t.file) && (covering.size === 0 || covering.has(t.id))).reduce((sum, t) => sum + t.ms, 0);
|
|
535
460
|
return 1500 + options.timeoutMS + options.timeoutFactor * ms * 2;
|
|
536
461
|
};
|
|
537
|
-
// A non-static mutant runs only its covering tests, armed across all of them in file order:
|
|
538
|
-
// Stryker's semantics, so cross-test effects still show (a no-op reset() lets state pile up
|
|
539
|
-
// for the next covering test). A static mutant runs every test of its files, because an
|
|
540
|
-
// import-time change reaches tests that never execute the mutated line.
|
|
541
|
-
/**
|
|
542
|
-
* Static mutants decided in live workers, one unit per (test file, batch of its static mutants),
|
|
543
|
-
* on a pool that inlines dependencies. Returns the mutants this path could not decide.
|
|
544
|
-
*/
|
|
545
462
|
const decideStaticLive = async (items) => {
|
|
546
463
|
if (items.length === 0)
|
|
547
464
|
return [];
|
|
548
465
|
const STATIC_BATCH = 25;
|
|
549
|
-
/** Units of up to STATIC_BATCH mutants per test file, for the files each mutant gets this round. */
|
|
550
466
|
const unitsFor = (filesOf) => {
|
|
551
467
|
const byFile = new Map();
|
|
552
468
|
for (const m of items)
|
|
@@ -558,9 +474,8 @@ export async function run(options) {
|
|
|
558
474
|
})));
|
|
559
475
|
};
|
|
560
476
|
const killed = new Map();
|
|
561
|
-
/** Decided by the hit-count limit: a runaway, Timeout like a warm one. */
|
|
562
477
|
const timedOut = new Set();
|
|
563
|
-
const passed = new Map();
|
|
478
|
+
const passed = new Map();
|
|
564
479
|
const back = new Set();
|
|
565
480
|
const ran = new Map();
|
|
566
481
|
const staticPool = pool;
|
|
@@ -584,7 +499,6 @@ export async function run(options) {
|
|
|
584
499
|
else
|
|
585
500
|
(passed.get(r.id) ?? passed.set(r.id, new Set()).get(r.id))?.add(file);
|
|
586
501
|
}
|
|
587
|
-
// A stop or a crash leaves the rest of the batch undecided: the fresh-worker path decides it.
|
|
588
502
|
if (outcome.killedFor || outcome.crashed)
|
|
589
503
|
for (const id of unit.staticIds ?? [])
|
|
590
504
|
if (!decided.has(id))
|
|
@@ -595,13 +509,8 @@ export async function run(options) {
|
|
|
595
509
|
unit.staticIds = (unit.staticIds ?? []).filter((id) => !killed.has(id) && !back.has(id));
|
|
596
510
|
return unit.staticIds.length > 0;
|
|
597
511
|
};
|
|
598
|
-
// Two rounds: each mutant first in the file most likely to kill it (filesFor puts the files whose
|
|
599
|
-
// tests reach it first, cheapest first), then its other files only if it survived. In one round
|
|
600
|
-
// every loading file ran a mutant at once, and a kill in one did not spare the others: 41 tests a
|
|
601
|
-
// static kill on burgee/controlroom against Stryker's 33.6 (2026-10-30).
|
|
602
512
|
const open = (m) => !killed.has(m.id) && !back.has(m.id);
|
|
603
513
|
const rounds = [(m) => filesFor(m).slice(0, 1), (m) => (open(m) ? filesFor(m).slice(1) : [])];
|
|
604
|
-
// the mutant's run and the unarmed canary after it
|
|
605
514
|
for (const round of rounds)
|
|
606
515
|
await staticPool.drain(unitsFor(round), (_, id) => 2 * isolatedBudget(id), afterStatic, keepUndecided);
|
|
607
516
|
const undecided = [];
|
|
@@ -626,9 +535,6 @@ export async function run(options) {
|
|
|
626
535
|
if (items.length === 0)
|
|
627
536
|
return verdicts;
|
|
628
537
|
const mk = (batch) => ({ mode: 'isolated', files: [], results: fileFor('iso', 'jsonl'), progress: fileFor('iso', 'progress'), batch: batch.map((m) => ({ id: m.id, files: filesFor(m), tests: m.static ? undefined : m.coveredBy, hitLimit: hitLimitOf(m.id) })) });
|
|
629
|
-
// Packed by estimated cost, heaviest first into the lightest bin, and the heaviest bins run
|
|
630
|
-
// first. Equal mutant counts left one unit at 8.4 s while the others finished in under 3 s on
|
|
631
|
-
// burgee/controlroom (2026-10-30): a mutant's cost is its files' dry-run time, not one.
|
|
632
538
|
const cost = (m) => isolatedBudget(m.id);
|
|
633
539
|
const bins = Array.from({ length: Math.min(items.length, options.concurrency * 2) }, () => ({ load: 0, batch: [] }));
|
|
634
540
|
for (const m of [...items].sort((x, y) => cost(y) - cost(x))) {
|
|
@@ -639,7 +545,6 @@ export async function run(options) {
|
|
|
639
545
|
const units = bins.sort((x, y) => y.load - x.load).map((b) => mk(b.batch));
|
|
640
546
|
stats.isolatedRuns += items.length;
|
|
641
547
|
await pool.drain(units, (_, id) => slack * isolatedBudget(id), (unit, outcome) => {
|
|
642
|
-
// Vitest reports per test file: a mutant is killed if any of its files failed.
|
|
643
548
|
for (const rec of readRecords(unit.results)) {
|
|
644
549
|
if (rec.type !== 'isolated' || !rec.id)
|
|
645
550
|
continue;
|
|
@@ -667,11 +572,6 @@ export async function run(options) {
|
|
|
667
572
|
verdicts.set(m.id, { status: 'RuntimeError' });
|
|
668
573
|
return verdicts;
|
|
669
574
|
};
|
|
670
|
-
// ── 5b. Static mutants in live workers (.sdlc/intents/oxmutate/staticwarm.qnt) ─────────────
|
|
671
|
-
// A fresh worker per (static mutant, file) cost ~80 ms of start-up around tests that take a few
|
|
672
|
-
// ms: 29 of 35 s on burgee/paratext (2026-10-30). A live worker re-evaluates the project's modules
|
|
673
|
-
// per mutant instead; whatever it cannot vouch for comes back here for the fresh-worker path.
|
|
674
|
-
// OXMUTATE_ISOLATE_STATIC=1 sends every static mutant down the fresh-worker path, as before.
|
|
675
575
|
const s0 = performance.now();
|
|
676
576
|
const staticBack = env['OXMUTATE_ISOLATE_STATIC'] ? staticMutants : await decideStaticLive(staticMutants);
|
|
677
577
|
stats.staticMs = performance.now() - s0;
|
|
@@ -679,13 +579,6 @@ export async function run(options) {
|
|
|
679
579
|
progress.phase(`isolated runs (static mutants and stragglers: ${String(isolatedQueue.length)})`);
|
|
680
580
|
const i0 = performance.now();
|
|
681
581
|
const first = await runIsolated(isolatedQueue);
|
|
682
|
-
// A watchdog stop on a loaded machine is a slow test, not a runaway: 31 of them on one
|
|
683
|
-
// burgee/paratext run while the load average sat at 40, all Survived for Stryker (2026-10-27).
|
|
684
|
-
// Each stop gets one more run with 3x the budget; only one that is stopped again is a Timeout.
|
|
685
|
-
// An idle stop is not re-run: its worker was waiting with its event loop turning and no CPU spent,
|
|
686
|
-
// on something the mutant never lets happen, which no budget cures; it is a Timeout as Stryker
|
|
687
|
-
// scores it. Re-run at 3x, 375 of them on caique's 10-31 run "finished" only because Vitest's 30 s
|
|
688
|
-
// test timeout failed the test (D-20261101-idle-stall-is-timeout, stall.qnt).
|
|
689
582
|
const stopped = isolatedQueue.filter((m) => first.get(m.id)?.status === 'Timeout' && !first.get(m.id)?.idle);
|
|
690
583
|
const again = await runIsolated(stopped, RERUN_SLACK);
|
|
691
584
|
for (const m of stopped) {
|
|
@@ -705,20 +598,8 @@ export async function run(options) {
|
|
|
705
598
|
m.statusReason = v.idle ? 'its test waited past the budget with the worker idle: a hang, not a slow test' : 'stopped by the watchdog twice, the second time with 3x the budget';
|
|
706
599
|
}
|
|
707
600
|
}
|
|
708
|
-
// ── 6b. Confirm survivors under whole-file semantics ──────────────────────────────────────
|
|
709
|
-
// The warm runner arms a mutant for one test at a time, so a mutant whose effect only shows
|
|
710
|
-
// across tests (a `reset()` in beforeEach made a no-op, so state piles up for the next test)
|
|
711
|
-
// survives warm and is killed when armed for the whole file, the way Stryker runs it (burgee/
|
|
712
|
-
// paratext plugin.ts, 2026-10-27). Survivors are what a person acts on, so each one is
|
|
713
|
-
// re-run once with the mutant armed across its covering files before it is reported.
|
|
714
|
-
// A survivor with one covering test is not re-run: confirmation runs only covering tests, so there
|
|
715
|
-
// is no later test for its leftover state to reach, and the warm run already armed that test
|
|
716
|
-
// through its hooks from clean state (.sdlc/intents/oxmutate/confirm.qnt).
|
|
717
601
|
const survivors = toRun.filter((m) => m.status === 'Survived' && new Set(m.coveredBy).size > 1);
|
|
718
602
|
const confirmed = await runIsolated(survivors);
|
|
719
|
-
// A timeout here is a watchdog stop like any other, and a loaded machine makes them: two
|
|
720
|
-
// closeout survivors were reported killed this way while a docs build shared the machine
|
|
721
|
-
// (2026-10-27). The same rule as section 6: one more run at 3x the budget before believing it.
|
|
722
603
|
const stoppedHere = survivors.filter((m) => confirmed.get(m.id)?.status === 'Timeout' && !confirmed.get(m.id)?.idle);
|
|
723
604
|
const recheck = await runIsolated(stoppedHere, RERUN_SLACK);
|
|
724
605
|
for (const m of stoppedHere) {
|
|
@@ -735,10 +616,8 @@ export async function run(options) {
|
|
|
735
616
|
stats.survivorsOverturned++;
|
|
736
617
|
}
|
|
737
618
|
}
|
|
738
|
-
// ── 6c. Survivors that do not type-check are CompileErrors (checker.ts) ────────────────────
|
|
739
619
|
if (options.typeCheck) {
|
|
740
620
|
progress.phase('type-checking survivors');
|
|
741
|
-
// Every undetected mutant is one somebody would act on: survivors and the uncovered.
|
|
742
621
|
const survived = mutants.filter((m) => m.status === 'Survived' || m.status === 'NoCoverage');
|
|
743
622
|
const refused = await checkSurvivors({ root, tsconfig: options.tsconfigFile, command: options.typeChecker, concurrency: options.concurrency }, survived).catch((error) => {
|
|
744
623
|
throw new InstrumentFault(error instanceof Error ? error.message : String(error));
|
|
@@ -749,7 +628,6 @@ export async function run(options) {
|
|
|
749
628
|
stats.compileErrors++;
|
|
750
629
|
}
|
|
751
630
|
}
|
|
752
|
-
// ── 7. Flaky verdicts: re-run mutants whose covering tests are nondeterministic ───────────
|
|
753
631
|
const flakyTests = await flakyCheck;
|
|
754
632
|
stats.flakyTests = flakyTests.size;
|
|
755
633
|
const flakyCandidates = toRun.filter((m) => m.coveredBy.some((t) => flakyTests.has(t)));
|
|
@@ -774,7 +652,6 @@ export async function run(options) {
|
|
|
774
652
|
stats.watchdogKills = pool.stats.killed;
|
|
775
653
|
stats.idleStalls = pool.stats.idle;
|
|
776
654
|
stats.crashes = pool.stats.crashed;
|
|
777
|
-
// ── 8. Score, cache, result ───────────────────────────────────────────────────────────────
|
|
778
655
|
if (options.incremental) {
|
|
779
656
|
mkdirSync(path.dirname(options.incrementalFile), { recursive: true });
|
|
780
657
|
const next = { ...cache };
|
|
@@ -814,4 +691,3 @@ export async function run(options) {
|
|
|
814
691
|
rmSync(work, { recursive: true, force: true });
|
|
815
692
|
}
|
|
816
693
|
}
|
|
817
|
-
//# sourceMappingURL=coordinator.js.map
|
package/dist/directives.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stryker's range directives: a `disable` comment naming mutators (or `all`) and a reason turns them
|
|
3
|
-
* off from the next line on, until a `restore` comment naming them (either spelled with `Stryker` or
|
|
4
|
-
* `oxmutate`). The `next-line` form is config.ts's `disables`. Before this, oxmutate read only the
|
|
5
|
-
* `next-line` form and graded every mutant inside a range as if no directive were there: 116 such
|
|
6
|
-
* mutants in compat-oracle's `hosts.ts` alone (.sdlc/intents/oxmutate/sc8-profile.md).
|
|
7
|
-
*
|
|
8
|
-
* Read by line, as written. Stryker reads a directive only as the leading comment of the next AST
|
|
9
|
-
* node, so a `restore` that closes an object (`// Stryker restore all` above its `}`) is invisible to
|
|
10
|
-
* it, and its `disable` then runs to the end of the file. oxmutate does not reproduce that: the
|
|
11
|
-
* directive means what it says.
|
|
12
|
-
*/
|
|
13
1
|
const HEAD = /\/\/\s*(?:Stryker|oxmutate)\s+(disable|restore)\s+(\S.*)/i;
|
|
14
|
-
/** `all`, or a comma list of mutator names. */
|
|
15
2
|
const LIST = /^(?:all|\w+(?:\s*,\s*\w+)*)$/i;
|
|
16
3
|
const covers = (r, mutator) => r.mutators === 'all' || r.mutators.includes(mutator);
|
|
17
|
-
/** The range directives of a file. A `disable` without a reason is an error, as `next-line`'s is. */
|
|
18
4
|
export function rangeDisables(source, file) {
|
|
19
5
|
const rules = [];
|
|
20
6
|
const errors = [];
|
|
@@ -22,7 +8,6 @@ export function rangeDisables(source, file) {
|
|
|
22
8
|
const m = HEAD.exec(text);
|
|
23
9
|
if (!m)
|
|
24
10
|
return;
|
|
25
|
-
// `next-line` is not a list (the hyphen), so that form is left to config.ts here too.
|
|
26
11
|
const body = m[2];
|
|
27
12
|
const colon = body.indexOf(':');
|
|
28
13
|
const list = (colon === -1 ? body : body.slice(0, colon)).trim();
|
|
@@ -41,7 +26,6 @@ export function rangeDisables(source, file) {
|
|
|
41
26
|
errors,
|
|
42
27
|
reasonFor(line, mutator) {
|
|
43
28
|
const name = mutator.toLowerCase();
|
|
44
|
-
// The latest directive before the line that names this mutator decides it.
|
|
45
29
|
for (let k = rules.length - 1; k >= 0; k--) {
|
|
46
30
|
const r = rules[k];
|
|
47
31
|
if (r.line >= line || !covers(r, name))
|
|
@@ -52,4 +36,3 @@ export function rangeDisables(source, file) {
|
|
|
52
36
|
},
|
|
53
37
|
};
|
|
54
38
|
}
|
|
55
|
-
//# sourceMappingURL=directives.js.map
|
package/dist/files.js
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The files to mutate, from Stryker-style globs (`src/**\/*.ts`, `!src/**\/*.test.ts`, `{a,b}`).
|
|
3
|
-
* No dependency: a glob is translated to one regular expression, and the walk skips the
|
|
4
|
-
* directories a project never mutates.
|
|
5
|
-
*/
|
|
6
1
|
import { readdirSync } from 'node:fs';
|
|
7
2
|
import path from 'node:path';
|
|
8
3
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo', '.oxmutate', '.stryker-tmp', 'reports']);
|
|
9
4
|
const escapeRegExp = (s) => s.replace(/[.+^$()|[\]\\]/g, '\\$&');
|
|
10
|
-
/** Translate one glob (without a leading `!`) into an anchored regular expression. */
|
|
11
5
|
export function globToRegExp(glob) {
|
|
12
6
|
let re = '';
|
|
13
|
-
// Stryker disable next-line EqualityOperator: one step past the end reads charAt(length), an empty string, which appends nothing
|
|
14
7
|
for (let i = 0; i < glob.length; i++) {
|
|
15
8
|
const c = glob.charAt(i);
|
|
16
9
|
if (c === '*') {
|
|
17
10
|
if (glob[i + 1] === '*') {
|
|
18
|
-
// `**/` matches zero or more directories; a bare `**` matches anything.
|
|
19
11
|
if (glob[i + 2] === '/') {
|
|
20
12
|
re += '(?:.*/)?';
|
|
21
13
|
i += 2;
|
|
@@ -40,7 +32,6 @@ export function globToRegExp(glob) {
|
|
|
40
32
|
}
|
|
41
33
|
return new RegExp(`^${re}$`);
|
|
42
34
|
}
|
|
43
|
-
/** Repository-relative paths (forward slashes) of files matching the include and exclude globs. */
|
|
44
35
|
export function matchFiles(root, patterns) {
|
|
45
36
|
const include = patterns.filter((p) => !p.startsWith('!')).map(globToRegExp);
|
|
46
37
|
const exclude = patterns.filter((p) => p.startsWith('!')).map((p) => globToRegExp(p.slice(1)));
|
|
@@ -59,4 +50,3 @@ export function matchFiles(root, patterns) {
|
|
|
59
50
|
walk('');
|
|
60
51
|
return out.sort();
|
|
61
52
|
}
|
|
62
|
-
//# sourceMappingURL=files.js.map
|
package/dist/frozen.js
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The frozen oracle (spec R14): a change that edits tests is also scored against the base ref's
|
|
3
|
-
* tests. The code under test is the same in both runs, so the mutants and their ids are the same;
|
|
4
|
-
* only the test files differ. A mutant the base tests detect and the changed tests do not is a test
|
|
5
|
-
* the change weakened, which is how a patch that "makes the check pass" by editing the check shows
|
|
6
|
-
* up (the reward-hacking concern in the intent, written as a CI result).
|
|
7
|
-
*
|
|
8
|
-
* The base tests are served to Vitest from memory, the way instrumented sources are: nothing in
|
|
9
|
-
* the working tree is touched.
|
|
10
|
-
*/
|
|
11
1
|
import path from 'node:path';
|
|
12
2
|
import { ambientRuntime, run as spawn } from 'bellpull';
|
|
13
3
|
import { run } from './coordinator.js';
|
|
14
4
|
const detected = (m) => m?.status === 'Killed' || m?.status === 'Timeout';
|
|
15
5
|
const undetected = (m) => m?.status === 'Survived' || m?.status === 'NoCoverage';
|
|
16
|
-
/**
|
|
17
|
-
* A test file the change added has no base version. It is served as one skipped test, so the base
|
|
18
|
-
* run has the file (Vitest finds it on disk) and none of its tests.
|
|
19
|
-
*/
|
|
20
6
|
const ABSENT_AT_BASE = "import { it } from 'vitest';\nit.skip('absent at the base ref (oxmutate frozen oracle)', () => {});\n";
|
|
21
7
|
async function git(root, args) {
|
|
22
8
|
const r = await spawn('git', args, { cwd: root, runtime: ambientRuntime() });
|
|
@@ -44,4 +30,3 @@ export async function frozenOracle(head, base) {
|
|
|
44
30
|
.map((m) => ({ id: m.id, file: m.file, line: m.start.line, mutatorName: m.mutatorName, replacement: m.replacement }));
|
|
45
31
|
return result;
|
|
46
32
|
}
|
|
47
|
-
//# sourceMappingURL=frozen.js.map
|
package/dist/heartbeat.js
CHANGED
|
@@ -1,65 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The watchdog's evidence for the difference between a mutant that loops and one that hangs
|
|
3
|
-
* (.sdlc/intents/oxmutate/stall.qnt, D-20261101-idle-stall-is-timeout).
|
|
4
|
-
*
|
|
5
|
-
* Two readings, both taken when a run passes its budget:
|
|
6
|
-
*
|
|
7
|
-
* - **The beat.** The test worker writes the time to its server's beat file every HEARTBEAT_MS
|
|
8
|
-
* from a real interval timer. The beat keeps coming only while the worker's event loop turns. A
|
|
9
|
-
* synchronous loop stops it, and so does a microtask loop.
|
|
10
|
-
* - **The CPU.** The server reports its process's CPU time (every thread, the worker included)
|
|
11
|
-
* every CPU_SAMPLE_MS. Over the last CPU_WINDOW_MS, a loop burns about a core.
|
|
12
|
-
*
|
|
13
|
-
* A run whose worker is inside a test body, with a fresh beat and under IDLE_CPU of a core, is
|
|
14
|
-
* **idle**: the test waits on something the mutant never lets happen (a prompt awaiting a
|
|
15
|
-
* keypress). Anything else is **busy**, including a worker still collecting its file: on a loaded
|
|
16
|
-
* machine a short budget can run out while modules load, at low CPU, and that is a slow machine. On a caique
|
|
17
|
-
* subset 18 of 19 stops read 0–2% CPU with a live beat, and the one loop read 72%
|
|
18
|
-
* (sc8-profile.md). An idle stop is a Timeout as it stands, the way Stryker scores it. A busy stop
|
|
19
|
-
* may be a slow test on a loaded machine, so it keeps its re-run at 3x.
|
|
20
|
-
*
|
|
21
|
-
* The beat file is the server's, named by its pid and set in its environment when it starts, so
|
|
22
|
-
* every worker it ever runs inherits it: a path set per unit could reach a worker that started
|
|
23
|
-
* under an earlier unit, and on caique two stops read a beat that never came.
|
|
24
|
-
*/
|
|
25
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
26
2
|
import path from 'node:path';
|
|
27
|
-
/** How often the worker writes its beat. */
|
|
28
3
|
export const HEARTBEAT_MS = 250;
|
|
29
|
-
/** A beat older than this means the worker's event loop is not turning: a loop, not a wait. */
|
|
30
4
|
export const STALE_MS = 1000;
|
|
31
|
-
/** How often a server reports its CPU time. */
|
|
32
5
|
export const CPU_SAMPLE_MS = 250;
|
|
33
|
-
/** The window a stop's CPU share is measured over. */
|
|
34
6
|
export const CPU_WINDOW_MS = 1000;
|
|
35
|
-
/** The shortest span of samples a CPU share is read from; less, and the stop counts as busy. */
|
|
36
7
|
export const MIN_CPU_SPAN_MS = 500;
|
|
37
|
-
/** Under this share of one core, with a fresh beat, a stopped run is idle. Measured: idle 0–2%, a loop 72–108%. */
|
|
38
8
|
export const IDLE_CPU = 0.25;
|
|
39
|
-
/** The environment variable that hands a server's beat file to its test workers. */
|
|
40
9
|
export const BEAT_ENV = 'OXMUTATE_BEAT';
|
|
41
|
-
/** A server's beat file, in the run's work directory. */
|
|
42
10
|
export const beatFile = (workDir, pid) => path.join(workDir, `beat-${String(pid)}`);
|
|
43
|
-
// Captured when this module is evaluated, before any test file can install fake timers: a faked
|
|
44
|
-
// clock would write a frozen time, and the watchdog would read a live worker as a stale one.
|
|
45
11
|
const { setInterval: realSetInterval, clearInterval: realClearInterval } = globalThis;
|
|
46
|
-
const realNow = Date.now.bind(Date);
|
|
47
|
-
/** The beat this thread runs: one at a time, whatever re-evaluates this module. */
|
|
48
|
-
// Stryker disable next-line StringLiteral: any key names one registry symbol on every evaluation of this module, which is all the key is for
|
|
12
|
+
export const realNow = Date.now.bind(Date);
|
|
49
13
|
const BEAT_KEY = Symbol.for('oxmutate.heartbeat');
|
|
50
|
-
/** Whether a test body is running in this worker (the runner marks it); the beat carries it. */
|
|
51
14
|
let inTest = false;
|
|
52
|
-
/** Mark a test body as running, or done. Only a wait inside a test body can be an idle stall. */
|
|
53
15
|
export function markTest(running) {
|
|
54
16
|
inTest = running;
|
|
55
17
|
}
|
|
56
|
-
/** Start this worker's beat into `file`. Does nothing when no beat file is set. */
|
|
57
18
|
export function startHeartbeat(file) {
|
|
58
19
|
if (!file)
|
|
59
20
|
return;
|
|
60
21
|
const holder = globalThis;
|
|
61
22
|
realClearInterval(holder[BEAT_KEY]);
|
|
62
|
-
/** One beat; false once the run's directory is gone: the run is over, and nothing reads the beat. */
|
|
63
23
|
const beat = () => {
|
|
64
24
|
try {
|
|
65
25
|
writeFileSync(file, inTest ? `${String(realNow())} test` : String(realNow()));
|
|
@@ -74,10 +34,8 @@ export function startHeartbeat(file) {
|
|
|
74
34
|
if (!beat())
|
|
75
35
|
realClearInterval(holder[BEAT_KEY]);
|
|
76
36
|
}, HEARTBEAT_MS);
|
|
77
|
-
// Stryker disable next-line CallExpression: unref only lets a process exit while its beat runs; Vitest ends its workers regardless, so no verdict depends on it
|
|
78
37
|
holder[BEAT_KEY].unref();
|
|
79
38
|
}
|
|
80
|
-
/** The last beat of this server's workers. */
|
|
81
39
|
export function readBeat(file, now = Date.now()) {
|
|
82
40
|
let text;
|
|
83
41
|
try {
|
|
@@ -91,21 +49,14 @@ export function readBeat(file, now = Date.now()) {
|
|
|
91
49
|
return { age: Number.isFinite(at) && at > 0 ? now - at : Number.POSITIVE_INFINITY, inTest: where === 'test' };
|
|
92
50
|
}
|
|
93
51
|
const US_PER_MS = 1000;
|
|
94
|
-
/**
|
|
95
|
-
* The share of one core a server used over the last CPU_WINDOW_MS before `now`, from its samples
|
|
96
|
-
* (oldest first). Undefined when the samples span less than MIN_CPU_SPAN_MS of that window.
|
|
97
|
-
*/
|
|
98
52
|
export function cpuShare(samples, now) {
|
|
99
53
|
const recent = samples.filter((s) => s.at >= now - CPU_WINDOW_MS && s.at <= now);
|
|
100
54
|
const first = recent[0];
|
|
101
55
|
const last = recent.at(-1);
|
|
102
|
-
// Stryker disable next-line LogicalOperator: the first and the last sample are both there or both missing, so `&&` reads the same
|
|
103
56
|
if (!first || !last || last.at - first.at < MIN_CPU_SPAN_MS)
|
|
104
57
|
return undefined;
|
|
105
58
|
return (last.us - first.us) / US_PER_MS / (last.at - first.at);
|
|
106
59
|
}
|
|
107
|
-
/** A stopped run is idle when its worker waits inside a test body, its event loop turning, and its server burns almost no CPU. */
|
|
108
60
|
export function classifyStall(o) {
|
|
109
61
|
return o.beat.inTest && o.beat.age < STALE_MS && (o.cpuShare ?? IDLE_CPU) < IDLE_CPU ? 'idle' : 'busy';
|
|
110
62
|
}
|
|
111
|
-
//# sourceMappingURL=heartbeat.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oxmutate's programmatic API. The CLI (`oxmutate run`) is this, declared on burgee.
|
|
3
|
-
*/
|
|
4
1
|
export { loadOptions } from './config.js';
|
|
5
2
|
export { InstrumentFault, run } from './coordinator.js';
|
|
6
3
|
export { changedLines, parseShard, shardOf } from './lanes.js';
|
|
7
4
|
export { native } from './native.js';
|
|
8
5
|
export { sarifReport, strykerReport, textSummary, VERSION } from './reports.js';
|
|
9
|
-
//# sourceMappingURL=index.js.map
|