oxmutate 1.2.3 → 1.2.4
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 +122 -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,60 @@ 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
|
+
if (!file || probed.has(file))
|
|
120
|
+
return;
|
|
121
|
+
probed.add(file);
|
|
122
|
+
await afterEach(test, test.context, []);
|
|
123
|
+
const cleanups = await beforeEach(test, test.context);
|
|
124
|
+
ns.currentTestId = PROBE_ID;
|
|
125
|
+
try {
|
|
126
|
+
await fn();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
runOnce.add(file);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
ns.currentTestId = test.id;
|
|
133
|
+
if (ns.mutantCoverage)
|
|
134
|
+
delete ns.mutantCoverage.perTest[PROBE_ID];
|
|
135
|
+
for (const cleanup of cleanups.reverse())
|
|
136
|
+
await cleanup();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
85
139
|
const POLLUTION_ERROR_CHARS = 300;
|
|
86
140
|
const FILE_ERROR_CHARS = 500;
|
|
87
141
|
function emit(record) {
|
|
@@ -194,24 +248,38 @@ export default class OxmutateRunner extends Base {
|
|
|
194
248
|
markTest(true);
|
|
195
249
|
try {
|
|
196
250
|
await fn();
|
|
251
|
+
if (mode === 'dry')
|
|
252
|
+
await probe(test, fn);
|
|
197
253
|
}
|
|
198
254
|
finally {
|
|
199
255
|
markTest(false);
|
|
200
256
|
}
|
|
201
257
|
return;
|
|
202
258
|
}
|
|
203
|
-
|
|
259
|
+
let current = test;
|
|
260
|
+
let run = fn;
|
|
261
|
+
let context = test.context;
|
|
262
|
+
let invoked = false;
|
|
204
263
|
const spec = specTests.has(test.id);
|
|
205
264
|
const done = spec ? decidedBySpec : decided;
|
|
206
265
|
const ids = polluted ? [] : (plan.tests[test.id] ?? []).filter((id) => !done.has(id));
|
|
207
266
|
let cleanups = [];
|
|
208
267
|
let attempt = 0;
|
|
209
268
|
const teardown = async () => {
|
|
210
|
-
await afterEach(
|
|
269
|
+
await afterEach(current, context, cleanups);
|
|
211
270
|
cleanups = [];
|
|
212
271
|
};
|
|
213
272
|
const setup = async () => {
|
|
214
|
-
|
|
273
|
+
if (invoked && test.file && runOnce.has(test.file.filepath)) {
|
|
274
|
+
const copy = takeCopy(test);
|
|
275
|
+
if (!copy)
|
|
276
|
+
throw new Error('oxmutate: a run-once test ran out of fresh collections');
|
|
277
|
+
current = copy;
|
|
278
|
+
run = statics.getTestFn(copy) ?? fn;
|
|
279
|
+
context = copy.context;
|
|
280
|
+
}
|
|
281
|
+
invoked = true;
|
|
282
|
+
cleanups = await beforeEach(current, context);
|
|
215
283
|
await this.retryReset(test, ++attempt);
|
|
216
284
|
};
|
|
217
285
|
const fresh = async () => {
|
|
@@ -224,7 +292,7 @@ export default class OxmutateRunner extends Base {
|
|
|
224
292
|
return true;
|
|
225
293
|
await fresh();
|
|
226
294
|
try {
|
|
227
|
-
await
|
|
295
|
+
await run();
|
|
228
296
|
unchecked = [];
|
|
229
297
|
canaryEvery = Math.min(MAX_CANARY_EVERY, canaryEvery * 2);
|
|
230
298
|
return true;
|
|
@@ -245,7 +313,7 @@ export default class OxmutateRunner extends Base {
|
|
|
245
313
|
let status = 'Survived';
|
|
246
314
|
try {
|
|
247
315
|
await setup();
|
|
248
|
-
await
|
|
316
|
+
await run();
|
|
249
317
|
}
|
|
250
318
|
catch (e) {
|
|
251
319
|
status = isHitLimit(e) ? 'Timeout' : 'Killed';
|
|
@@ -282,7 +350,7 @@ export default class OxmutateRunner extends Base {
|
|
|
282
350
|
if (attempt > 0)
|
|
283
351
|
await fresh();
|
|
284
352
|
try {
|
|
285
|
-
await
|
|
353
|
+
await run();
|
|
286
354
|
}
|
|
287
355
|
finally {
|
|
288
356
|
for (const cleanup of cleanups.reverse())
|
|
@@ -294,6 +362,15 @@ export default class OxmutateRunner extends Base {
|
|
|
294
362
|
pristine = snapshot();
|
|
295
363
|
return super.importFile?.(filepath, source);
|
|
296
364
|
}
|
|
365
|
+
async onAfterRunSuite(suite) {
|
|
366
|
+
const copies = suite.filepath ? collections.get(suite.filepath) : undefined;
|
|
367
|
+
if (suite.filepath && copies) {
|
|
368
|
+
collections.delete(suite.filepath);
|
|
369
|
+
for (const copy of copies)
|
|
370
|
+
await afterAllOf(copy);
|
|
371
|
+
}
|
|
372
|
+
return super.onAfterRunSuite?.(suite);
|
|
373
|
+
}
|
|
297
374
|
async onBeforeRunSuite(suite) {
|
|
298
375
|
if (mode === 'static' && suite.filepath && staticIds.length)
|
|
299
376
|
await this.decideStatic(suite);
|
|
@@ -374,6 +451,44 @@ export default class OxmutateRunner extends Base {
|
|
|
374
451
|
writeFileSync(progressPath, '');
|
|
375
452
|
vi.resetModules();
|
|
376
453
|
}
|
|
454
|
+
async onBeforeRunTask(test) {
|
|
455
|
+
const filepath = test.file?.filepath;
|
|
456
|
+
if (mode === 'mutant' && !polluted && filepath && runOnce.has(filepath)) {
|
|
457
|
+
const done = specTests.has(test.id) ? decidedBySpec : decided;
|
|
458
|
+
const runs = (plan.tests[test.id] ?? []).filter((id) => !done.has(id)).length * RUNS_PER_MUTANT + RUNS_AFTER;
|
|
459
|
+
if (!(await this.collectCopies(filepath, (taken.get(test.id) ?? 0) + runs)))
|
|
460
|
+
runOnce.delete(filepath);
|
|
461
|
+
}
|
|
462
|
+
return super.onBeforeRunTask?.(test);
|
|
463
|
+
}
|
|
464
|
+
async collectCopies(filepath, count) {
|
|
465
|
+
const files = collections.get(filepath) ?? [];
|
|
466
|
+
collections.set(filepath, files);
|
|
467
|
+
if (files.length >= count)
|
|
468
|
+
return true;
|
|
469
|
+
const collect = await loadCollect();
|
|
470
|
+
if (!collect)
|
|
471
|
+
return false;
|
|
472
|
+
const { workerState } = this;
|
|
473
|
+
const was = { current: workerState?.current, spec: this._currentSpecification };
|
|
474
|
+
try {
|
|
475
|
+
while (files.length < count) {
|
|
476
|
+
const node = workerState?.evaluatedModules?.getModuleById(filepath);
|
|
477
|
+
if (node)
|
|
478
|
+
workerState?.evaluatedModules?.invalidateModule(node);
|
|
479
|
+
const [file] = await collect([filepath], this);
|
|
480
|
+
if (!file || file.result?.state === 'fail' || hasBeforeAll(file))
|
|
481
|
+
return false;
|
|
482
|
+
files.push(file);
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
if (workerState)
|
|
488
|
+
workerState.current = was.current;
|
|
489
|
+
this._currentSpecification = was.spec;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
377
492
|
async retryReset(test, attempt) {
|
|
378
493
|
await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
|
|
379
494
|
}
|
|
@@ -394,7 +509,7 @@ export default class OxmutateRunner extends Base {
|
|
|
394
509
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
395
510
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
396
511
|
}
|
|
397
|
-
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors });
|
|
512
|
+
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce] });
|
|
398
513
|
}
|
|
399
514
|
else if (mode === 'isolated') {
|
|
400
515
|
const failed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
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": {
|