oxmutate 1.2.3 → 1.2.5
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 +3 -3
- package/dist/runner.d.ts +23 -0
- package/dist/runner.js +123 -7
- package/package.json +1 -1
|
Binary file
|
package/dist/coordinator.js
CHANGED
|
@@ -159,7 +159,7 @@ export async function run(options) {
|
|
|
159
159
|
if (fileErrors.length) {
|
|
160
160
|
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('; ')}`);
|
|
161
161
|
}
|
|
162
|
-
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat };
|
|
162
|
+
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []) };
|
|
163
163
|
};
|
|
164
164
|
progress.phase('dry run');
|
|
165
165
|
const d0 = performance.now();
|
|
@@ -287,7 +287,7 @@ export async function run(options) {
|
|
|
287
287
|
const unfinished = new Set();
|
|
288
288
|
const makeUnit = (file, plan, depth = 0) => {
|
|
289
289
|
const unit = { mode: 'mutant', files: [file], results: fileFor('unit', 'jsonl'), progress: fileFor('unit', 'progress'), plan: fileFor('plan', 'json') };
|
|
290
|
-
writeFileSync(unit.plan, JSON.stringify({ tests: plan, hitLimits, skip: [], specTests: [...specOf.keys()] }));
|
|
290
|
+
writeFileSync(unit.plan, JSON.stringify({ tests: plan, hitLimits, skip: [], specTests: [...specOf.keys()], runOnce: dry.runOnce }));
|
|
291
291
|
planUnits.set(unit.results, { file, plan, depth });
|
|
292
292
|
stats.units++;
|
|
293
293
|
return unit;
|
|
@@ -384,7 +384,7 @@ export async function run(options) {
|
|
|
384
384
|
if (Object.keys(left).length === 0)
|
|
385
385
|
return false;
|
|
386
386
|
entry.plan = left;
|
|
387
|
-
writeFileSync(unit.plan, JSON.stringify({ tests: left, hitLimits, skip: [], specTests: [...specOf.keys()] }));
|
|
387
|
+
writeFileSync(unit.plan, JSON.stringify({ tests: left, hitLimits, skip: [], specTests: [...specOf.keys()], runOnce: dry.runOnce }));
|
|
388
388
|
return true;
|
|
389
389
|
};
|
|
390
390
|
await pool.drain(unitsForRound(toRun, (m) => fileOrder.get(m.id) ?? []), (_, id) => warmBudget(id), afterUnit, beforeUnit);
|
package/dist/runner.d.ts
CHANGED
|
@@ -26,7 +26,18 @@ declare const OxmutateRunner_base: new (config: unknown) => {
|
|
|
26
26
|
onBeforeCollect?(paths: string[]): unknown;
|
|
27
27
|
importFile?(filepath: string, source: string): unknown;
|
|
28
28
|
onBeforeRunSuite?(suite: AnyTask): unknown;
|
|
29
|
+
onAfterRunSuite?(suite: AnyTask): unknown;
|
|
30
|
+
onBeforeRunTask?(test: AnyTask): unknown;
|
|
29
31
|
onCollectStart?(file: unknown): unknown;
|
|
32
|
+
/** Vitest's own: the current task, and the module graph a test file is invalidated in (Vitest 4). */
|
|
33
|
+
workerState?: {
|
|
34
|
+
current?: unknown;
|
|
35
|
+
evaluatedModules?: {
|
|
36
|
+
getModuleById: (id: string) => unknown;
|
|
37
|
+
invalidateModule: (node: unknown) => void;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
_currentSpecification?: unknown;
|
|
30
41
|
onBeforeTryTask?(test: AnyTask, options: {
|
|
31
42
|
retry: number;
|
|
32
43
|
repeats: number;
|
|
@@ -53,6 +64,7 @@ export default class OxmutateRunner extends OxmutateRunner_base {
|
|
|
53
64
|
* each time (the family's colour setup sets TERM, COLORTERM and NO_COLOR), so they are not leaks.
|
|
54
65
|
*/
|
|
55
66
|
importFile(filepath: string, source: string): unknown;
|
|
67
|
+
onAfterRunSuite(suite: AnyTask): Promise<unknown>;
|
|
56
68
|
onBeforeRunSuite(suite: AnyTask): Promise<unknown>;
|
|
57
69
|
/**
|
|
58
70
|
* `static` mode (.sdlc/intents/oxmutate/staticwarm.qnt). Re-evaluating the project's modules in
|
|
@@ -63,6 +75,17 @@ export default class OxmutateRunner extends OxmutateRunner_base {
|
|
|
63
75
|
* rest of the batch back to the fresh-worker path.
|
|
64
76
|
*/
|
|
65
77
|
private decideStatic;
|
|
78
|
+
/**
|
|
79
|
+
* Collected here, before Vitest marks the test running: it refuses `it()` inside a running test.
|
|
80
|
+
* A file whose copies cannot be collected falls back to the canary restarting its context.
|
|
81
|
+
*/
|
|
82
|
+
onBeforeRunTask(test: AnyTask): Promise<unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Fresh collections of `filepath` until there are `count`. Only the test file is evaluated again
|
|
85
|
+
* (Vitest 5 re-imports it; on Vitest 4 its module is invalidated first), so each costs the file's
|
|
86
|
+
* own top level, not its imports.
|
|
87
|
+
*/
|
|
88
|
+
private collectCopies;
|
|
66
89
|
/** Reset per-try state (snapshot counters, assertion counts) the way Vitest does for a retry. */
|
|
67
90
|
private retryReset;
|
|
68
91
|
onAfterRunFiles(files: AnyTask[]): unknown;
|
package/dist/runner.js
CHANGED
|
@@ -82,6 +82,61 @@ let polluted = false;
|
|
|
82
82
|
const STATIC_CANARY_MAX = 4;
|
|
83
83
|
let canaryEvery = 1;
|
|
84
84
|
const MAX_CANARY_EVERY = 8;
|
|
85
|
+
const runOnce = new Set(mode === 'mutant' ? (plan.runOnce ?? []) : []);
|
|
86
|
+
const probed = new Set();
|
|
87
|
+
const collections = new Map();
|
|
88
|
+
const taken = new Map();
|
|
89
|
+
const RUNS_PER_MUTANT = 4;
|
|
90
|
+
const RUNS_AFTER = 2;
|
|
91
|
+
async function findCollect() {
|
|
92
|
+
const browser = (await fromProject('vitest/internal/browser').catch(() => ({})));
|
|
93
|
+
if (browser.collectTests)
|
|
94
|
+
return browser.collectTests;
|
|
95
|
+
const vitestDir = path.dirname(createRequire(path.join(cwd(), 'package.json')).resolve('vitest/package.json'));
|
|
96
|
+
return (await fromProject('@vitest/runner', vitestDir).catch(() => ({}))).collectTests;
|
|
97
|
+
}
|
|
98
|
+
let collector;
|
|
99
|
+
const loadCollect = () => (collector ??= findCollect());
|
|
100
|
+
const find = (t, id) => (t.id === id ? t : t.tasks?.reduce((hit, c) => hit ?? find(c, id), undefined));
|
|
101
|
+
const hasBeforeAll = (t) => t.type !== 'test' && (statics.getSuiteHooks(t).beforeAll.length > 0 || (t.tasks ?? []).some(hasBeforeAll));
|
|
102
|
+
async function afterAllOf(t) {
|
|
103
|
+
if (t.type === 'test')
|
|
104
|
+
return;
|
|
105
|
+
for (const c of t.tasks ?? [])
|
|
106
|
+
await afterAllOf(c);
|
|
107
|
+
for (const hook of statics.getSuiteHooks(t).afterAll)
|
|
108
|
+
await hook(t);
|
|
109
|
+
}
|
|
110
|
+
function takeCopy(test) {
|
|
111
|
+
const n = taken.get(test.id) ?? 0;
|
|
112
|
+
taken.set(test.id, n + 1);
|
|
113
|
+
const file = collections.get(test.file?.filepath ?? '')?.[n];
|
|
114
|
+
return file && find(file, test.id);
|
|
115
|
+
}
|
|
116
|
+
const PROBE_ID = '__oxmutate_probe__';
|
|
117
|
+
async function probe(test, fn) {
|
|
118
|
+
const file = test.file?.filepath;
|
|
119
|
+
const scope = test.suite?.id ?? file;
|
|
120
|
+
if (!file || !scope || probed.has(scope) || runOnce.has(file))
|
|
121
|
+
return;
|
|
122
|
+
probed.add(scope);
|
|
123
|
+
await afterEach(test, test.context, []);
|
|
124
|
+
const cleanups = await beforeEach(test, test.context);
|
|
125
|
+
ns.currentTestId = PROBE_ID;
|
|
126
|
+
try {
|
|
127
|
+
await fn();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
runOnce.add(file);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
ns.currentTestId = test.id;
|
|
134
|
+
if (ns.mutantCoverage)
|
|
135
|
+
delete ns.mutantCoverage.perTest[PROBE_ID];
|
|
136
|
+
for (const cleanup of cleanups.reverse())
|
|
137
|
+
await cleanup();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
85
140
|
const POLLUTION_ERROR_CHARS = 300;
|
|
86
141
|
const FILE_ERROR_CHARS = 500;
|
|
87
142
|
function emit(record) {
|
|
@@ -194,24 +249,38 @@ export default class OxmutateRunner extends Base {
|
|
|
194
249
|
markTest(true);
|
|
195
250
|
try {
|
|
196
251
|
await fn();
|
|
252
|
+
if (mode === 'dry')
|
|
253
|
+
await probe(test, fn);
|
|
197
254
|
}
|
|
198
255
|
finally {
|
|
199
256
|
markTest(false);
|
|
200
257
|
}
|
|
201
258
|
return;
|
|
202
259
|
}
|
|
203
|
-
|
|
260
|
+
let current = test;
|
|
261
|
+
let run = fn;
|
|
262
|
+
let context = test.context;
|
|
263
|
+
let invoked = false;
|
|
204
264
|
const spec = specTests.has(test.id);
|
|
205
265
|
const done = spec ? decidedBySpec : decided;
|
|
206
266
|
const ids = polluted ? [] : (plan.tests[test.id] ?? []).filter((id) => !done.has(id));
|
|
207
267
|
let cleanups = [];
|
|
208
268
|
let attempt = 0;
|
|
209
269
|
const teardown = async () => {
|
|
210
|
-
await afterEach(
|
|
270
|
+
await afterEach(current, context, cleanups);
|
|
211
271
|
cleanups = [];
|
|
212
272
|
};
|
|
213
273
|
const setup = async () => {
|
|
214
|
-
|
|
274
|
+
if (invoked && test.file && runOnce.has(test.file.filepath)) {
|
|
275
|
+
const copy = takeCopy(test);
|
|
276
|
+
if (!copy)
|
|
277
|
+
throw new Error('oxmutate: a run-once test ran out of fresh collections');
|
|
278
|
+
current = copy;
|
|
279
|
+
run = statics.getTestFn(copy) ?? fn;
|
|
280
|
+
context = copy.context;
|
|
281
|
+
}
|
|
282
|
+
invoked = true;
|
|
283
|
+
cleanups = await beforeEach(current, context);
|
|
215
284
|
await this.retryReset(test, ++attempt);
|
|
216
285
|
};
|
|
217
286
|
const fresh = async () => {
|
|
@@ -224,7 +293,7 @@ export default class OxmutateRunner extends Base {
|
|
|
224
293
|
return true;
|
|
225
294
|
await fresh();
|
|
226
295
|
try {
|
|
227
|
-
await
|
|
296
|
+
await run();
|
|
228
297
|
unchecked = [];
|
|
229
298
|
canaryEvery = Math.min(MAX_CANARY_EVERY, canaryEvery * 2);
|
|
230
299
|
return true;
|
|
@@ -245,7 +314,7 @@ export default class OxmutateRunner extends Base {
|
|
|
245
314
|
let status = 'Survived';
|
|
246
315
|
try {
|
|
247
316
|
await setup();
|
|
248
|
-
await
|
|
317
|
+
await run();
|
|
249
318
|
}
|
|
250
319
|
catch (e) {
|
|
251
320
|
status = isHitLimit(e) ? 'Timeout' : 'Killed';
|
|
@@ -282,7 +351,7 @@ export default class OxmutateRunner extends Base {
|
|
|
282
351
|
if (attempt > 0)
|
|
283
352
|
await fresh();
|
|
284
353
|
try {
|
|
285
|
-
await
|
|
354
|
+
await run();
|
|
286
355
|
}
|
|
287
356
|
finally {
|
|
288
357
|
for (const cleanup of cleanups.reverse())
|
|
@@ -294,6 +363,15 @@ export default class OxmutateRunner extends Base {
|
|
|
294
363
|
pristine = snapshot();
|
|
295
364
|
return super.importFile?.(filepath, source);
|
|
296
365
|
}
|
|
366
|
+
async onAfterRunSuite(suite) {
|
|
367
|
+
const copies = suite.filepath ? collections.get(suite.filepath) : undefined;
|
|
368
|
+
if (suite.filepath && copies) {
|
|
369
|
+
collections.delete(suite.filepath);
|
|
370
|
+
for (const copy of copies)
|
|
371
|
+
await afterAllOf(copy);
|
|
372
|
+
}
|
|
373
|
+
return super.onAfterRunSuite?.(suite);
|
|
374
|
+
}
|
|
297
375
|
async onBeforeRunSuite(suite) {
|
|
298
376
|
if (mode === 'static' && suite.filepath && staticIds.length)
|
|
299
377
|
await this.decideStatic(suite);
|
|
@@ -374,6 +452,44 @@ export default class OxmutateRunner extends Base {
|
|
|
374
452
|
writeFileSync(progressPath, '');
|
|
375
453
|
vi.resetModules();
|
|
376
454
|
}
|
|
455
|
+
async onBeforeRunTask(test) {
|
|
456
|
+
const filepath = test.file?.filepath;
|
|
457
|
+
if (mode === 'mutant' && !polluted && filepath && runOnce.has(filepath)) {
|
|
458
|
+
const done = specTests.has(test.id) ? decidedBySpec : decided;
|
|
459
|
+
const runs = (plan.tests[test.id] ?? []).filter((id) => !done.has(id)).length * RUNS_PER_MUTANT + RUNS_AFTER;
|
|
460
|
+
if (!(await this.collectCopies(filepath, (taken.get(test.id) ?? 0) + runs)))
|
|
461
|
+
runOnce.delete(filepath);
|
|
462
|
+
}
|
|
463
|
+
return super.onBeforeRunTask?.(test);
|
|
464
|
+
}
|
|
465
|
+
async collectCopies(filepath, count) {
|
|
466
|
+
const files = collections.get(filepath) ?? [];
|
|
467
|
+
collections.set(filepath, files);
|
|
468
|
+
if (files.length >= count)
|
|
469
|
+
return true;
|
|
470
|
+
const collect = await loadCollect();
|
|
471
|
+
if (!collect)
|
|
472
|
+
return false;
|
|
473
|
+
const { workerState } = this;
|
|
474
|
+
const was = { current: workerState?.current, spec: this._currentSpecification };
|
|
475
|
+
try {
|
|
476
|
+
while (files.length < count) {
|
|
477
|
+
const node = workerState?.evaluatedModules?.getModuleById(filepath);
|
|
478
|
+
if (node)
|
|
479
|
+
workerState?.evaluatedModules?.invalidateModule(node);
|
|
480
|
+
const [file] = await collect([filepath], this);
|
|
481
|
+
if (!file || file.result?.state === 'fail' || hasBeforeAll(file))
|
|
482
|
+
return false;
|
|
483
|
+
files.push(file);
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
if (workerState)
|
|
489
|
+
workerState.current = was.current;
|
|
490
|
+
this._currentSpecification = was.spec;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
377
493
|
async retryReset(test, attempt) {
|
|
378
494
|
await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
|
|
379
495
|
}
|
|
@@ -394,7 +510,7 @@ export default class OxmutateRunner extends Base {
|
|
|
394
510
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
395
511
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
396
512
|
}
|
|
397
|
-
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors });
|
|
513
|
+
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce] });
|
|
398
514
|
}
|
|
399
515
|
else if (mode === 'isolated') {
|
|
400
516
|
const failed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
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": {
|