oxmutate 2.0.0 → 2.0.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/coordinator.js +16 -6
- package/dist/progress-file.js +22 -0
- package/dist/runner.js +24 -15
- package/dist/scheduler.js +5 -6
- package/package.json +1 -1
|
Binary file
|
package/dist/coordinator.js
CHANGED
|
@@ -54,8 +54,11 @@ function timeoutReason(v) {
|
|
|
54
54
|
return DEADLOCK_REASON;
|
|
55
55
|
if (v.idle)
|
|
56
56
|
return 'its test waited past the budget with the worker idle: a hang, not a slow test';
|
|
57
|
+
if (v.slowTwice)
|
|
58
|
+
return "its tests failed only by their own timeout, in two runs";
|
|
57
59
|
return 'stopped by the watchdog twice, the second time with 3x the budget';
|
|
58
60
|
}
|
|
61
|
+
const slowKill = (v) => v?.status === 'Killed' && v.slow === true;
|
|
59
62
|
const RERUN_SLACK = 3;
|
|
60
63
|
export const MAX_TEST_MS = 5000;
|
|
61
64
|
const MS_PER_MINUTE = 60_000;
|
|
@@ -659,7 +662,7 @@ export async function run(options) {
|
|
|
659
662
|
if (rec.timeout)
|
|
660
663
|
verdicts.set(rec.id, { status: 'Timeout', testsCompleted, ...(rec.deadlock && { idle: true, deadlock: true }) });
|
|
661
664
|
else if (rec.killed)
|
|
662
|
-
verdicts.set(rec.id, { status: 'Killed', killedBy: [...(prev?.killedBy ?? []), ...(rec.failed ?? [])], testsCompleted });
|
|
665
|
+
verdicts.set(rec.id, { status: 'Killed', killedBy: [...(prev?.killedBy ?? []), ...(rec.failed ?? [])], testsCompleted, slow: rec.slow === true && (prev?.status !== 'Killed' || prev.slow === true) });
|
|
663
666
|
else
|
|
664
667
|
verdicts.set(rec.id, { status: prev?.status ?? 'Survived', killedBy: prev?.killedBy, testsCompleted });
|
|
665
668
|
}
|
|
@@ -686,12 +689,19 @@ export async function run(options) {
|
|
|
686
689
|
progress.phase(`isolated runs (static mutants and stragglers: ${String(isolatedQueue.length)})`);
|
|
687
690
|
const i0 = performance.now();
|
|
688
691
|
const first = await runIsolated(isolatedQueue);
|
|
689
|
-
const stopped = isolatedQueue.filter((m) => first.get(m.id)?.status === 'Timeout' && !first.get(m.id)?.idle);
|
|
692
|
+
const stopped = isolatedQueue.filter((m) => (first.get(m.id)?.status === 'Timeout' && !first.get(m.id)?.idle) || slowKill(first.get(m.id)));
|
|
690
693
|
stats.isolatedByCause.rerun += stopped.length;
|
|
691
694
|
const again = await runIsolated(stopped, RERUN_SLACK);
|
|
692
695
|
for (const m of stopped) {
|
|
693
696
|
const v = again.get(m.id);
|
|
694
|
-
if (v
|
|
697
|
+
if (!v || v.status === 'RuntimeError')
|
|
698
|
+
continue;
|
|
699
|
+
if (slowKill(first.get(m.id))) {
|
|
700
|
+
first.set(m.id, slowKill(v) ? { status: 'Timeout', slowTwice: true, testsCompleted: v.testsCompleted } : v);
|
|
701
|
+
if (v.status === 'Survived')
|
|
702
|
+
stats.timeoutsOverturned++;
|
|
703
|
+
}
|
|
704
|
+
else if (v.status !== 'Timeout') {
|
|
695
705
|
first.set(m.id, v);
|
|
696
706
|
stats.timeoutsOverturned++;
|
|
697
707
|
}
|
|
@@ -709,20 +719,20 @@ export async function run(options) {
|
|
|
709
719
|
const survivors = toRun.filter((m) => m.status === 'Survived' && new Set(m.coveredBy).size > 1);
|
|
710
720
|
stats.isolatedByCause.confirm += survivors.length;
|
|
711
721
|
const confirmed = await runIsolated(survivors);
|
|
712
|
-
const stoppedHere = survivors.filter((m) => confirmed.get(m.id)?.status === 'Timeout' && !confirmed.get(m.id)?.idle);
|
|
722
|
+
const stoppedHere = survivors.filter((m) => (confirmed.get(m.id)?.status === 'Timeout' && !confirmed.get(m.id)?.idle) || slowKill(confirmed.get(m.id)));
|
|
713
723
|
stats.isolatedByCause.rerun += stoppedHere.length;
|
|
714
724
|
const recheck = await runIsolated(stoppedHere, RERUN_SLACK);
|
|
715
725
|
for (const m of stoppedHere) {
|
|
716
726
|
const v = recheck.get(m.id);
|
|
717
727
|
if (v)
|
|
718
|
-
confirmed.set(m.id, v);
|
|
728
|
+
confirmed.set(m.id, slowKill(confirmed.get(m.id)) && slowKill(v) ? { status: 'Timeout', slowTwice: true, testsCompleted: v.testsCompleted } : v);
|
|
719
729
|
}
|
|
720
730
|
for (const m of survivors) {
|
|
721
731
|
const v = confirmed.get(m.id);
|
|
722
732
|
if (v && v.status !== 'Survived' && v.status !== 'RuntimeError') {
|
|
723
733
|
m.status = v.status;
|
|
724
734
|
m.killedBy = v.killedBy;
|
|
725
|
-
m.statusReason = 'killed only when the mutant stays active across the whole test file (cross-test effect)';
|
|
735
|
+
m.statusReason = v.slowTwice ? timeoutReason(v) : 'killed only when the mutant stays active across the whole test file (cross-test effect)';
|
|
726
736
|
stats.survivorsOverturned++;
|
|
727
737
|
}
|
|
728
738
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, writeSync } from 'node:fs';
|
|
2
|
+
const MARK_BYTES = 256;
|
|
3
|
+
export function markWriter(file) {
|
|
4
|
+
const fd = openSync(file, 'w');
|
|
5
|
+
let length = 0;
|
|
6
|
+
return {
|
|
7
|
+
write(text) {
|
|
8
|
+
const padded = text.padEnd(length);
|
|
9
|
+
length = padded.length;
|
|
10
|
+
writeSync(fd, padded, 0);
|
|
11
|
+
},
|
|
12
|
+
close: () => closeSync(fd),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function markReader(file) {
|
|
16
|
+
const fd = openSync(file, 'w+');
|
|
17
|
+
const buffer = Buffer.alloc(MARK_BYTES);
|
|
18
|
+
return {
|
|
19
|
+
read: () => buffer.toString('utf8', 0, readSync(fd, buffer, 0, MARK_BYTES, 0)).trim(),
|
|
20
|
+
close: () => closeSync(fd),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { availableParallelism, loadavg } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { BEAT_ENV, markTest, realNow, startHeartbeat } from './heartbeat.js';
|
|
6
|
+
import { markWriter } from './progress-file.js';
|
|
6
7
|
import { isDeadlock, takeFloor, trackRpc, trackTimers, watch } from './quiescence.js';
|
|
7
8
|
import { resolveEsm } from './resolve.js';
|
|
8
9
|
import { ACTIVE_MUTANT_ENV, FILE_KEY, NAMESPACE, cwd, env } from './runtime.js';
|
|
@@ -154,8 +155,12 @@ async function probe(test, fn, reset) {
|
|
|
154
155
|
const POLLUTION_ERROR_CHARS = 300;
|
|
155
156
|
const FILE_ERROR_CHARS = 500;
|
|
156
157
|
let resultsFd;
|
|
157
|
-
let
|
|
158
|
-
|
|
158
|
+
let progress;
|
|
159
|
+
const OWN_TIMEOUT = /^(?:Test|Hook) timed out in \d+ms/u;
|
|
160
|
+
function ownTimeout(t) {
|
|
161
|
+
const errors = t.result?.errors ?? [];
|
|
162
|
+
return errors.length > 0 && errors.every((e) => OWN_TIMEOUT.test(e.message ?? ''));
|
|
163
|
+
}
|
|
159
164
|
function emit(record) {
|
|
160
165
|
if (!resultsPath)
|
|
161
166
|
return;
|
|
@@ -165,19 +170,15 @@ function emit(record) {
|
|
|
165
170
|
function mark(text) {
|
|
166
171
|
if (!progressPath)
|
|
167
172
|
return;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
markLength = padded.length;
|
|
171
|
-
if (padded)
|
|
172
|
-
writeSync(progressFd, padded, 0);
|
|
173
|
+
progress ??= markWriter(progressPath);
|
|
174
|
+
progress.write(text);
|
|
173
175
|
}
|
|
174
176
|
function closeFiles() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
if (resultsFd !== undefined)
|
|
178
|
+
closeSync(resultsFd);
|
|
179
|
+
progress?.close();
|
|
178
180
|
resultsFd = undefined;
|
|
179
|
-
|
|
180
|
-
markLength = 0;
|
|
181
|
+
progress = undefined;
|
|
181
182
|
}
|
|
182
183
|
function chain(test) {
|
|
183
184
|
const suites = [];
|
|
@@ -617,24 +618,32 @@ export default class OxmutateRunner extends Base {
|
|
|
617
618
|
else if (mode === 'isolated') {
|
|
618
619
|
const failed = [];
|
|
619
620
|
let ran = 0;
|
|
621
|
+
let decisive = 0;
|
|
620
622
|
const visit = (t) => {
|
|
621
623
|
if (t.type === 'test' && (t.result?.state === 'pass' || t.result?.state === 'fail'))
|
|
622
624
|
ran++;
|
|
623
|
-
if (t.type === 'test' && t.result?.state === 'fail')
|
|
625
|
+
if (t.type === 'test' && t.result?.state === 'fail') {
|
|
624
626
|
failed.push(t.id);
|
|
627
|
+
if (!ownTimeout(t))
|
|
628
|
+
decisive++;
|
|
629
|
+
}
|
|
625
630
|
for (const c of t.tasks ?? [])
|
|
626
631
|
visit(c);
|
|
627
632
|
};
|
|
628
633
|
for (const f of files) {
|
|
629
634
|
visit(f);
|
|
630
|
-
if (f.result?.state === 'fail' && !failed.length)
|
|
635
|
+
if (f.result?.state === 'fail' && !failed.length) {
|
|
631
636
|
failed.push(f.id);
|
|
637
|
+
decisive++;
|
|
638
|
+
}
|
|
632
639
|
}
|
|
633
640
|
const [first] = files;
|
|
641
|
+
if (unhandled > 0)
|
|
642
|
+
decisive++;
|
|
634
643
|
if (unhandled > 0 && !failed.length && first)
|
|
635
644
|
failed.push(first.id);
|
|
636
645
|
const timeout = ranAway() || deadlocked;
|
|
637
|
-
emit({ type: 'isolated', id: env[ACTIVE_MUTANT_ENV] ?? ns.activeMutant, killed: failed.length > 0 && !timeout, failed, ran, ...(timeout && { timeout }), ...(deadlocked && { deadlock: true }) });
|
|
646
|
+
emit({ type: 'isolated', id: env[ACTIVE_MUTANT_ENV] ?? ns.activeMutant, killed: failed.length > 0 && !timeout, failed, ran, ...(timeout && { timeout }), ...(deadlocked && { deadlock: true }), ...(failed.length > 0 && decisive === 0 && { slow: true }) });
|
|
638
647
|
}
|
|
639
648
|
closeFiles();
|
|
640
649
|
return super.onAfterRunFiles?.(files);
|
package/dist/scheduler.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, 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';
|
|
6
|
+
import { markReader } from './progress-file.js';
|
|
6
7
|
import { env, stderr } from './runtime.js';
|
|
7
8
|
const childJs = fileURLToPath(new URL('./child.js', import.meta.url));
|
|
8
9
|
const STDERR_TAIL = 4000;
|
|
9
10
|
const WATCHDOG_POLL_MS = 50;
|
|
10
11
|
const CLOSE_GRACE_MS = 2000;
|
|
11
|
-
const PROGRESS_BYTES = 256;
|
|
12
12
|
function inFlight(progress) {
|
|
13
13
|
try {
|
|
14
14
|
return readFileSync(progress, 'utf8').trim().split(' ')[0] || undefined;
|
|
@@ -57,8 +57,7 @@ class Server {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
run(unit, budget) {
|
|
60
|
-
const progress =
|
|
61
|
-
const mark = Buffer.alloc(PROGRESS_BYTES);
|
|
60
|
+
const progress = markReader(unit.progress);
|
|
62
61
|
const sent = performance.now();
|
|
63
62
|
return new Promise((resolve) => {
|
|
64
63
|
let settled = false;
|
|
@@ -67,13 +66,13 @@ class Server {
|
|
|
67
66
|
return;
|
|
68
67
|
settled = true;
|
|
69
68
|
clearInterval(timer);
|
|
70
|
-
|
|
69
|
+
progress.close();
|
|
71
70
|
this.onMessage = undefined;
|
|
72
71
|
this.onExit = undefined;
|
|
73
72
|
resolve({ ...o, ms: performance.now() - sent });
|
|
74
73
|
};
|
|
75
74
|
const timer = setInterval(() => {
|
|
76
|
-
const line =
|
|
75
|
+
const line = progress.read();
|
|
77
76
|
if (!line)
|
|
78
77
|
return;
|
|
79
78
|
const [id = '', since = '0'] = line.split(' ');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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
|
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|