oxmutate 2.3.0 → 2.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.darwin-arm64.node +0 -0
- package/binding/oxmutate.darwin-x64.node +0 -0
- package/binding/oxmutate.linux-arm64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-musl.node +0 -0
- package/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/child.js +4 -0
- package/dist/cli.js +19 -5
- package/dist/config.d.ts +9 -0
- package/dist/config.js +4 -0
- package/dist/coordinator.d.ts +14 -0
- package/dist/coordinator.js +65 -7
- package/dist/native.d.ts +2 -0
- package/dist/reports.js +7 -0
- package/dist/runner.js +1 -1
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/child.js
CHANGED
|
@@ -75,6 +75,10 @@ async function runIsolated(unit) {
|
|
|
75
75
|
}
|
|
76
76
|
async function runUnit(unit) {
|
|
77
77
|
env['OXMUTATE_MODE'] = unit.mode;
|
|
78
|
+
if (unit.trackReads)
|
|
79
|
+
env['__OXMUTATE_TRACK_READS__'] = '1';
|
|
80
|
+
else
|
|
81
|
+
delete env['__OXMUTATE_TRACK_READS__'];
|
|
78
82
|
env['OXMUTATE_RESULTS'] = unit.results;
|
|
79
83
|
env['OXMUTATE_PROGRESS'] = unit.progress;
|
|
80
84
|
if (unit.plan)
|
package/dist/cli.js
CHANGED
|
@@ -44,8 +44,20 @@ function summaryData(r, written) {
|
|
|
44
44
|
return data;
|
|
45
45
|
}
|
|
46
46
|
const WEAKENED_SHOWN = 3;
|
|
47
|
-
function
|
|
47
|
+
function staticOverridesFrom(o) {
|
|
48
48
|
const overrides = {};
|
|
49
|
+
if (typeof o['staticGroup'] === 'number')
|
|
50
|
+
overrides.staticGroup = Math.max(1, Math.floor(o['staticGroup']));
|
|
51
|
+
if (typeof o['staticBudget'] === 'number' && o['staticBudget'] >= 1)
|
|
52
|
+
overrides.staticBudgetMs = Math.floor(o['staticBudget']);
|
|
53
|
+
if (o['reportStaticReaders'] === true)
|
|
54
|
+
overrides.staticReaders = 'report';
|
|
55
|
+
if (o['staticReaders'] === true)
|
|
56
|
+
overrides.staticReaders = 'on';
|
|
57
|
+
return overrides;
|
|
58
|
+
}
|
|
59
|
+
function overridesFrom(o, root) {
|
|
60
|
+
const overrides = { ...staticOverridesFrom(o) };
|
|
49
61
|
if (typeof o['since'] === 'string')
|
|
50
62
|
overrides.since = o['since'];
|
|
51
63
|
if (typeof o['frozenOracle'] === 'string')
|
|
@@ -60,10 +72,6 @@ function overridesFrom(o, root) {
|
|
|
60
72
|
overrides.strykerCompat = true;
|
|
61
73
|
if (o['ignoreStatic'] === true)
|
|
62
74
|
overrides.ignoreStatic = true;
|
|
63
|
-
if (typeof o['staticGroup'] === 'number')
|
|
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']);
|
|
67
75
|
if (o['incremental'] === true)
|
|
68
76
|
overrides.incremental = true;
|
|
69
77
|
if (typeof o['flakyReruns'] === 'number')
|
|
@@ -96,6 +104,8 @@ const runCommand = defineCommand({
|
|
|
96
104
|
ignoreStatic: { type: 'boolean', description: 'skip static mutants (module-level code): faster, and the score leaves them out' },
|
|
97
105
|
staticGroup: { type: 'number', description: 'arm this many static mutants together in a file that is not their first (default 1; a heuristic, audited)' },
|
|
98
106
|
staticBudget: { type: 'number', description: 'the most the static phase may take, ms; static mutants still undecided then are left out of the score' },
|
|
107
|
+
staticReaders: { type: 'boolean', description: 'run each static mutant only in the test files that read its value (a heuristic, audited)' },
|
|
108
|
+
reportStaticReaders: { type: 'boolean', description: 'one more dry run that reports how many tests static mutants would need if each ran only where its value is read; changes no verdict' },
|
|
99
109
|
incremental: { type: 'boolean', description: 'reuse verdicts whose source and covering tests did not change' },
|
|
100
110
|
flakyReruns: { type: 'number', description: 're-runs for verdicts with nondeterministic tests (default 2; 0 disables)' },
|
|
101
111
|
reporter: { type: 'string', description: 'comma-separated: clear-text, json, sarif, html (default clear-text,json)' },
|
|
@@ -129,6 +139,10 @@ const runCommand = defineCommand({
|
|
|
129
139
|
stderr.write(`oxmutate: the change weakened its tests: ${String(weakened.length)} mutant(s) the tests at ${result.frozen?.base ?? ''} detect now survive (${weakened.slice(0, WEAKENED_SHOWN).map((w) => `${w.file}:${String(w.line)} ${w.mutatorName}`).join('; ')}). fix: restore the assertions the change removed, or replace them with ones that kill these mutants\n`);
|
|
130
140
|
data['exitCode'] = ExitCode.RUNTIME;
|
|
131
141
|
}
|
|
142
|
+
if ((result.stats.staticReaders?.auditDisagreed ?? 0) > 0) {
|
|
143
|
+
stderr.write(`oxmutate: ${String(result.stats.staticReaders?.auditDisagreed ?? 0)} static mutant(s) that survived the files reading their value were killed in a file that loads it; they are reported Killed. fix: run without --static-readers; a test reads the value through a path the tracking does not see\n`);
|
|
144
|
+
data['exitCode'] = ExitCode.RUNTIME;
|
|
145
|
+
}
|
|
132
146
|
if (result.stats.staticAuditDisagreed > 0) {
|
|
133
147
|
stderr.write(`oxmutate: ${String(result.stats.staticAuditDisagreed)} mutant(s) reported as survivors by a group were killed when run alone; they are reported Killed. fix: run without static.group (--static-group 1); two of this file's mutants hide each other when armed together\n`);
|
|
134
148
|
data['exitCode'] = ExitCode.RUNTIME;
|
package/dist/config.d.ts
CHANGED
|
@@ -33,6 +33,15 @@ export interface Options {
|
|
|
33
33
|
* left out of the score. A kill found before that stands. Unset: no limit.
|
|
34
34
|
*/
|
|
35
35
|
staticBudgetMs?: number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* `static.readers: 'report'`: one more dry run, with every module-level literal reporting which
|
|
38
|
+
* tests read each of its properties, and a summary line of how many tests the static phase would
|
|
39
|
+
* run if each static mutant ran only where its value is read (roadmap 2.4). Changes no verdict.
|
|
40
|
+
* `'on'`: the same dry run, and then each static mutant runs only in the files that read its
|
|
41
|
+
* value. A heuristic: one survivor in twenty is run again in every file that loads it, and one
|
|
42
|
+
* that fails there is reported Killed and fails the run.
|
|
43
|
+
*/
|
|
44
|
+
staticReaders: 'off' | 'report' | 'on';
|
|
36
45
|
strykerCompat: boolean;
|
|
37
46
|
reporters: string[];
|
|
38
47
|
jsonReport: string;
|
package/dist/config.js
CHANGED
|
@@ -12,6 +12,9 @@ function staticBudgetOf(ms) {
|
|
|
12
12
|
return asked >= 1 ? asked : undefined;
|
|
13
13
|
}
|
|
14
14
|
export const STATIC_BUDGET_REASON = 'static mutant (static budget spent)';
|
|
15
|
+
function staticReadersOf(readers) {
|
|
16
|
+
return readers === 'report' || readers === 'on' ? readers : 'off';
|
|
17
|
+
}
|
|
15
18
|
export const IGNORE_STATIC_REASON = 'static mutant (ignoreStatic)';
|
|
16
19
|
const NOT_NEEDED = {
|
|
17
20
|
inPlace: 'oxmutate never copies or rewrites the project; instrumented sources are served from memory',
|
|
@@ -111,6 +114,7 @@ export async function loadOptions(root, overrides = {}, configFile) {
|
|
|
111
114
|
ignoreStatic: raw['ignoreStatic'] === true,
|
|
112
115
|
staticGroup: staticGroupOf(raw['static']?.group),
|
|
113
116
|
staticBudgetMs: staticBudgetOf(raw['static']?.budgetMs),
|
|
117
|
+
staticReaders: staticReadersOf(raw['static']?.readers),
|
|
114
118
|
strykerCompat: raw['strykerCompat'] === true,
|
|
115
119
|
reporters: raw['reporters'] ?? ['clear-text', 'json'],
|
|
116
120
|
jsonReport: path.resolve(root, json.fileName ?? 'reports/mutation/mutation.json'),
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -76,6 +76,20 @@ export interface RunStats {
|
|
|
76
76
|
pollutionRestarts: number;
|
|
77
77
|
/** 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. */
|
|
78
78
|
diskWrites: number;
|
|
79
|
+
/**
|
|
80
|
+
* `static.readers: 'report'` (roadmap 2.4): over the static mutants, the tests of every file that
|
|
81
|
+
* loads each one's module (what the static phase runs), and the tests of only the files that read
|
|
82
|
+
* its value. A mutant whose value is untracked, or read while a module loaded, counts its loaders.
|
|
83
|
+
*/
|
|
84
|
+
staticReaders?: {
|
|
85
|
+
mutants: number;
|
|
86
|
+
loaderTests: number;
|
|
87
|
+
readerTests: number;
|
|
88
|
+
fellBack: number;
|
|
89
|
+
dryRunMs: number;
|
|
90
|
+
auditSampled?: number;
|
|
91
|
+
auditDisagreed?: number;
|
|
92
|
+
} | undefined;
|
|
79
93
|
/** The first few paths those writes touched, so a suite's own scratch in a tracked folder can be told from a mutant's write. */
|
|
80
94
|
diskWritePaths: string[];
|
|
81
95
|
/** Survivors of the warm run that a whole-file re-run killed (cross-test effects). */
|
package/dist/coordinator.js
CHANGED
|
@@ -49,6 +49,8 @@ const verdictOptions = (o) => ({
|
|
|
49
49
|
vitestConfig: o.vitestConfig && existsSync(o.vitestConfig) ? sha(readFileSync(o.vitestConfig, 'utf8')) : null,
|
|
50
50
|
});
|
|
51
51
|
const UNIT_PAIRS = 400;
|
|
52
|
+
const READERS_AUDIT_EVERY = 20;
|
|
53
|
+
const auditedReader = (id) => env['OXMUTATE_STATIC_AUDIT'] === 'all' || [...id].reduce((sum, c) => sum + (c.codePointAt(0) ?? 0), 0) % READERS_AUDIT_EVERY === 0;
|
|
52
54
|
const detected = (s) => s === 'Killed' || s === 'Timeout';
|
|
53
55
|
const AUDIT_SHARE = 0.05;
|
|
54
56
|
const DEADLOCK_REASON = 'its test deadlocked: nothing left in the worker could resume it (a hang, called in under a second)';
|
|
@@ -121,6 +123,7 @@ export async function run(options) {
|
|
|
121
123
|
throw new InstrumentFault(`cannot parse ${String(parseErrors.length)} file(s): ${parseErrors.slice(0, 3).join('; ')}`);
|
|
122
124
|
const changed = options.since ? await changedLines(root, options.since) : undefined;
|
|
123
125
|
const mutants = [];
|
|
126
|
+
const trackedIds = new Set();
|
|
124
127
|
const codeMap = {};
|
|
125
128
|
const directiveErrors = [];
|
|
126
129
|
for (const f of instrumented) {
|
|
@@ -139,8 +142,11 @@ export async function run(options) {
|
|
|
139
142
|
const off = offs.find((d) => d.line === m.start.line && (d.mutators === 'all' || d.mutators.includes(m.mutatorName)));
|
|
140
143
|
const inRange = ranged.reasonFor(m.start.line, m.mutatorName);
|
|
141
144
|
const excluded = options.excludedMutations.includes(m.mutatorName);
|
|
145
|
+
if (m.tracked)
|
|
146
|
+
trackedIds.add(m.id);
|
|
147
|
+
const { tracked: _tracked, ...plain } = m;
|
|
142
148
|
mutants.push({
|
|
143
|
-
...
|
|
149
|
+
...plain,
|
|
144
150
|
file: f.path,
|
|
145
151
|
coveredBy: [],
|
|
146
152
|
status: off || inRange !== undefined || excluded || m.ignoredReason ? 'Ignored' : 'NoCoverage',
|
|
@@ -184,8 +190,8 @@ export async function run(options) {
|
|
|
184
190
|
env[SPEC_TRACES_ENV] = JSON.stringify(traceDirs);
|
|
185
191
|
const dryPool = createPool({ root, vitestConfig: options.vitestConfig, workers: options.concurrency, instrumentedMap: mapFile, nodeArgs: options.testRunnerNodeArgs }, 1, work, 'dry');
|
|
186
192
|
pools.push(dryPool);
|
|
187
|
-
const dryRun = async () => {
|
|
188
|
-
const unit = { mode: 'dry', files: [], results: fileFor('dry', 'jsonl'), progress: fileFor('dry', 'progress') };
|
|
193
|
+
const dryRun = async (trackReads = false) => {
|
|
194
|
+
const unit = { mode: 'dry', files: [], results: fileFor('dry', 'jsonl'), progress: fileFor('dry', 'progress'), ...(trackReads && { trackReads: true }) };
|
|
189
195
|
let failure;
|
|
190
196
|
let overBudget = false;
|
|
191
197
|
await dryPool.drain([unit], () => options.dryRunTimeoutMinutes * MS_PER_MINUTE, (_, o) => {
|
|
@@ -202,10 +208,13 @@ export async function run(options) {
|
|
|
202
208
|
throw new InstrumentFault(`the dry run did not complete: ${failure.stderr.slice(-1500)}`);
|
|
203
209
|
const perTest = {};
|
|
204
210
|
const stat = {};
|
|
211
|
+
const reads = { static: {}, perTest: {} };
|
|
205
212
|
for (const r of recs) {
|
|
206
213
|
Object.assign(perTest, r.coverage?.perTest ?? {});
|
|
207
214
|
for (const [k, v] of Object.entries(r.coverage?.static ?? {}))
|
|
208
215
|
stat[k] = (stat[k] ?? 0) + v;
|
|
216
|
+
Object.assign(reads.perTest, r.reads?.perTest ?? {});
|
|
217
|
+
Object.assign(reads.static, r.reads?.static ?? {});
|
|
209
218
|
}
|
|
210
219
|
const fileErrors = recs.flatMap((r) => r.fileErrors ?? []);
|
|
211
220
|
if (fileErrors.length) {
|
|
@@ -214,12 +223,17 @@ export async function run(options) {
|
|
|
214
223
|
const unhandled = recs.reduce((n, r) => n + (r.unhandled ?? 0), 0);
|
|
215
224
|
if (unhandled > 0)
|
|
216
225
|
throw new InstrumentFault(`the suite raised ${String(unhandled)} unhandled error(s) with no mutant active, so no verdict can be trusted. fix: make \`vitest run\` pass with no "Unhandled Errors" first`);
|
|
217
|
-
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []), modules: Object.assign({}, ...recs.map((r) => r.modules ?? {})) };
|
|
226
|
+
return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []), modules: Object.assign({}, ...recs.map((r) => r.modules ?? {})), reads };
|
|
218
227
|
};
|
|
219
228
|
progress.phase('dry run');
|
|
220
229
|
const d0 = performance.now();
|
|
221
230
|
const dry = await dryRun();
|
|
222
231
|
stats.dryRunMs = performance.now() - d0;
|
|
232
|
+
const r0 = performance.now();
|
|
233
|
+
if (options.staticReaders !== 'off' && trackedIds.size > 0)
|
|
234
|
+
progress.phase('dry run with reads tracked (static.readers)');
|
|
235
|
+
const tracking = options.staticReaders !== 'off' && trackedIds.size > 0 ? await dryRun(true) : undefined;
|
|
236
|
+
const readersDryRunMs = performance.now() - r0;
|
|
223
237
|
const failing = dry.tests.filter((t) => t.state === 'fail');
|
|
224
238
|
if (failing.length)
|
|
225
239
|
throw new InstrumentFault(`${String(failing.length)} test(s) fail with no mutant active, so no verdict can be trusted: ${failing.slice(0, 5).map((t) => t.name).join('; ')}. fix: make the suite pass first`);
|
|
@@ -576,6 +590,37 @@ export async function run(options) {
|
|
|
576
590
|
const covering = byCost([...new Set(tests)]);
|
|
577
591
|
return [...covering, ...byAffinity(m.file, [...loaders].filter((f) => !covering.includes(f)))];
|
|
578
592
|
};
|
|
593
|
+
const readerFiles = new Map();
|
|
594
|
+
const trackingTests = tracking?.tests ?? [];
|
|
595
|
+
const readsTracked = tracking !== undefined && !trackingTests.some((t) => t.state === 'fail');
|
|
596
|
+
const trackedReads = tracking?.reads ?? { static: {}, perTest: {} };
|
|
597
|
+
for (const [key, ids] of Object.entries(trackedReads.perTest)) {
|
|
598
|
+
const file = key.startsWith(FILE_KEY) ? key.slice(FILE_KEY.length) : testInfo.get(key)?.file;
|
|
599
|
+
if (!file)
|
|
600
|
+
continue;
|
|
601
|
+
for (const id of Object.keys(ids))
|
|
602
|
+
(readerFiles.get(id) ?? readerFiles.set(id, new Set()).get(id))?.add(file);
|
|
603
|
+
}
|
|
604
|
+
const readersFor = (m) => readsTracked && trackedIds.has(m.id) && !(m.id in trackedReads.static) ? filesFor(m).filter((f) => readerFiles.get(m.id)?.has(f)) : undefined;
|
|
605
|
+
const staticFilesFor = (m) => (options.staticReaders === 'on' ? readersFor(m) ?? filesFor(m) : filesFor(m));
|
|
606
|
+
const readersOf = (items) => {
|
|
607
|
+
const testsIn = new Map();
|
|
608
|
+
for (const t of dry.tests)
|
|
609
|
+
testsIn.set(t.file, (testsIn.get(t.file) ?? 0) + 1);
|
|
610
|
+
const sum = (files) => [...files].reduce((n, f) => n + (testsIn.get(f) ?? 0), 0);
|
|
611
|
+
let loaderTests = 0;
|
|
612
|
+
let readerTests = 0;
|
|
613
|
+
let fellBack = 0;
|
|
614
|
+
for (const m of items) {
|
|
615
|
+
const loaders = filesFor(m);
|
|
616
|
+
const readers = readersFor(m);
|
|
617
|
+
loaderTests += sum(loaders);
|
|
618
|
+
if (!readers)
|
|
619
|
+
fellBack++;
|
|
620
|
+
readerTests += sum(readers ?? loaders);
|
|
621
|
+
}
|
|
622
|
+
return { mutants: items.length, loaderTests, readerTests, fellBack };
|
|
623
|
+
};
|
|
579
624
|
const testsRun = (id) => {
|
|
580
625
|
const m = byId.get(id);
|
|
581
626
|
const fs = m ? filesFor(m) : allTestFiles;
|
|
@@ -590,7 +635,7 @@ export async function run(options) {
|
|
|
590
635
|
return Math.min(MAX_TEST_MS * testsRun(id), 1500 + options.timeoutMS + options.timeoutFactor * ms * 2);
|
|
591
636
|
};
|
|
592
637
|
const isolatedCapped = (id, slack) => Math.min(slack * isolatedBudget(id), MAX_TEST_MS * testsRun(id));
|
|
593
|
-
const decideStaticLive = async (items) => {
|
|
638
|
+
const decideStaticLive = async (items, filesOfMutant = staticFilesFor) => {
|
|
594
639
|
if (items.length === 0)
|
|
595
640
|
return [];
|
|
596
641
|
const STATIC_BATCH = 25;
|
|
@@ -661,7 +706,7 @@ export async function run(options) {
|
|
|
661
706
|
return unit.staticIds.length > 0;
|
|
662
707
|
};
|
|
663
708
|
const open = (m) => !killed.has(m.id) && !back.has(m.id);
|
|
664
|
-
const rounds = [(m) =>
|
|
709
|
+
const rounds = [(m) => filesOfMutant(m).slice(0, 1), (m) => (open(m) ? filesOfMutant(m).slice(1) : [])];
|
|
665
710
|
for (const [i, round] of rounds.entries())
|
|
666
711
|
await staticPool.drain(unitsFor(round, i > 0), (_, id) => isolatedCapped(id, 2), afterStatic, keepUndecided);
|
|
667
712
|
const undecided = [];
|
|
@@ -672,7 +717,7 @@ export async function run(options) {
|
|
|
672
717
|
m.killedBy = [...new Set(k.killedBy)];
|
|
673
718
|
m.testsCompleted = ran.get(m.id) ?? 0;
|
|
674
719
|
}
|
|
675
|
-
else if (!back.has(m.id) &&
|
|
720
|
+
else if (!back.has(m.id) && filesOfMutant(m).every((f) => passed.get(m.id)?.has(f))) {
|
|
676
721
|
m.status = 'Survived';
|
|
677
722
|
m.testsCompleted = ran.get(m.id) ?? 0;
|
|
678
723
|
}
|
|
@@ -727,8 +772,21 @@ export async function run(options) {
|
|
|
727
772
|
verdicts.set(m.id, { status: 'RuntimeError' });
|
|
728
773
|
return verdicts;
|
|
729
774
|
};
|
|
775
|
+
if (tracking)
|
|
776
|
+
stats.staticReaders = { ...readersOf(staticMutants), dryRunMs: readersDryRunMs };
|
|
730
777
|
const s0 = performance.now();
|
|
731
778
|
const staticBack = env['OXMUTATE_ISOLATE_STATIC'] ? staticMutants : await decideStaticLive(staticMutants);
|
|
779
|
+
if (options.staticReaders === 'on' && stats.staticReaders) {
|
|
780
|
+
const sampled = staticMutants.filter((m) => m.status === 'Survived' && staticFilesFor(m).length < filesFor(m).length && auditedReader(m.id));
|
|
781
|
+
for (const m of sampled)
|
|
782
|
+
m.status = 'NoCoverage';
|
|
783
|
+
await decideStaticLive(sampled, filesFor);
|
|
784
|
+
stats.staticReaders.auditSampled = sampled.length;
|
|
785
|
+
stats.staticReaders.auditDisagreed = sampled.filter((m) => m.status === 'Killed' || m.status === 'Timeout').length;
|
|
786
|
+
for (const m of sampled)
|
|
787
|
+
if (m.status === 'NoCoverage')
|
|
788
|
+
m.status = 'Survived';
|
|
789
|
+
}
|
|
732
790
|
stats.staticMs = performance.now() - s0;
|
|
733
791
|
stats.isolatedByCause.static = staticBack.length;
|
|
734
792
|
isolatedQueue.push(...staticBack);
|
package/dist/native.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface NativeMutant {
|
|
|
10
10
|
end: Location;
|
|
11
11
|
/** Set when an ignore rule covers the mutant: reported as Ignored, never placed in the code. */
|
|
12
12
|
ignoredReason?: string;
|
|
13
|
+
/** Reads of the property holding it are reported by a tracking dry run (`oxmTrack`, roadmap 2.4). */
|
|
14
|
+
tracked?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export interface InstrumentedFile {
|
|
15
17
|
path: string;
|
package/dist/reports.js
CHANGED
|
@@ -140,6 +140,13 @@ function whereItWent(r) {
|
|
|
140
140
|
if (s.warmUnitMs > 0)
|
|
141
141
|
lines.push(` warm servers ran ${(s.warmUnitMs / MS_PER_S).toFixed(1)} s, ${(s.warmArmedMs / MS_PER_S).toFixed(1)} s of it inside test runs`);
|
|
142
142
|
lines.push(...staticLine(r));
|
|
143
|
+
const sr = s.staticReaders;
|
|
144
|
+
if (sr && sr.mutants > 0) {
|
|
145
|
+
const ratio = sr.readerTests > 0 ? `, ${(sr.loaderTests / sr.readerTests).toFixed(1)}x fewer` : '';
|
|
146
|
+
const inUse = sr.auditSampled !== undefined;
|
|
147
|
+
const how = inUse ? `in use, ${String(sr.auditSampled)} survivor(s) audited in every loader, ${String(sr.auditDisagreed ?? 0)} disagreed` : 'report only';
|
|
148
|
+
lines.push(` static readers: ${String(sr.mutants)} static mutants ${inUse ? 'ran' : 'would run'} ${String(sr.readerTests)} tests where their value is read, against ${String(sr.loaderTests)} in every file that loads them${ratio}; ${String(sr.fellBack)} kept every loader (${how}, ${(sr.dryRunMs / MS_PER_S).toFixed(1)} s)`);
|
|
149
|
+
}
|
|
143
150
|
const c = s.isolatedByCause;
|
|
144
151
|
const causes = [`static ${String(c.static)}`, `stopped ${String(c.stopped)}`, `unfinished ${String(c.unfinished)}`, `re-run ${String(c.rerun)}`, `survivors confirmed ${String(c.confirm)}`, `flaky ${String(c.flaky)}`];
|
|
145
152
|
if (s.isolatedRuns > 0)
|
package/dist/runner.js
CHANGED
|
@@ -655,7 +655,7 @@ export default class OxmutateRunner extends Base {
|
|
|
655
655
|
if (result?.state === 'fail' && !f.tasks?.length)
|
|
656
656
|
fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
|
|
657
657
|
}
|
|
658
|
-
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules), unhandled });
|
|
658
|
+
emit({ type: 'dry', tests, coverage: ns.mutantCoverage, reads: ns.readCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules), unhandled });
|
|
659
659
|
}
|
|
660
660
|
else if (mode === 'isolated') {
|
|
661
661
|
const failed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxmutate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.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
|
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|