oxmutate 1.3.0 → 1.5.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/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/coordinator.d.ts +7 -0
- package/dist/coordinator.js +95 -6
- package/dist/diskguard.js +92 -0
- package/dist/reports.js +2 -1
- package/dist/runner.d.ts +1 -0
- package/dist/runner.js +6 -1
- package/dist/scheduler.js +1 -1
- package/package.json +1 -1
|
Binary file
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface MutantResult extends NativeMutant {
|
|
|
23
23
|
flaky?: boolean | undefined;
|
|
24
24
|
/** Reused from the incremental cache, not run. */
|
|
25
25
|
reused?: boolean | undefined;
|
|
26
|
+
/** Paths this mutant's run wrote outside the sandbox, found in its unit and put back (diskguard.ts). */
|
|
27
|
+
wrote?: string[] | undefined;
|
|
26
28
|
}
|
|
27
29
|
export interface TestInfo {
|
|
28
30
|
id: string;
|
|
@@ -48,8 +50,13 @@ export interface RunStats {
|
|
|
48
50
|
idleStalls: number;
|
|
49
51
|
crashes: number;
|
|
50
52
|
canaryFiles: number;
|
|
53
|
+
/** Of the verdicts the incremental cache would have reused, how many were run afresh to check it, and how many disagreed on detected or not. */
|
|
54
|
+
auditSampled: number;
|
|
55
|
+
auditDisagreed: number;
|
|
51
56
|
/** Contexts restarted because a mutant left state behind (the per-execution canary fired). */
|
|
52
57
|
pollutionRestarts: number;
|
|
58
|
+
/** Units after which the project on disk had changed (diskguard.ts): put back, and their verdicts and those of the units running beside them re-run. */
|
|
59
|
+
diskWrites: number;
|
|
53
60
|
/** Survivors of the warm run that a whole-file re-run killed (cross-test effects). */
|
|
54
61
|
survivorsOverturned: number;
|
|
55
62
|
/** Watchdog timeouts that finished when re-run with 3x the budget: the machine was slow, not the mutant. */
|
package/dist/coordinator.js
CHANGED
|
@@ -7,10 +7,12 @@ import { checkSurvivors } from './checker.js';
|
|
|
7
7
|
import { runCommandMutants } from './command-runner.js';
|
|
8
8
|
import { disables } from './config.js';
|
|
9
9
|
import { rangeDisables } from './directives.js';
|
|
10
|
+
import { guardDisk, isClean } from './diskguard.js';
|
|
10
11
|
import { globToRegExp, matchFiles } from './files.js';
|
|
11
12
|
import { changedLines, parseMutateSpec, shardOf, touches, withinRange } from './lanes.js';
|
|
12
13
|
import { native } from './native.js';
|
|
13
14
|
import { startProgress } from './progress.js';
|
|
15
|
+
import { VERSION } from './reports.js';
|
|
14
16
|
import { FILE_KEY, env, stderr } from './runtime.js';
|
|
15
17
|
import { createPool } from './scheduler.js';
|
|
16
18
|
import { summarizeSpec, tracesFor } from './spec-oracle.js';
|
|
@@ -26,7 +28,26 @@ function readRecords(file) {
|
|
|
26
28
|
.map((l) => JSON.parse(l));
|
|
27
29
|
}
|
|
28
30
|
const sha = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
31
|
+
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'];
|
|
32
|
+
function lockfileHash(root) {
|
|
33
|
+
const found = LOCKFILES.map((f) => path.join(root, f)).find((f) => existsSync(f));
|
|
34
|
+
if (found)
|
|
35
|
+
return sha(readFileSync(found, 'utf8'));
|
|
36
|
+
const above = path.dirname(root);
|
|
37
|
+
return above === root ? 'none' : lockfileHash(above);
|
|
38
|
+
}
|
|
39
|
+
const verdictOptions = (o) => ({
|
|
40
|
+
timeoutMS: o.timeoutMS,
|
|
41
|
+
timeoutFactor: o.timeoutFactor,
|
|
42
|
+
excludedMutations: [...o.excludedMutations].sort(),
|
|
43
|
+
ignoreStatic: o.ignoreStatic,
|
|
44
|
+
strykerCompat: o.strykerCompat,
|
|
45
|
+
testOverrides: o.testOverrides,
|
|
46
|
+
vitestConfig: o.vitestConfig && existsSync(o.vitestConfig) ? sha(readFileSync(o.vitestConfig, 'utf8')) : null,
|
|
47
|
+
});
|
|
29
48
|
const UNIT_PAIRS = 400;
|
|
49
|
+
const detected = (s) => s === 'Killed' || s === 'Timeout';
|
|
50
|
+
const AUDIT_SHARE = 0.05;
|
|
30
51
|
const DEADLOCK_REASON = 'its test deadlocked: nothing left in the worker could resume it (a hang, called in under a second)';
|
|
31
52
|
function timeoutReason(v) {
|
|
32
53
|
if (v.deadlock)
|
|
@@ -53,7 +74,7 @@ export async function run(options) {
|
|
|
53
74
|
const work = mkdtempSync(path.join(tmpdir(), 'oxmutate-'));
|
|
54
75
|
const stats = {
|
|
55
76
|
files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0,
|
|
56
|
-
watchdogKills: 0, idleStalls: 0, deadlocks: 0, crashes: 0, canaryFiles: 0, pollutionRestarts: 0, survivorsOverturned: 0, timeoutsOverturned: 0, compileErrors: 0, isolatedRuns: 0, reused: 0, flakyTests: 0, flakyMutants: 0, units: 0, serversSpawned: 0,
|
|
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,
|
|
57
78
|
};
|
|
58
79
|
let unitSeq = 0;
|
|
59
80
|
const fileFor = (kind, ext) => path.join(work, `${kind}-${String(unitSeq++)}.${ext}`);
|
|
@@ -168,7 +189,7 @@ export async function run(options) {
|
|
|
168
189
|
if (fileErrors.length) {
|
|
169
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('; ')}`);
|
|
170
191
|
}
|
|
171
|
-
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []) };
|
|
192
|
+
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []), modules: Object.assign({}, ...recs.map((r) => r.modules ?? {})) };
|
|
172
193
|
};
|
|
173
194
|
progress.phase('dry run');
|
|
174
195
|
const d0 = performance.now();
|
|
@@ -262,13 +283,23 @@ export async function run(options) {
|
|
|
262
283
|
}
|
|
263
284
|
return h;
|
|
264
285
|
};
|
|
265
|
-
const
|
|
286
|
+
const envKey = sha(JSON.stringify({ oxmutate: VERSION, node: process.version, lock: lockfileHash(root), options: verdictOptions(options) }));
|
|
287
|
+
const fingerprint = (m) => {
|
|
288
|
+
const testFiles = new Set([...m.coveredBy.map((t) => testInfo.get(t)?.file ?? ''), ...(staticFiles.get(m.id) ?? [])]);
|
|
289
|
+
const graph = new Set([...testFiles].flatMap((f) => dry.modules[f] ?? []).filter((f) => !testFiles.has(f) && f !== path.join(root, m.file)));
|
|
290
|
+
return sha([envKey, sha(sources[m.file] ?? ''), ...[...testFiles].sort().map(fileHash), '|', ...[...graph].sort().map((f) => `${f}=${fileHash(f)}`)].join(':'));
|
|
291
|
+
};
|
|
292
|
+
const audited = new Map();
|
|
266
293
|
const cache = options.incremental && existsSync(options.incrementalFile) ? JSON.parse(readFileSync(options.incrementalFile, 'utf8')).mutants : {};
|
|
267
294
|
for (const m of mutants) {
|
|
268
295
|
if (m.status === 'Ignored' || (!m.static && m.coveredBy.length === 0))
|
|
269
296
|
continue;
|
|
270
297
|
const hit = cache[m.id];
|
|
271
298
|
if (hit && hit.fp === fingerprint(m)) {
|
|
299
|
+
if (Math.random() < AUDIT_SHARE) {
|
|
300
|
+
audited.set(m.id, hit.status);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
272
303
|
m.status = hit.status;
|
|
273
304
|
m.killedBy = hit.killedBy;
|
|
274
305
|
m.testsCompleted = hit.testsCompleted;
|
|
@@ -332,12 +363,58 @@ export async function run(options) {
|
|
|
332
363
|
return units.sort((a, b) => weight(b) - weight(a));
|
|
333
364
|
};
|
|
334
365
|
const records = [];
|
|
366
|
+
const disk = env['OXMUTATE_NO_DISKGUARD'] ? undefined : await guardDisk(root);
|
|
367
|
+
const inFlight = new Set();
|
|
368
|
+
const tainted = new Set();
|
|
369
|
+
const writers = new Map();
|
|
370
|
+
const diskWrittenDuring = async (unit) => {
|
|
371
|
+
const change = await disk?.check();
|
|
372
|
+
const dirty = change !== undefined && !isClean(change);
|
|
373
|
+
if (dirty) {
|
|
374
|
+
stats.diskWrites++;
|
|
375
|
+
if (!disk?.restore(change)) {
|
|
376
|
+
throw new InstrumentFault(`a mutant wrote outside the sandbox and the project could not be put back: ${[...change.changed, ...change.added, ...change.removed].join(', ')}. fix: restore them (git checkout / git clean), and see which test writes into the project`);
|
|
377
|
+
}
|
|
378
|
+
for (const other of inFlight)
|
|
379
|
+
tainted.add(other);
|
|
380
|
+
}
|
|
381
|
+
if (!dirty && !tainted.delete(unit.results))
|
|
382
|
+
return undefined;
|
|
383
|
+
const entry = planUnits.get(unit.results);
|
|
384
|
+
if (!entry)
|
|
385
|
+
return [];
|
|
386
|
+
const ids = [...new Set(Object.values(entry.plan).flat())];
|
|
387
|
+
const paths = change ? [...change.changed, ...change.added, ...change.removed] : [];
|
|
388
|
+
if (dirty && ids.length === 1) {
|
|
389
|
+
const id = ids[0];
|
|
390
|
+
writers.set(id, paths);
|
|
391
|
+
if (inFlight.size === 0)
|
|
392
|
+
records.push(...readRecords(unit.results));
|
|
393
|
+
else
|
|
394
|
+
unfinished.add(id);
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
if (entry.depth >= 40) {
|
|
398
|
+
for (const id of ids)
|
|
399
|
+
unfinished.add(id);
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
if (ids.length < 2)
|
|
403
|
+
return [makeUnit(entry.file, entry.plan, entry.depth + 1)];
|
|
404
|
+
const half = new Set(ids.slice(0, Math.ceil(ids.length / 2)));
|
|
405
|
+
const pick = (inHalf) => Object.fromEntries(Object.entries(entry.plan).map(([t, l]) => [t, l.filter((id) => half.has(id) === inHalf)]).filter(([, l]) => l.length > 0));
|
|
406
|
+
return [makeUnit(entry.file, pick(true), entry.depth + 1), makeUnit(entry.file, pick(false), entry.depth + 1)];
|
|
407
|
+
};
|
|
335
408
|
const runaways = new Set();
|
|
336
409
|
const crashedIds = new Set();
|
|
337
410
|
const pool = createPool({ root, vitestConfig: options.vitestConfig, workers: 1, instrumentedMap: mapFile, nodeArgs: options.testRunnerNodeArgs, inlineDeps: true }, options.concurrency, work, 'mutant');
|
|
338
411
|
pools.push(pool);
|
|
339
412
|
const killedSoFar = (spec) => new Set([...runaways, ...crashedIds, ...records.filter((r) => r.type === 'mutant' && r.status !== 'Survived' && Boolean(r.spec) === spec).map((r) => r.id ?? '')]);
|
|
340
|
-
const afterUnit = (unit, outcome) => {
|
|
413
|
+
const afterUnit = async (unit, outcome) => {
|
|
414
|
+
inFlight.delete(unit.results);
|
|
415
|
+
const wrote = await diskWrittenDuring(unit);
|
|
416
|
+
if (wrote)
|
|
417
|
+
return wrote;
|
|
341
418
|
const raw = readRecords(unit.results);
|
|
342
419
|
const suspect = new Set(raw.filter((r) => r.type === 'polluted').flatMap((r) => (r.suspects ?? []).map((id) => `${id}|${r.test ?? ''}`)));
|
|
343
420
|
const recs = raw.filter((r) => !(r.type === 'mutant' && suspect.has(`${r.id ?? ''}|${r.test ?? ''}`)));
|
|
@@ -380,6 +457,7 @@ export async function run(options) {
|
|
|
380
457
|
progress.phase('warm runs', killedCount);
|
|
381
458
|
const m0 = performance.now();
|
|
382
459
|
const beforeUnit = (unit) => {
|
|
460
|
+
inFlight.add(unit.results);
|
|
383
461
|
const entry = planUnits.get(unit.results);
|
|
384
462
|
if (!entry)
|
|
385
463
|
return true;
|
|
@@ -391,8 +469,10 @@ export async function run(options) {
|
|
|
391
469
|
if (keep.length)
|
|
392
470
|
left[t] = keep;
|
|
393
471
|
}
|
|
394
|
-
if (Object.keys(left).length === 0)
|
|
472
|
+
if (Object.keys(left).length === 0) {
|
|
473
|
+
inFlight.delete(unit.results);
|
|
395
474
|
return false;
|
|
475
|
+
}
|
|
396
476
|
entry.plan = left;
|
|
397
477
|
writeFileSync(unit.plan, JSON.stringify({ tests: left, hitLimits, skip: [], specTests: [...specOf.keys()], runOnce: dry.runOnce, testMs: testMsOf(left) }));
|
|
398
478
|
return true;
|
|
@@ -670,11 +750,20 @@ export async function run(options) {
|
|
|
670
750
|
stats.isolatedMs = performance.now() - i0;
|
|
671
751
|
stats.watchdogKills = pool.stats.killed;
|
|
672
752
|
stats.idleStalls = pool.stats.idle;
|
|
753
|
+
for (const [id, paths] of writers) {
|
|
754
|
+
const m = byId.get(id);
|
|
755
|
+
if (m)
|
|
756
|
+
m.wrote = paths;
|
|
757
|
+
}
|
|
673
758
|
stats.deadlocks = mutants.filter((m) => m.statusReason === DEADLOCK_REASON).length;
|
|
674
759
|
stats.crashes = pool.stats.crashed;
|
|
675
760
|
if (options.incremental) {
|
|
676
761
|
mkdirSync(path.dirname(options.incrementalFile), { recursive: true });
|
|
677
|
-
|
|
762
|
+
stats.auditSampled = audited.size;
|
|
763
|
+
stats.auditDisagreed = [...audited].filter(([id, was]) => detected(byId.get(id)?.status) !== detected(was)).length;
|
|
764
|
+
if (stats.auditDisagreed > 0)
|
|
765
|
+
stderr.write(`oxmutate: ${String(stats.auditDisagreed)} of ${String(audited.size)} audited cached verdicts disagree with a fresh run; the incremental cache is rebuilt from this run\n`);
|
|
766
|
+
const next = stats.auditDisagreed > 0 ? {} : { ...cache };
|
|
678
767
|
for (const m of mutants) {
|
|
679
768
|
if (['Killed', 'Survived', 'Timeout'].includes(m.status) && !m.flaky)
|
|
680
769
|
next[m.id] = { fp: fingerprint(m), status: m.status, killedBy: m.killedBy, testsCompleted: m.testsCompleted };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ambientRuntime, run } from 'bellpull';
|
|
4
|
+
export const MAX_SNAPSHOT_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
export const isClean = (c) => c.changed.length + c.added.length + c.removed.length === 0;
|
|
6
|
+
async function git(cwd, args) {
|
|
7
|
+
const r = await run('git', args, { cwd, runtime: ambientRuntime() }).catch(() => undefined);
|
|
8
|
+
return r?.ok ? r.stdout : undefined;
|
|
9
|
+
}
|
|
10
|
+
export async function guardDisk(root, maxSnapshotBytes = MAX_SNAPSHOT_BYTES) {
|
|
11
|
+
const listed = await git(root, ['ls-files', '-z', '--cached', '--', '.']);
|
|
12
|
+
if (listed === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const files = listed.split('\0').filter(Boolean);
|
|
15
|
+
const tracked = new Map();
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
for (const rel of files) {
|
|
18
|
+
const abs = path.join(root, rel);
|
|
19
|
+
if (!existsSync(abs))
|
|
20
|
+
continue;
|
|
21
|
+
const st = statSync(abs);
|
|
22
|
+
if (!st.isFile())
|
|
23
|
+
continue;
|
|
24
|
+
bytes += st.size;
|
|
25
|
+
tracked.set(rel, { size: st.size, mtimeMs: st.mtimeMs });
|
|
26
|
+
}
|
|
27
|
+
const restorable = bytes <= maxSnapshotBytes;
|
|
28
|
+
if (restorable)
|
|
29
|
+
for (const [rel, t] of tracked)
|
|
30
|
+
t.content = readFileSync(path.join(root, rel));
|
|
31
|
+
const dirs = new Map();
|
|
32
|
+
for (const rel of tracked.keys()) {
|
|
33
|
+
const dir = path.dirname(rel);
|
|
34
|
+
if (!dirs.has(dir))
|
|
35
|
+
dirs.set(dir, new Set(readdirSync(path.join(root, dir))));
|
|
36
|
+
}
|
|
37
|
+
const knownIgnored = new Map();
|
|
38
|
+
const ignored = async (rels) => {
|
|
39
|
+
const ask = rels.filter((rel) => !knownIgnored.has(rel));
|
|
40
|
+
if (ask.length) {
|
|
41
|
+
const r = await run('git', ['check-ignore', '--', ...ask], { cwd: root, runtime: ambientRuntime() }).catch(() => undefined);
|
|
42
|
+
const yes = new Set((r?.stdout ?? '').split('\n').filter(Boolean));
|
|
43
|
+
for (const rel of ask)
|
|
44
|
+
knownIgnored.set(rel, yes.has(rel));
|
|
45
|
+
}
|
|
46
|
+
return new Set(rels.filter((rel) => knownIgnored.get(rel)));
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
async check() {
|
|
50
|
+
const changed = [];
|
|
51
|
+
const removed = [];
|
|
52
|
+
for (const [rel, t] of tracked) {
|
|
53
|
+
const abs = path.join(root, rel);
|
|
54
|
+
if (!existsSync(abs)) {
|
|
55
|
+
removed.push(rel);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const st = statSync(abs);
|
|
59
|
+
if (st.size !== t.size || st.mtimeMs !== t.mtimeMs)
|
|
60
|
+
changed.push(rel);
|
|
61
|
+
}
|
|
62
|
+
const fresh = [];
|
|
63
|
+
for (const [dir, before] of dirs) {
|
|
64
|
+
const abs = path.join(root, dir);
|
|
65
|
+
if (!existsSync(abs))
|
|
66
|
+
continue;
|
|
67
|
+
for (const name of readdirSync(abs))
|
|
68
|
+
if (!before.has(name))
|
|
69
|
+
fresh.push(path.join(dir, name));
|
|
70
|
+
}
|
|
71
|
+
const skip = await ignored(fresh);
|
|
72
|
+
return { changed, added: fresh.filter((rel) => !skip.has(rel)), removed };
|
|
73
|
+
},
|
|
74
|
+
restore(change) {
|
|
75
|
+
if (!restorable)
|
|
76
|
+
return false;
|
|
77
|
+
for (const rel of [...change.changed, ...change.removed]) {
|
|
78
|
+
const t = tracked.get(rel);
|
|
79
|
+
if (!t?.content)
|
|
80
|
+
return false;
|
|
81
|
+
mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
82
|
+
writeFileSync(path.join(root, rel), t.content);
|
|
83
|
+
const st = statSync(path.join(root, rel));
|
|
84
|
+
t.mtimeMs = st.mtimeMs;
|
|
85
|
+
t.size = st.size;
|
|
86
|
+
}
|
|
87
|
+
for (const rel of change.added)
|
|
88
|
+
rmSync(path.join(root, rel), { recursive: true, force: true });
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/reports.js
CHANGED
|
@@ -96,12 +96,13 @@ function corrections(s) {
|
|
|
96
96
|
return [
|
|
97
97
|
s.watchdogKills && `${String(s.watchdogKills)} runaway(s) stopped`,
|
|
98
98
|
s.deadlocks && `${String(s.deadlocks)} deadlock(s) called by their worker`,
|
|
99
|
+
s.diskWrites && `${String(s.diskWrites)} write(s) outside the sandbox put back, their units re-run`,
|
|
99
100
|
s.pollutionRestarts && `${String(s.pollutionRestarts)} polluted context(s) restarted (in ${String(s.canaryFiles)} file(s))`,
|
|
100
101
|
s.flakyMutants && `${String(s.flakyMutants)} flaky verdict(s)`,
|
|
101
102
|
s.survivorsOverturned && `${String(s.survivorsOverturned)} survivor(s) killed by a whole-file re-run`,
|
|
102
103
|
s.timeoutsOverturned && `${String(s.timeoutsOverturned)} timeout(s) that finished given 3x the time`,
|
|
103
104
|
s.compileErrors && `${String(s.compileErrors)} survivor(s) that do not type-check, out of the score`,
|
|
104
|
-
s.reused && `${String(s.reused)} verdict(s) reused`,
|
|
105
|
+
s.reused && `${String(s.reused)} verdict(s) reused${s.auditSampled ? ` (${String(s.auditSampled)} audited, ${String(s.auditDisagreed)} disagreeing)` : ''}`,
|
|
105
106
|
s.crashes && `${String(s.crashes)} worker crash(es)`,
|
|
106
107
|
].filter((c) => Boolean(c));
|
|
107
108
|
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ declare const OxmutateRunner_base: new (config: unknown) => {
|
|
|
35
35
|
evaluatedModules?: {
|
|
36
36
|
getModuleById: (id: string) => unknown;
|
|
37
37
|
invalidateModule: (node: unknown) => void;
|
|
38
|
+
fileToModulesMap?: Map<string, unknown>;
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
41
|
_currentSpecification?: unknown;
|
package/dist/runner.js
CHANGED
|
@@ -120,6 +120,7 @@ function takeCopy(test) {
|
|
|
120
120
|
const file = collections.get(test.file?.filepath ?? '')?.[n];
|
|
121
121
|
return file && find(file, test.id);
|
|
122
122
|
}
|
|
123
|
+
const dryModules = new Map();
|
|
123
124
|
const PROBE_ID = '__oxmutate_probe__';
|
|
124
125
|
async function probe(test, fn) {
|
|
125
126
|
const file = test.file?.filepath;
|
|
@@ -425,6 +426,10 @@ export default class OxmutateRunner extends Base {
|
|
|
425
426
|
return super.importFile?.(filepath, source);
|
|
426
427
|
}
|
|
427
428
|
async onAfterRunSuite(suite) {
|
|
429
|
+
if (mode === 'dry' && suite.filepath) {
|
|
430
|
+
const files = [...(this.workerState?.evaluatedModules?.fileToModulesMap?.keys() ?? [])];
|
|
431
|
+
dryModules.set(suite.filepath, files.filter((f) => path.isAbsolute(f) && !f.split(path.sep).includes('node_modules')));
|
|
432
|
+
}
|
|
428
433
|
const copies = suite.filepath ? collections.get(suite.filepath) : undefined;
|
|
429
434
|
if (suite.filepath && copies) {
|
|
430
435
|
collections.delete(suite.filepath);
|
|
@@ -574,7 +579,7 @@ export default class OxmutateRunner extends Base {
|
|
|
574
579
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
575
580
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
576
581
|
}
|
|
577
|
-
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce] });
|
|
582
|
+
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules) });
|
|
578
583
|
}
|
|
579
584
|
else if (mode === 'isolated') {
|
|
580
585
|
const failed = [];
|
package/dist/scheduler.js
CHANGED
|
@@ -172,7 +172,7 @@ export function createPool(config, size, workDir, tag) {
|
|
|
172
172
|
if (outcome.killedFor || outcome.crashed) {
|
|
173
173
|
servers[slot] = replacement();
|
|
174
174
|
}
|
|
175
|
-
const more = after(unit, outcome);
|
|
175
|
+
const more = await after(unit, outcome);
|
|
176
176
|
queue.unshift(...more);
|
|
177
177
|
busy--;
|
|
178
178
|
signal();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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": {
|