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.
@@ -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 && v.status !== 'Timeout' && v.status !== 'RuntimeError') {
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 progressFd;
158
- let markLength = 0;
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
- progressFd ??= openSync(progressPath, 'w');
169
- const padded = text.trimEnd().padEnd(markLength);
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
- for (const fd of [resultsFd, progressFd])
176
- if (fd !== undefined)
177
- closeSync(fd);
177
+ if (resultsFd !== undefined)
178
+ closeSync(resultsFd);
179
+ progress?.close();
178
180
  resultsFd = undefined;
179
- progressFd = undefined;
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 { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs';
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 = openSync(unit.progress, 'w+');
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
- closeSync(progress);
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 = mark.toString('utf8', 0, readSync(progress, mark, 0, PROGRESS_BYTES, 0)).trim();
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.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>",