oxmutate 2.2.0 → 2.3.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/dist/cli.js CHANGED
@@ -62,6 +62,8 @@ function overridesFrom(o, root) {
62
62
  overrides.ignoreStatic = true;
63
63
  if (typeof o['staticGroup'] === 'number')
64
64
  overrides.staticGroup = Math.max(1, Math.floor(o['staticGroup']));
65
+ if (typeof o['staticBudget'] === 'number' && o['staticBudget'] >= 1)
66
+ overrides.staticBudgetMs = Math.floor(o['staticBudget']);
65
67
  if (o['incremental'] === true)
66
68
  overrides.incremental = true;
67
69
  if (typeof o['flakyReruns'] === 'number')
@@ -93,6 +95,7 @@ const runCommand = defineCommand({
93
95
  strykerCompat: { type: 'boolean', description: 'reproduce Stryker 10.0.0 skips (no mutants inside `as` casts or enums)' },
94
96
  ignoreStatic: { type: 'boolean', description: 'skip static mutants (module-level code): faster, and the score leaves them out' },
95
97
  staticGroup: { type: 'number', description: 'arm this many static mutants together in a file that is not their first (default 1; a heuristic, audited)' },
98
+ staticBudget: { type: 'number', description: 'the most the static phase may take, ms; static mutants still undecided then are left out of the score' },
96
99
  incremental: { type: 'boolean', description: 'reuse verdicts whose source and covering tests did not change' },
97
100
  flakyReruns: { type: 'number', description: 're-runs for verdicts with nondeterministic tests (default 2; 0 disables)' },
98
101
  reporter: { type: 'string', description: 'comma-separated: clear-text, json, sarif, html (default clear-text,json)' },
package/dist/config.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface Thresholds {
6
6
  /** Fail the run (exit 1) below this score. `null`: never fail on score. */
7
7
  break: number | null;
8
8
  }
9
+ /** Why a static mutant is Ignored when `static.budgetMs` ran out before it was decided; the summary counts them by it. */
10
+ export declare const STATIC_BUDGET_REASON = "static mutant (static budget spent)";
9
11
  /** Why a static mutant is Ignored under `ignoreStatic`; the summary counts them by it. */
10
12
  export declare const IGNORE_STATIC_REASON = "static mutant (ignoreStatic)";
11
13
  export interface Options {
@@ -25,6 +27,12 @@ export interface Options {
25
27
  * heuristic: two mutants can hide each other, so one in twenty group survivors is run again alone.
26
28
  */
27
29
  staticGroup: number;
30
+ /**
31
+ * The most the static phase may take, in milliseconds (`static.budgetMs`). No new static unit
32
+ * starts once it is spent; a static mutant still undecided then is Ignored, with a reason, and
33
+ * left out of the score. A kill found before that stands. Unset: no limit.
34
+ */
35
+ staticBudgetMs?: number | undefined;
28
36
  strykerCompat: boolean;
29
37
  reporters: string[];
30
38
  jsonReport: string;
package/dist/config.js CHANGED
@@ -7,6 +7,11 @@ function staticGroupOf(group) {
7
7
  const asked = Math.floor(Number(group));
8
8
  return asked >= 2 ? asked : 1;
9
9
  }
10
+ function staticBudgetOf(ms) {
11
+ const asked = Math.floor(Number(ms));
12
+ return asked >= 1 ? asked : undefined;
13
+ }
14
+ export const STATIC_BUDGET_REASON = 'static mutant (static budget spent)';
10
15
  export const IGNORE_STATIC_REASON = 'static mutant (ignoreStatic)';
11
16
  const NOT_NEEDED = {
12
17
  inPlace: 'oxmutate never copies or rewrites the project; instrumented sources are served from memory',
@@ -105,6 +110,7 @@ export async function loadOptions(root, overrides = {}, configFile) {
105
110
  excludedMutations: mutator.excludedMutations ?? [],
106
111
  ignoreStatic: raw['ignoreStatic'] === true,
107
112
  staticGroup: staticGroupOf(raw['static']?.group),
113
+ staticBudgetMs: staticBudgetOf(raw['static']?.budgetMs),
108
114
  strykerCompat: raw['strykerCompat'] === true,
109
115
  reporters: raw['reporters'] ?? ['clear-text', 'json'],
110
116
  jsonReport: path.resolve(root, json.fileName ?? 'reports/mutation/mutation.json'),
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import { ambientRuntime, run as spawn } from 'bellpull';
6
6
  import { checkSurvivors } from './checker.js';
7
7
  import { runCommandMutants } from './command-runner.js';
8
- import { IGNORE_STATIC_REASON, disables } from './config.js';
8
+ import { IGNORE_STATIC_REASON, STATIC_BUDGET_REASON, disables } from './config.js';
9
9
  import { rangeDisables } from './directives.js';
10
10
  import { guardDisk, isClean } from './diskguard.js';
11
11
  import { globToRegExp, matchFiles } from './files.js';
@@ -43,6 +43,7 @@ const verdictOptions = (o) => ({
43
43
  excludedMutations: [...o.excludedMutations].sort(),
44
44
  ignoreStatic: o.ignoreStatic,
45
45
  staticGroup: o.staticGroup,
46
+ staticBudgetMs: o.staticBudgetMs,
46
47
  strykerCompat: o.strykerCompat,
47
48
  testOverrides: o.testOverrides,
48
49
  vitestConfig: o.vitestConfig && existsSync(o.vitestConfig) ? sha(readFileSync(o.vitestConfig, 'utf8')) : null,
@@ -649,7 +650,11 @@ export async function run(options) {
649
650
  back.add(id);
650
651
  return [];
651
652
  };
653
+ const deadline = options.staticBudgetMs === undefined ? Infinity : performance.now() + options.staticBudgetMs;
654
+ const spent = () => performance.now() >= deadline;
652
655
  const keepUndecided = (unit) => {
656
+ if (spent())
657
+ return false;
653
658
  unit.staticIds = (unit.staticIds ?? []).filter((id) => !killed.has(id) && !back.has(id));
654
659
  if (unit.staticGroups)
655
660
  unit.staticGroups = unit.staticGroups.map((g) => g.filter((id) => !killed.has(id) && !back.has(id))).filter((g) => g.length > 0);
@@ -671,6 +676,10 @@ export async function run(options) {
671
676
  m.status = 'Survived';
672
677
  m.testsCompleted = ran.get(m.id) ?? 0;
673
678
  }
679
+ else if (spent()) {
680
+ m.status = 'Ignored';
681
+ m.statusReason = STATIC_BUDGET_REASON;
682
+ }
674
683
  else
675
684
  undecided.push(m);
676
685
  }
package/dist/reports.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
- import { IGNORE_STATIC_REASON } from './config.js';
4
+ import { IGNORE_STATIC_REASON, STATIC_BUDGET_REASON } from './config.js';
5
5
  export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
6
6
  const REPLACEMENT_WIDTH = 60;
7
7
  const ELLIPSIS = '...';
@@ -128,6 +128,9 @@ function staticLine(r) {
128
128
  if (ignored > 0)
129
129
  return [` static mutants: ${String(ignored)} ignored (ignoreStatic), not scored`];
130
130
  const share = r.stats.wallMs > 0 ? r.stats.staticMs / r.stats.wallMs : 0;
131
+ const unspent = statics.filter((m) => m.statusReason === STATIC_BUDGET_REASON).length;
132
+ if (unspent > 0)
133
+ return [` static mutants: ${String(statics.length - unspent)} decided in ${(r.stats.staticMs / MS_PER_S).toFixed(1)} s, ${String(unspent)} left undecided when the static budget ran out, not scored`];
131
134
  const hint = share >= STATIC_SHARE_WORTH_SAYING ? '; --ignore-static skips them, and the score then leaves them out' : '';
132
135
  return [` static mutants: ${String(statics.length)} decided in ${(r.stats.staticMs / MS_PER_S).toFixed(1)} s, ${(share * PERCENT).toFixed(0)}% of the run${hint}`];
133
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxmutate",
3
- "version": "2.2.0",
3
+ "version": "2.3.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
  "author": "Ofri Peretz <ofriperetzdev@gmail.com>",