oxmutate 1.4.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 +3 -0
- package/dist/coordinator.js +38 -4
- package/dist/reports.js +1 -1
- package/dist/runner.d.ts +1 -0
- package/dist/runner.js +6 -1
- package/package.json +1 -1
|
Binary file
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export interface RunStats {
|
|
|
50
50
|
idleStalls: number;
|
|
51
51
|
crashes: number;
|
|
52
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;
|
|
53
56
|
/** Contexts restarted because a mutant left state behind (the per-execution canary fired). */
|
|
54
57
|
pollutionRestarts: number;
|
|
55
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. */
|
package/dist/coordinator.js
CHANGED
|
@@ -12,6 +12,7 @@ import { globToRegExp, matchFiles } from './files.js';
|
|
|
12
12
|
import { changedLines, parseMutateSpec, shardOf, touches, withinRange } from './lanes.js';
|
|
13
13
|
import { native } from './native.js';
|
|
14
14
|
import { startProgress } from './progress.js';
|
|
15
|
+
import { VERSION } from './reports.js';
|
|
15
16
|
import { FILE_KEY, env, stderr } from './runtime.js';
|
|
16
17
|
import { createPool } from './scheduler.js';
|
|
17
18
|
import { summarizeSpec, tracesFor } from './spec-oracle.js';
|
|
@@ -27,7 +28,26 @@ function readRecords(file) {
|
|
|
27
28
|
.map((l) => JSON.parse(l));
|
|
28
29
|
}
|
|
29
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
|
+
});
|
|
30
48
|
const UNIT_PAIRS = 400;
|
|
49
|
+
const detected = (s) => s === 'Killed' || s === 'Timeout';
|
|
50
|
+
const AUDIT_SHARE = 0.05;
|
|
31
51
|
const DEADLOCK_REASON = 'its test deadlocked: nothing left in the worker could resume it (a hang, called in under a second)';
|
|
32
52
|
function timeoutReason(v) {
|
|
33
53
|
if (v.deadlock)
|
|
@@ -54,7 +74,7 @@ export async function run(options) {
|
|
|
54
74
|
const work = mkdtempSync(path.join(tmpdir(), 'oxmutate-'));
|
|
55
75
|
const stats = {
|
|
56
76
|
files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0,
|
|
57
|
-
watchdogKills: 0, idleStalls: 0, deadlocks: 0, crashes: 0, canaryFiles: 0, pollutionRestarts: 0, diskWrites: 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,
|
|
58
78
|
};
|
|
59
79
|
let unitSeq = 0;
|
|
60
80
|
const fileFor = (kind, ext) => path.join(work, `${kind}-${String(unitSeq++)}.${ext}`);
|
|
@@ -169,7 +189,7 @@ export async function run(options) {
|
|
|
169
189
|
if (fileErrors.length) {
|
|
170
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('; ')}`);
|
|
171
191
|
}
|
|
172
|
-
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 ?? {})) };
|
|
173
193
|
};
|
|
174
194
|
progress.phase('dry run');
|
|
175
195
|
const d0 = performance.now();
|
|
@@ -263,13 +283,23 @@ export async function run(options) {
|
|
|
263
283
|
}
|
|
264
284
|
return h;
|
|
265
285
|
};
|
|
266
|
-
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();
|
|
267
293
|
const cache = options.incremental && existsSync(options.incrementalFile) ? JSON.parse(readFileSync(options.incrementalFile, 'utf8')).mutants : {};
|
|
268
294
|
for (const m of mutants) {
|
|
269
295
|
if (m.status === 'Ignored' || (!m.static && m.coveredBy.length === 0))
|
|
270
296
|
continue;
|
|
271
297
|
const hit = cache[m.id];
|
|
272
298
|
if (hit && hit.fp === fingerprint(m)) {
|
|
299
|
+
if (Math.random() < AUDIT_SHARE) {
|
|
300
|
+
audited.set(m.id, hit.status);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
273
303
|
m.status = hit.status;
|
|
274
304
|
m.killedBy = hit.killedBy;
|
|
275
305
|
m.testsCompleted = hit.testsCompleted;
|
|
@@ -729,7 +759,11 @@ export async function run(options) {
|
|
|
729
759
|
stats.crashes = pool.stats.crashed;
|
|
730
760
|
if (options.incremental) {
|
|
731
761
|
mkdirSync(path.dirname(options.incrementalFile), { recursive: true });
|
|
732
|
-
|
|
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 };
|
|
733
767
|
for (const m of mutants) {
|
|
734
768
|
if (['Killed', 'Survived', 'Timeout'].includes(m.status) && !m.flaky)
|
|
735
769
|
next[m.id] = { fp: fingerprint(m), status: m.status, killedBy: m.killedBy, testsCompleted: m.testsCompleted };
|
package/dist/reports.js
CHANGED
|
@@ -102,7 +102,7 @@ function corrections(s) {
|
|
|
102
102
|
s.survivorsOverturned && `${String(s.survivorsOverturned)} survivor(s) killed by a whole-file re-run`,
|
|
103
103
|
s.timeoutsOverturned && `${String(s.timeoutsOverturned)} timeout(s) that finished given 3x the time`,
|
|
104
104
|
s.compileErrors && `${String(s.compileErrors)} survivor(s) that do not type-check, out of the score`,
|
|
105
|
-
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)` : ''}`,
|
|
106
106
|
s.crashes && `${String(s.crashes)} worker crash(es)`,
|
|
107
107
|
].filter((c) => Boolean(c));
|
|
108
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/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": {
|