oxmutate 1.5.0 → 2.0.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 +2 -2
- package/binding/oxmutate.darwin-arm64.node +0 -0
- package/binding/oxmutate.darwin-x64.node +0 -0
- package/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/coordinator.d.ts +13 -0
- package/dist/coordinator.js +13 -1
- package/dist/reports.d.ts +0 -1
- package/dist/reports.js +11 -0
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +54 -17
- package/dist/scheduler.js +8 -10
- package/package.json +28 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<a href="https://www.npmjs.com/package/oxmutate?activeTab=code"><img src="https://img.shields.io/npm/unpacked-size/oxmutate?style=flat-square" alt="Unpacked size of the latest oxmutate release on npm" /></a>
|
|
21
21
|
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/dependencies-2%20in%20family%2C%200%20outside-0a6b47?style=flat-square" alt="Two dependencies, all in the burgee family (bellpull, burgee), none outside it" /></a>
|
|
22
22
|
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/types-included-blue?style=flat-square" alt="TypeScript types included for every entry point" /></a>
|
|
23
|
-
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/Node.js-
|
|
23
|
+
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/Node.js-22.13%2B-green?style=flat-square" alt="Node.js 22.13+" /></a>
|
|
24
24
|
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" alt="License: MIT" /></a>
|
|
25
25
|
<a href="https://www.npmjs.com/package/oxmutate#provenance"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2Foxmutate%2Flatest&query=%24.dist.attestations.provenance~&label=npm&style=flat-square&color=0a6b47" alt="npm provenance of the latest release, read live from its registry attestation" /></a>
|
|
26
26
|
</p>
|
|
@@ -335,7 +335,7 @@ The `oxmutate` bin is that API, declared once on burgee.
|
|
|
335
335
|
|
|
336
336
|
## Requirements
|
|
337
337
|
|
|
338
|
-
Node
|
|
338
|
+
Node 22.13 or later. Vitest 4.1 or later in the project, for the
|
|
339
339
|
default runner. macOS (arm64, x64), Linux (glibc x64 and arm64, musl x64) or Windows (x64).
|
|
340
340
|
|
|
341
341
|
## Where it sits
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -45,6 +45,19 @@ export interface RunStats {
|
|
|
45
45
|
isolatedMs: number;
|
|
46
46
|
wallMs: number;
|
|
47
47
|
testExecutions: number;
|
|
48
|
+
/** Warm units, send to outcome, summed over servers: what `mutationMs` is in server time. */
|
|
49
|
+
warmUnitMs: number;
|
|
50
|
+
/** Of `warmUnitMs`, the time inside armed test runs. The rest is starting workers, collecting files, canaries and hooks. */
|
|
51
|
+
warmArmedMs: number;
|
|
52
|
+
/** Isolated runs by what sent the mutant there: the parts of `isolatedRuns`. */
|
|
53
|
+
isolatedByCause: {
|
|
54
|
+
static: number;
|
|
55
|
+
stopped: number;
|
|
56
|
+
unfinished: number;
|
|
57
|
+
rerun: number;
|
|
58
|
+
confirm: number;
|
|
59
|
+
flaky: number;
|
|
60
|
+
};
|
|
48
61
|
watchdogKills: number;
|
|
49
62
|
/** Of `watchdogKills`, the stops whose worker was idle, waiting (heartbeat.ts). In an isolated run each is a Timeout as it stands. */
|
|
50
63
|
idleStalls: number;
|
package/dist/coordinator.js
CHANGED
|
@@ -73,7 +73,7 @@ export async function run(options) {
|
|
|
73
73
|
const { root } = options;
|
|
74
74
|
const work = mkdtempSync(path.join(tmpdir(), 'oxmutate-'));
|
|
75
75
|
const stats = {
|
|
76
|
-
files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0,
|
|
76
|
+
files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0, warmUnitMs: 0, warmArmedMs: 0, isolatedByCause: { static: 0, stopped: 0, unfinished: 0, rerun: 0, confirm: 0, flaky: 0 },
|
|
77
77
|
watchdogKills: 0, idleStalls: 0, deadlocks: 0, crashes: 0, canaryFiles: 0, pollutionRestarts: 0, diskWrites: 0, auditSampled: 0, auditDisagreed: 0, survivorsOverturned: 0, timeoutsOverturned: 0, compileErrors: 0, isolatedRuns: 0, reused: 0, flakyTests: 0, flakyMutants: 0, units: 0, serversSpawned: 0,
|
|
78
78
|
};
|
|
79
79
|
let unitSeq = 0;
|
|
@@ -189,6 +189,9 @@ export async function run(options) {
|
|
|
189
189
|
if (fileErrors.length) {
|
|
190
190
|
throw new InstrumentFault(`${String(fileErrors.length)} test file(s) failed to load during the dry run, so their tests would be missing from every verdict: ${fileErrors.slice(0, 3).map((e) => `${path.relative(root, e.file)}: ${e.error}`).join('; ')}`);
|
|
191
191
|
}
|
|
192
|
+
const unhandled = recs.reduce((n, r) => n + (r.unhandled ?? 0), 0);
|
|
193
|
+
if (unhandled > 0)
|
|
194
|
+
throw new InstrumentFault(`the suite raised ${String(unhandled)} unhandled error(s) with no mutant active, so no verdict can be trusted. fix: make \`vitest run\` pass with no "Unhandled Errors" first`);
|
|
192
195
|
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []), modules: Object.assign({}, ...recs.map((r) => r.modules ?? {})) };
|
|
193
196
|
};
|
|
194
197
|
progress.phase('dry run');
|
|
@@ -412,6 +415,7 @@ export async function run(options) {
|
|
|
412
415
|
const killedSoFar = (spec) => new Set([...runaways, ...crashedIds, ...records.filter((r) => r.type === 'mutant' && r.status !== 'Survived' && Boolean(r.spec) === spec).map((r) => r.id ?? '')]);
|
|
413
416
|
const afterUnit = async (unit, outcome) => {
|
|
414
417
|
inFlight.delete(unit.results);
|
|
418
|
+
stats.warmUnitMs += outcome.ms ?? 0;
|
|
415
419
|
const wrote = await diskWrittenDuring(unit);
|
|
416
420
|
if (wrote)
|
|
417
421
|
return wrote;
|
|
@@ -484,6 +488,7 @@ export async function run(options) {
|
|
|
484
488
|
if (r.type === 'mutant' && r.id)
|
|
485
489
|
(byMutant.get(r.id) ?? byMutant.set(r.id, []).get(r.id))?.push(r);
|
|
486
490
|
stats.testExecutions = records.filter((r) => r.type === 'mutant').length;
|
|
491
|
+
stats.warmArmedMs = records.reduce((sum, r) => sum + (r.type === 'mutant' ? (r.ms ?? 0) : 0), 0);
|
|
487
492
|
stats.canaryFiles = new Set(records.filter((r) => r.type === 'polluted' && r.file).map((r) => r.file ?? '')).size;
|
|
488
493
|
const isolatedQueue = [];
|
|
489
494
|
for (const m of toRun) {
|
|
@@ -496,6 +501,7 @@ export async function run(options) {
|
|
|
496
501
|
continue;
|
|
497
502
|
}
|
|
498
503
|
else if (runaways.has(m.id)) {
|
|
504
|
+
stats.isolatedByCause.stopped++;
|
|
499
505
|
isolatedQueue.push(m);
|
|
500
506
|
}
|
|
501
507
|
else if (crashedIds.has(m.id)) {
|
|
@@ -514,6 +520,7 @@ export async function run(options) {
|
|
|
514
520
|
m.testsCompleted = recs.length;
|
|
515
521
|
}
|
|
516
522
|
else {
|
|
523
|
+
stats.isolatedByCause.unfinished++;
|
|
517
524
|
isolatedQueue.push(m);
|
|
518
525
|
}
|
|
519
526
|
}
|
|
@@ -674,11 +681,13 @@ export async function run(options) {
|
|
|
674
681
|
const s0 = performance.now();
|
|
675
682
|
const staticBack = env['OXMUTATE_ISOLATE_STATIC'] ? staticMutants : await decideStaticLive(staticMutants);
|
|
676
683
|
stats.staticMs = performance.now() - s0;
|
|
684
|
+
stats.isolatedByCause.static = staticBack.length;
|
|
677
685
|
isolatedQueue.push(...staticBack);
|
|
678
686
|
progress.phase(`isolated runs (static mutants and stragglers: ${String(isolatedQueue.length)})`);
|
|
679
687
|
const i0 = performance.now();
|
|
680
688
|
const first = await runIsolated(isolatedQueue);
|
|
681
689
|
const stopped = isolatedQueue.filter((m) => first.get(m.id)?.status === 'Timeout' && !first.get(m.id)?.idle);
|
|
690
|
+
stats.isolatedByCause.rerun += stopped.length;
|
|
682
691
|
const again = await runIsolated(stopped, RERUN_SLACK);
|
|
683
692
|
for (const m of stopped) {
|
|
684
693
|
const v = again.get(m.id);
|
|
@@ -698,8 +707,10 @@ export async function run(options) {
|
|
|
698
707
|
}
|
|
699
708
|
}
|
|
700
709
|
const survivors = toRun.filter((m) => m.status === 'Survived' && new Set(m.coveredBy).size > 1);
|
|
710
|
+
stats.isolatedByCause.confirm += survivors.length;
|
|
701
711
|
const confirmed = await runIsolated(survivors);
|
|
702
712
|
const stoppedHere = survivors.filter((m) => confirmed.get(m.id)?.status === 'Timeout' && !confirmed.get(m.id)?.idle);
|
|
713
|
+
stats.isolatedByCause.rerun += stoppedHere.length;
|
|
703
714
|
const recheck = await runIsolated(stoppedHere, RERUN_SLACK);
|
|
704
715
|
for (const m of stoppedHere) {
|
|
705
716
|
const v = recheck.get(m.id);
|
|
@@ -732,6 +743,7 @@ export async function run(options) {
|
|
|
732
743
|
const flakyCandidates = toRun.filter((m) => m.coveredBy.some((t) => flakyTests.has(t)));
|
|
733
744
|
const seen = new Map(flakyCandidates.map((m) => [m.id, new Set([m.status === 'Timeout' ? 'Killed' : m.status])]));
|
|
734
745
|
for (let n = 1; n <= options.flakyReruns && flakyCandidates.length; n++) {
|
|
746
|
+
stats.isolatedByCause.flaky += flakyCandidates.length;
|
|
735
747
|
const again = await runIsolated(flakyCandidates);
|
|
736
748
|
for (const m of flakyCandidates) {
|
|
737
749
|
const v = again.get(m.id);
|
package/dist/reports.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export declare function strykerReport(r: RunResult): object;
|
|
|
6
6
|
/** SARIF 2.1.0: one result per surviving or uncovered mutant, fingerprinted by stable id. */
|
|
7
7
|
export declare function sarifReport(r: RunResult): object;
|
|
8
8
|
export declare function describe(m: MutantResult): string;
|
|
9
|
-
/** The terminal summary: the score, what to look at first, what the run corrected. */
|
|
10
9
|
export declare function textSummary(r: RunResult, opts?: {
|
|
11
10
|
survivors?: number;
|
|
12
11
|
}): string;
|
package/dist/reports.js
CHANGED
|
@@ -116,6 +116,16 @@ function frozenLine(r) {
|
|
|
116
116
|
const sign = f.delta >= 0 ? '+' : '';
|
|
117
117
|
return [` frozen oracle vs ${f.base}: ${f.headScore.toFixed(2)}% with the change's tests, ${f.baseScore.toFixed(2)}% with the base's (${sign}${f.delta.toFixed(2)}), ${String(f.weakened.length)} mutant(s) the change stopped detecting`];
|
|
118
118
|
}
|
|
119
|
+
function whereItWent(s) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
if (s.warmUnitMs > 0)
|
|
122
|
+
lines.push(` warm servers ran ${(s.warmUnitMs / MS_PER_S).toFixed(1)} s, ${(s.warmArmedMs / MS_PER_S).toFixed(1)} s of it inside test runs`);
|
|
123
|
+
const c = s.isolatedByCause;
|
|
124
|
+
const causes = [`static ${String(c.static)}`, `stopped ${String(c.stopped)}`, `unfinished ${String(c.unfinished)}`, `re-run ${String(c.rerun)}`, `survivors confirmed ${String(c.confirm)}`, `flaky ${String(c.flaky)}`];
|
|
125
|
+
if (s.isolatedRuns > 0)
|
|
126
|
+
lines.push(` isolated runs: ${causes.join(' · ')}`);
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
119
129
|
export function textSummary(r, opts = {}) {
|
|
120
130
|
const count = (s) => r.mutants.filter((m) => m.status === s).length;
|
|
121
131
|
const s = r.stats;
|
|
@@ -132,6 +142,7 @@ export function textSummary(r, opts = {}) {
|
|
|
132
142
|
const corrected = corrections(s);
|
|
133
143
|
if (corrected.length)
|
|
134
144
|
lines.push(` corrected: ${corrected.join(' · ')}`);
|
|
145
|
+
lines.push(...whereItWent(s));
|
|
135
146
|
lines.push(...frozenLine(r));
|
|
136
147
|
lines.push(...specLines(r));
|
|
137
148
|
const survivors = r.mutants.filter((m) => m.status === 'Survived' || m.status === 'NoCoverage');
|
package/dist/runner.d.ts
CHANGED
|
@@ -95,6 +95,6 @@ export default class OxmutateRunner extends OxmutateRunner_base {
|
|
|
95
95
|
private collectCopies;
|
|
96
96
|
/** Reset per-try state (snapshot counters, assertion counts) the way Vitest does for a retry. */
|
|
97
97
|
private retryReset;
|
|
98
|
-
onAfterRunFiles(files: AnyTask[]): unknown
|
|
98
|
+
onAfterRunFiles(files: AnyTask[]): Promise<unknown>;
|
|
99
99
|
}
|
|
100
100
|
export {};
|
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeSync, openSync, readFileSync, writeSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { availableParallelism, loadavg } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -53,6 +53,11 @@ if (mode === 'isolated' && env['OXMUTATE_HIT_LIMIT']) {
|
|
|
53
53
|
ns.hitLimit = Number(env['OXMUTATE_HIT_LIMIT']);
|
|
54
54
|
}
|
|
55
55
|
const ranAway = () => ns.hitCount !== undefined && ns.hitLimit !== undefined && ns.hitCount > ns.hitLimit;
|
|
56
|
+
let unhandled = 0;
|
|
57
|
+
process.on('unhandledRejection', () => void unhandled++);
|
|
58
|
+
process.on('uncaughtException', () => void unhandled++);
|
|
59
|
+
const realSetImmediate = globalThis.setImmediate;
|
|
60
|
+
const settle = () => new Promise((resolve) => realSetImmediate(resolve));
|
|
56
61
|
const deadlocks = mode === 'dry' ? undefined : watch();
|
|
57
62
|
let deadlocked = false;
|
|
58
63
|
let tornDown = false;
|
|
@@ -122,7 +127,7 @@ function takeCopy(test) {
|
|
|
122
127
|
}
|
|
123
128
|
const dryModules = new Map();
|
|
124
129
|
const PROBE_ID = '__oxmutate_probe__';
|
|
125
|
-
async function probe(test, fn) {
|
|
130
|
+
async function probe(test, fn, reset) {
|
|
126
131
|
const file = test.file?.filepath;
|
|
127
132
|
const scope = test.suite?.id ?? file;
|
|
128
133
|
if (!file || !scope || probed.has(scope) || runOnce.has(file))
|
|
@@ -130,6 +135,7 @@ async function probe(test, fn) {
|
|
|
130
135
|
probed.add(scope);
|
|
131
136
|
await afterEach(test, test.context, []);
|
|
132
137
|
const cleanups = await beforeEach(test, test.context);
|
|
138
|
+
await reset();
|
|
133
139
|
ns.currentTestId = PROBE_ID;
|
|
134
140
|
try {
|
|
135
141
|
await fn();
|
|
@@ -147,9 +153,31 @@ async function probe(test, fn) {
|
|
|
147
153
|
}
|
|
148
154
|
const POLLUTION_ERROR_CHARS = 300;
|
|
149
155
|
const FILE_ERROR_CHARS = 500;
|
|
156
|
+
let resultsFd;
|
|
157
|
+
let progressFd;
|
|
158
|
+
let markLength = 0;
|
|
150
159
|
function emit(record) {
|
|
151
|
-
if (resultsPath)
|
|
152
|
-
|
|
160
|
+
if (!resultsPath)
|
|
161
|
+
return;
|
|
162
|
+
resultsFd ??= openSync(resultsPath, 'a');
|
|
163
|
+
writeSync(resultsFd, `${JSON.stringify(record)}\n`);
|
|
164
|
+
}
|
|
165
|
+
function mark(text) {
|
|
166
|
+
if (!progressPath)
|
|
167
|
+
return;
|
|
168
|
+
progressFd ??= openSync(progressPath, 'w');
|
|
169
|
+
const padded = text.trimEnd().padEnd(markLength);
|
|
170
|
+
markLength = padded.length;
|
|
171
|
+
if (padded)
|
|
172
|
+
writeSync(progressFd, padded, 0);
|
|
173
|
+
}
|
|
174
|
+
function closeFiles() {
|
|
175
|
+
for (const fd of [resultsFd, progressFd])
|
|
176
|
+
if (fd !== undefined)
|
|
177
|
+
closeSync(fd);
|
|
178
|
+
resultsFd = undefined;
|
|
179
|
+
progressFd = undefined;
|
|
180
|
+
markLength = 0;
|
|
153
181
|
}
|
|
154
182
|
function chain(test) {
|
|
155
183
|
const suites = [];
|
|
@@ -271,7 +299,7 @@ export default class OxmutateRunner extends Base {
|
|
|
271
299
|
try {
|
|
272
300
|
await guarded(test.id, fn);
|
|
273
301
|
if (mode === 'dry')
|
|
274
|
-
await probe(test, fn);
|
|
302
|
+
await probe(test, fn, () => this.retryReset(test, 1));
|
|
275
303
|
}
|
|
276
304
|
catch (e) {
|
|
277
305
|
if (isDeadlock(e))
|
|
@@ -359,9 +387,13 @@ export default class OxmutateRunner extends Base {
|
|
|
359
387
|
ns.hitLimit = plan.hitLimits[id] ?? Number.MAX_SAFE_INTEGER;
|
|
360
388
|
let status = 'Survived';
|
|
361
389
|
let deadlock = false;
|
|
390
|
+
const before = unhandled;
|
|
362
391
|
try {
|
|
363
392
|
await setup();
|
|
364
393
|
await guarded(current.id, run);
|
|
394
|
+
await settle();
|
|
395
|
+
if (unhandled > before)
|
|
396
|
+
status = 'Killed';
|
|
365
397
|
}
|
|
366
398
|
catch (e) {
|
|
367
399
|
deadlock = isDeadlock(e);
|
|
@@ -377,8 +409,7 @@ export default class OxmutateRunner extends Base {
|
|
|
377
409
|
for (const id of ids) {
|
|
378
410
|
if (done.has(id))
|
|
379
411
|
continue;
|
|
380
|
-
|
|
381
|
-
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
412
|
+
mark(`${id} ${String(realNow())}\n`);
|
|
382
413
|
let { status, ms, deadlock } = await runArmed(id);
|
|
383
414
|
if (status === 'Polluted')
|
|
384
415
|
break;
|
|
@@ -402,14 +433,13 @@ export default class OxmutateRunner extends Base {
|
|
|
402
433
|
break;
|
|
403
434
|
}
|
|
404
435
|
if (tornDown) {
|
|
405
|
-
|
|
406
|
-
writeFileSync(progressPath, '');
|
|
436
|
+
mark('');
|
|
407
437
|
return;
|
|
408
438
|
}
|
|
409
439
|
if (!polluted)
|
|
410
440
|
await canary();
|
|
411
|
-
if (
|
|
412
|
-
|
|
441
|
+
if (ids.length)
|
|
442
|
+
mark('');
|
|
413
443
|
if (attempt > 0)
|
|
414
444
|
await fresh();
|
|
415
445
|
try {
|
|
@@ -474,10 +504,14 @@ export default class OxmutateRunner extends Base {
|
|
|
474
504
|
ns.hitCount = armed ? 0 : undefined;
|
|
475
505
|
ns.hitLimit = armed ? (staticLimits[armed] ?? Number.MAX_SAFE_INTEGER) : undefined;
|
|
476
506
|
deadlocked = false;
|
|
507
|
+
const before = unhandled;
|
|
477
508
|
try {
|
|
478
509
|
vi.resetModules();
|
|
479
510
|
const [file] = await collectTests([filepath], this);
|
|
480
511
|
const result = file ? await runCollected(file, runnable) : { failed: [filepath], ran: 0 };
|
|
512
|
+
await settle();
|
|
513
|
+
if (unhandled > before && result.failed.length === 0)
|
|
514
|
+
result.failed.push(filepath);
|
|
481
515
|
return { ...result, timeout: ranAway() || deadlocked, ...(deadlocked ? { deadlock: true } : {}) };
|
|
482
516
|
}
|
|
483
517
|
finally {
|
|
@@ -495,8 +529,7 @@ export default class OxmutateRunner extends Base {
|
|
|
495
529
|
};
|
|
496
530
|
while (rest.length) {
|
|
497
531
|
const id = rest.shift();
|
|
498
|
-
|
|
499
|
-
writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
|
|
532
|
+
mark(`${id} ${String(realNow())}\n`);
|
|
500
533
|
held.push({ id, verdict: await evaluate(id) });
|
|
501
534
|
if (held.at(-1)?.verdict.deadlock)
|
|
502
535
|
return back('torn down: a deadlocked run is still pending in this worker');
|
|
@@ -517,8 +550,7 @@ export default class OxmutateRunner extends Base {
|
|
|
517
550
|
held = [];
|
|
518
551
|
every = Math.min(STATIC_CANARY_MAX, every * 2);
|
|
519
552
|
}
|
|
520
|
-
|
|
521
|
-
writeFileSync(progressPath, '');
|
|
553
|
+
mark('');
|
|
522
554
|
vi.resetModules();
|
|
523
555
|
}
|
|
524
556
|
async onBeforeRunTask(test) {
|
|
@@ -562,7 +594,8 @@ export default class OxmutateRunner extends Base {
|
|
|
562
594
|
async retryReset(test, attempt) {
|
|
563
595
|
await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
|
|
564
596
|
}
|
|
565
|
-
onAfterRunFiles(files) {
|
|
597
|
+
async onAfterRunFiles(files) {
|
|
598
|
+
await settle();
|
|
566
599
|
if (mode === 'dry') {
|
|
567
600
|
const tests = [];
|
|
568
601
|
const visit = (t, file) => {
|
|
@@ -579,7 +612,7 @@ export default class OxmutateRunner extends Base {
|
|
|
579
612
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
580
613
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
581
614
|
}
|
|
582
|
-
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules) });
|
|
615
|
+
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules), unhandled });
|
|
583
616
|
}
|
|
584
617
|
else if (mode === 'isolated') {
|
|
585
618
|
const failed = [];
|
|
@@ -597,9 +630,13 @@ export default class OxmutateRunner extends Base {
|
|
|
597
630
|
if (f.result?.state === 'fail' && !failed.length)
|
|
598
631
|
failed.push(f.id);
|
|
599
632
|
}
|
|
633
|
+
const [first] = files;
|
|
634
|
+
if (unhandled > 0 && !failed.length && first)
|
|
635
|
+
failed.push(first.id);
|
|
600
636
|
const timeout = ranAway() || deadlocked;
|
|
601
637
|
emit({ type: 'isolated', id: env[ACTIVE_MUTANT_ENV] ?? ns.activeMutant, killed: failed.length > 0 && !timeout, failed, ran, ...(timeout && { timeout }), ...(deadlocked && { deadlock: true }) });
|
|
602
638
|
}
|
|
639
|
+
closeFiles();
|
|
603
640
|
return super.onAfterRunFiles?.(files);
|
|
604
641
|
}
|
|
605
642
|
}
|
package/dist/scheduler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { beatFile, classifyStall, CPU_WINDOW_MS, cpuShare, readBeat } from './heartbeat.js';
|
|
@@ -8,6 +8,7 @@ const childJs = fileURLToPath(new URL('./child.js', import.meta.url));
|
|
|
8
8
|
const STDERR_TAIL = 4000;
|
|
9
9
|
const WATCHDOG_POLL_MS = 50;
|
|
10
10
|
const CLOSE_GRACE_MS = 2000;
|
|
11
|
+
const PROGRESS_BYTES = 256;
|
|
11
12
|
function inFlight(progress) {
|
|
12
13
|
try {
|
|
13
14
|
return readFileSync(progress, 'utf8').trim().split(' ')[0] || undefined;
|
|
@@ -56,7 +57,9 @@ class Server {
|
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
run(unit, budget) {
|
|
59
|
-
|
|
60
|
+
const progress = openSync(unit.progress, 'w+');
|
|
61
|
+
const mark = Buffer.alloc(PROGRESS_BYTES);
|
|
62
|
+
const sent = performance.now();
|
|
60
63
|
return new Promise((resolve) => {
|
|
61
64
|
let settled = false;
|
|
62
65
|
const finish = (o) => {
|
|
@@ -64,18 +67,13 @@ class Server {
|
|
|
64
67
|
return;
|
|
65
68
|
settled = true;
|
|
66
69
|
clearInterval(timer);
|
|
70
|
+
closeSync(progress);
|
|
67
71
|
this.onMessage = undefined;
|
|
68
72
|
this.onExit = undefined;
|
|
69
|
-
resolve(o);
|
|
73
|
+
resolve({ ...o, ms: performance.now() - sent });
|
|
70
74
|
};
|
|
71
75
|
const timer = setInterval(() => {
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
line = readFileSync(unit.progress, 'utf8').trim();
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
76
|
+
const line = mark.toString('utf8', 0, readSync(progress, mark, 0, PROGRESS_BYTES, 0)).trim();
|
|
79
77
|
if (!line)
|
|
80
78
|
return;
|
|
81
79
|
const [id = '', since = '0'] = line.split(' ');
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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
|
+
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/ofri-peretz/burgee.git",
|
|
@@ -14,8 +15,13 @@
|
|
|
14
15
|
"homepage": "https://oxmutate.interlace.tools",
|
|
15
16
|
"type": "module",
|
|
16
17
|
"private": false,
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"./dist/bin.js",
|
|
20
|
+
"./dist/child.js",
|
|
21
|
+
"./dist/runner.js"
|
|
22
|
+
],
|
|
17
23
|
"engines": {
|
|
18
|
-
"node": "
|
|
24
|
+
"node": ">=22.13.0"
|
|
19
25
|
},
|
|
20
26
|
"bin": {
|
|
21
27
|
"oxmutate": "./dist/bin.js"
|
|
@@ -57,6 +63,24 @@
|
|
|
57
63
|
]
|
|
58
64
|
}
|
|
59
65
|
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"mutation-testing",
|
|
68
|
+
"mutation",
|
|
69
|
+
"mutation-score",
|
|
70
|
+
"stryker",
|
|
71
|
+
"vitest",
|
|
72
|
+
"oxc",
|
|
73
|
+
"test-quality",
|
|
74
|
+
"sarif",
|
|
75
|
+
"typescript",
|
|
76
|
+
"javascript",
|
|
77
|
+
"ci",
|
|
78
|
+
"cli"
|
|
79
|
+
],
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public",
|
|
82
|
+
"provenance": true
|
|
83
|
+
},
|
|
60
84
|
"files": [
|
|
61
85
|
"dist",
|
|
62
86
|
"binding/oxmutate.*.node",
|
|
@@ -75,8 +99,8 @@
|
|
|
75
99
|
"compat:mutation": "vitest run --config compat/vitest.config.ts"
|
|
76
100
|
},
|
|
77
101
|
"dependencies": {
|
|
78
|
-
"bellpull": "^
|
|
79
|
-
"burgee": "^
|
|
102
|
+
"bellpull": "^2.0.0",
|
|
103
|
+
"burgee": "^2.0.0"
|
|
80
104
|
},
|
|
81
105
|
"peerDependencies": {
|
|
82
106
|
"vitest": ">=4.1.0"
|