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.
@@ -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
- const context = test.context;
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(test, context, cleanups);
270
+ await afterEach(current, context, cleanups);
211
271
  cleanups = [];
212
272
  };
213
273
  const setup = async () => {
214
- cleanups = await beforeEach(test, context);
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 fn();
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 fn();
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 fn();
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",
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": {