@systemfsoftware/stryker-js-mutation-run 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/LICENSE +212 -0
- package/README.md +38 -0
- package/dist/checker-C4tGiXtZ.mjs +588 -0
- package/dist/checker-resource-DTYEKXdg.d.mts +108 -0
- package/dist/checker-worker.d.mts +16 -0
- package/dist/checker-worker.mjs +2 -0
- package/dist/child-process-proxy-worker-lx1M5Mz3.mjs +567 -0
- package/dist/child-process-proxy-worker-main.d.mts +1 -0
- package/dist/child-process-proxy-worker-main.mjs +6 -0
- package/dist/child-process-test-runner-worker-BTWRYb8V.mjs +50 -0
- package/dist/child-process-test-runner-worker.d.mts +16 -0
- package/dist/child-process-test-runner-worker.mjs +2 -0
- package/dist/config/base.d.mts +21 -0
- package/dist/config/base.mjs +47 -0
- package/dist/config/config-resolution.d.mts +80 -0
- package/dist/config/config-resolution.mjs +2 -0
- package/dist/config/fork-schema.d.mts +5 -0
- package/dist/config/fork-schema.mjs +2 -0
- package/dist/errors-BkjxkY_S.d.mts +6 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +10 -0
- package/dist/exit-classification.d.mts +28 -0
- package/dist/exit-classification.mjs +39 -0
- package/dist/fork-schema-BHuTbRBR.mjs +62 -0
- package/dist/incremental-differ-DDVwtXBn.d.mts +72 -0
- package/dist/incremental-differ-DY8AA09Q.mjs +579 -0
- package/dist/index-CyOSz6LC.d.mts +104 -0
- package/dist/index.d.mts +87 -0
- package/dist/index.mjs +1885 -0
- package/dist/mutants/incremental-differ.d.mts +2 -0
- package/dist/mutants/incremental-differ.mjs +2 -0
- package/dist/output-mode-CfGHDpi5.d.mts +17 -0
- package/dist/output-mode.d.mts +2 -0
- package/dist/output-mode.mjs +1 -0
- package/dist/plugins-Nx86IJQW.mjs +1618 -0
- package/dist/plugins.d.mts +2 -0
- package/dist/plugins.mjs +2 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/run-event-CzRz9Gtk.d.mts +121 -0
- package/dist/run-event.d.mts +2 -0
- package/dist/run-event.mjs +1 -0
- package/dist/stryker-package-CTcsIZ4S.mjs +23 -0
- package/dist/stryker-package.d.mts +7 -0
- package/dist/stryker-package.mjs +2 -0
- package/dist/timer-BMrE1SQ3.d.mts +20 -0
- package/dist/timer.d.mts +2 -0
- package/dist/timer.mjs +48 -0
- package/dist/verdict-envelope-BWc61mHm.mjs +229 -0
- package/dist/verdict-envelope-CKLZPjxC.d.mts +94 -0
- package/dist/verdict-envelope.d.mts +2 -0
- package/dist/verdict-envelope.mjs +2 -0
- package/package.json +102 -0
- package/schema/stryker-schema.json +903 -0
package/dist/plugins.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { n as OutputMode, t as ModeSignal } from "./output-mode-CfGHDpi5.mjs";
|
|
2
|
+
import { i as VerdictEnvelope } from "./verdict-envelope-CKLZPjxC.mjs";
|
|
3
|
+
import { MutantStatus, schema } from "@systemfsoftware/stryker-js-plugin-api/core";
|
|
4
|
+
//#region src/run-event.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a run emits, as a closed vocabulary (R1, R2, R35).
|
|
7
|
+
*
|
|
8
|
+
* Declaration only. Core pushes these into a sink it receives through
|
|
9
|
+
* `injectionTokens.runEventSink`; how they reach a descriptor — NDJSON framing,
|
|
10
|
+
* ordering, the terminal-once guarantee, the drain — belongs to whoever
|
|
11
|
+
* binds the sink, not here. The `kind` tags are the wire contract and are
|
|
12
|
+
* fixed: a consumer parses on them, so they may be added to but never
|
|
13
|
+
* renamed.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The lifecycle phases of a run (R18), in the order the executor chain emits
|
|
17
|
+
* them. `prepare` is the first observable moment of the run.
|
|
18
|
+
*/
|
|
19
|
+
type RunPhase = 'prepare' | 'instrument' | 'dry-run' | 'mutation-test';
|
|
20
|
+
/**
|
|
21
|
+
* Opens the run (R5, R21): carries the stream schema version, the run id
|
|
22
|
+
* shared with the verdict envelope, and the resolved mode plus the signal
|
|
23
|
+
* that decided it.
|
|
24
|
+
*/
|
|
25
|
+
interface RunStarted {
|
|
26
|
+
readonly kind: 'stream';
|
|
27
|
+
readonly schemaVersion: string;
|
|
28
|
+
readonly runId: string;
|
|
29
|
+
readonly mode: OutputMode;
|
|
30
|
+
readonly signal: ModeSignal;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A lifecycle phase boundary (R18). `elapsedMs` is time since the run began.
|
|
34
|
+
*/
|
|
35
|
+
interface PhaseEntered {
|
|
36
|
+
readonly kind: 'phase';
|
|
37
|
+
readonly phase: RunPhase;
|
|
38
|
+
readonly elapsedMs: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The mutant-testing plan (R17): emitted once the plan is known, before any
|
|
42
|
+
* mutant is tested. Names the total a consumer can count `mutant` events
|
|
43
|
+
* against.
|
|
44
|
+
*/
|
|
45
|
+
interface PlanKnown {
|
|
46
|
+
readonly kind: 'plan';
|
|
47
|
+
readonly total: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One tested mutant that passed the R20 actionable filter. Carries the full
|
|
51
|
+
* survivor re-run matching key — file, location, mutator, replacement — so a
|
|
52
|
+
* consumer can address the mutant without opening the report file (R11).
|
|
53
|
+
*/
|
|
54
|
+
interface MutantTested {
|
|
55
|
+
readonly kind: 'mutant';
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly status: MutantStatus;
|
|
58
|
+
readonly file: string;
|
|
59
|
+
readonly location: schema.Location;
|
|
60
|
+
readonly mutator: string;
|
|
61
|
+
readonly replacement: string | null;
|
|
62
|
+
readonly completed: number;
|
|
63
|
+
readonly total: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The heartbeat (R19), emitted on the tick interval while a phase is in
|
|
67
|
+
* flight, so a long phase never looks hung. `total` is `null` until the plan
|
|
68
|
+
* is known — a pre-plan `0` would read as a finished run of zero mutants.
|
|
69
|
+
*/
|
|
70
|
+
interface Heartbeat {
|
|
71
|
+
readonly kind: 'tick';
|
|
72
|
+
readonly elapsedMs: number;
|
|
73
|
+
readonly completed: number;
|
|
74
|
+
readonly total: number | null;
|
|
75
|
+
}
|
|
76
|
+
type VerdictReached = VerdictEnvelope & {
|
|
77
|
+
readonly kind: 'verdict';
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The run did not reach a verdict (R7). `code` is the classed exit code (R6).
|
|
81
|
+
* Terminal.
|
|
82
|
+
*/
|
|
83
|
+
interface RunFailed {
|
|
84
|
+
readonly kind: 'error';
|
|
85
|
+
readonly schemaVersion: string;
|
|
86
|
+
readonly code: number;
|
|
87
|
+
readonly error: string;
|
|
88
|
+
readonly remediation: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A `--help` invocation closed with the rendered usage text, so an agent that
|
|
92
|
+
* reads the last stdout line still gets a tagged event. Terminal.
|
|
93
|
+
*/
|
|
94
|
+
interface HelpRendered {
|
|
95
|
+
readonly kind: 'help';
|
|
96
|
+
readonly schemaVersion: string;
|
|
97
|
+
readonly code: 0;
|
|
98
|
+
readonly help: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A `--llms` invocation closed with the rendered manifest (R9). Terminal.
|
|
102
|
+
*/
|
|
103
|
+
interface ManifestRendered {
|
|
104
|
+
readonly kind: 'manifest';
|
|
105
|
+
readonly schemaVersion: string;
|
|
106
|
+
readonly code: 0;
|
|
107
|
+
readonly manifest: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Exactly one of these ends a run (R5): a score, a failure, or a rendered
|
|
111
|
+
* document closing a non-run command.
|
|
112
|
+
*/
|
|
113
|
+
type RunTerminalEvent = VerdictReached | RunFailed | HelpRendered | ManifestRendered;
|
|
114
|
+
type RunEvent = RunStarted | PhaseEntered | PlanKnown | MutantTested | Heartbeat | RunTerminalEvent;
|
|
115
|
+
/**
|
|
116
|
+
* Where a run's events go. Synchronous by contract: core pushes from promise
|
|
117
|
+
* code that cannot suspend, and the callback must never reject.
|
|
118
|
+
*/
|
|
119
|
+
type RunEventSink = (event: RunEvent) => void;
|
|
120
|
+
//#endregion
|
|
121
|
+
export { PhaseEntered as a, RunEventSink as c, RunStarted as d, RunTerminalEvent as f, MutantTested as i, RunFailed as l, HelpRendered as n, PlanKnown as o, VerdictReached as p, ManifestRendered as r, RunEvent as s, Heartbeat as t, RunPhase as u };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as PhaseEntered, c as RunEventSink, d as RunStarted, f as RunTerminalEvent, i as MutantTested, l as RunFailed, n as HelpRendered, o as PlanKnown, p as VerdictReached, r as ManifestRendered, s as RunEvent, t as Heartbeat, u as RunPhase } from "./run-event-CzRz9Gtk.mjs";
|
|
2
|
+
export { Heartbeat, HelpRendered, ManifestRendered, MutantTested, PhaseEntered, PlanKnown, RunEvent, RunEventSink, RunFailed, RunPhase, RunStarted, RunTerminalEvent, VerdictReached };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deepFreeze } from "@stryker-mutator/util";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { URL, fileURLToPath } from "url";
|
|
4
|
+
import * as S from "effect/Schema";
|
|
5
|
+
//#region src/stryker-package.schema.ts
|
|
6
|
+
/**
|
|
7
|
+
* The shape of this package's own `package.json` that the public surface
|
|
8
|
+
* exposes (`strykerVersion`, `strykerEngines`). The file is data read from
|
|
9
|
+
* disk, so it is decoded through this schema rather than trusted after
|
|
10
|
+
* `JSON.parse`; every other `package.json` key is ignored.
|
|
11
|
+
*/
|
|
12
|
+
const PackageJsonSchema = S.Struct({
|
|
13
|
+
version: S.String,
|
|
14
|
+
engines: S.Struct({ node: S.String })
|
|
15
|
+
});
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/stryker-package.ts
|
|
18
|
+
const rawPackageJson = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"));
|
|
19
|
+
const pkg = deepFreeze(S.decodeUnknownSync(PackageJsonSchema)(rawPackageJson));
|
|
20
|
+
const strykerVersion = pkg.version;
|
|
21
|
+
const strykerEngines = pkg.engines;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { strykerVersion as n, strykerEngines as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/timer.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Published as the `./timer` subpath: the mutation-report package's
|
|
4
|
+
* progress reporters construct a `Timer` from here (KTD7).
|
|
5
|
+
*/
|
|
6
|
+
declare class Timer {
|
|
7
|
+
private readonly now;
|
|
8
|
+
private readonly start;
|
|
9
|
+
private readonly markers;
|
|
10
|
+
constructor(now?: () => Date);
|
|
11
|
+
humanReadableElapsed(sinceMarker?: string): string;
|
|
12
|
+
elapsedSeconds(sinceMarker?: string): number;
|
|
13
|
+
elapsedMs(sinceMarker?: string): number;
|
|
14
|
+
mark(name: string): void;
|
|
15
|
+
private static humanReadableElapsedSeconds;
|
|
16
|
+
private static humanReadableElapsedMinutes;
|
|
17
|
+
private static formatTime;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { Timer as t };
|
package/dist/timer.d.mts
ADDED
package/dist/timer.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/timer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Published as the `./timer` subpath: the mutation-report package's
|
|
4
|
+
* progress reporters construct a `Timer` from here (KTD7).
|
|
5
|
+
*/
|
|
6
|
+
var Timer = class Timer {
|
|
7
|
+
now;
|
|
8
|
+
start;
|
|
9
|
+
markers = /* @__PURE__ */ new Map();
|
|
10
|
+
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
11
|
+
this.now = now;
|
|
12
|
+
this.start = this.now();
|
|
13
|
+
}
|
|
14
|
+
humanReadableElapsed(sinceMarker) {
|
|
15
|
+
const elapsedSeconds = this.elapsedSeconds(sinceMarker);
|
|
16
|
+
return new Intl.ListFormat("en").format([Timer.humanReadableElapsedMinutes(elapsedSeconds), Timer.humanReadableElapsedSeconds(elapsedSeconds)].filter(Boolean));
|
|
17
|
+
}
|
|
18
|
+
elapsedSeconds(sinceMarker) {
|
|
19
|
+
const elapsedMs = this.elapsedMs(sinceMarker);
|
|
20
|
+
return Math.floor(elapsedMs / 1e3);
|
|
21
|
+
}
|
|
22
|
+
elapsedMs(sinceMarker) {
|
|
23
|
+
const marker = sinceMarker && this.markers.get(sinceMarker);
|
|
24
|
+
if (marker) return this.now().getTime() - marker.getTime();
|
|
25
|
+
else return this.now().getTime() - this.start.getTime();
|
|
26
|
+
}
|
|
27
|
+
mark(name) {
|
|
28
|
+
this.markers.set(name, this.now());
|
|
29
|
+
}
|
|
30
|
+
static humanReadableElapsedSeconds(elapsedSeconds) {
|
|
31
|
+
const restSeconds = elapsedSeconds % 60;
|
|
32
|
+
return this.formatTime("second", restSeconds);
|
|
33
|
+
}
|
|
34
|
+
static humanReadableElapsedMinutes(elapsedSeconds) {
|
|
35
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
36
|
+
if (elapsedMinutes === 0) return "";
|
|
37
|
+
else return this.formatTime("minute", elapsedMinutes);
|
|
38
|
+
}
|
|
39
|
+
static formatTime(word, elapsed) {
|
|
40
|
+
return elapsed.toLocaleString("en", {
|
|
41
|
+
unit: word,
|
|
42
|
+
style: "unit",
|
|
43
|
+
unitDisplay: "long"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { Timer };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import "@systemfsoftware/stryker-js-plugin-api/core";
|
|
2
|
+
import { normalizeFileName } from "@stryker-mutator/util";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { calculateMutationTestMetrics } from "mutation-testing-metrics";
|
|
5
|
+
import { randomFillSync } from "node:crypto";
|
|
6
|
+
//#region src/test-contribution.ts
|
|
7
|
+
const KILLING_STATUSES = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
8
|
+
const testFileById = (testFiles) => {
|
|
9
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const [fileName, testFile] of Object.entries(testFiles)) for (const test of testFile.tests) byId.set(test.id, fileName);
|
|
11
|
+
return byId;
|
|
12
|
+
};
|
|
13
|
+
const killersOf = (killedBy, fileById) => new Set(killedBy.map((testId) => fileById.get(testId) ?? testId));
|
|
14
|
+
const contributionByTestFile = (report) => {
|
|
15
|
+
const testFiles = report.testFiles ?? {};
|
|
16
|
+
const fileById = testFileById(testFiles);
|
|
17
|
+
const soleKills = /* @__PURE__ */ new Map();
|
|
18
|
+
const totalKills = /* @__PURE__ */ new Map();
|
|
19
|
+
const unattributed = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const file of Object.values(report.files)) for (const mutant of file.mutants) {
|
|
21
|
+
if (!KILLING_STATUSES.has(mutant.status)) continue;
|
|
22
|
+
const killers = killersOf(mutant.killedBy ?? [], fileById);
|
|
23
|
+
if (killers.size === 0) {
|
|
24
|
+
const covered = mutant.coveredBy;
|
|
25
|
+
if (covered !== void 0) for (const fileName of killersOf(covered, fileById)) unattributed.add(fileName);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const soleKill = killers.size === 1;
|
|
29
|
+
for (const fileName of killers) {
|
|
30
|
+
totalKills.set(fileName, (totalKills.get(fileName) ?? 0) + 1);
|
|
31
|
+
if (soleKill) soleKills.set(fileName, (soleKills.get(fileName) ?? 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const byTestFile = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const fileName of Object.keys(testFiles)) byTestFile.set(fileName, {
|
|
36
|
+
soleKills: soleKills.get(fileName) ?? 0,
|
|
37
|
+
totalKills: totalKills.get(fileName) ?? 0,
|
|
38
|
+
coversUnattributedKill: unattributed.has(fileName)
|
|
39
|
+
});
|
|
40
|
+
return byTestFile;
|
|
41
|
+
};
|
|
42
|
+
const toothlessTestFiles = (contribution, { suffixes, everyKillerRecorded }) => {
|
|
43
|
+
const toothless = [];
|
|
44
|
+
for (const [fileName, { soleKills, totalKills, coversUnattributedKill }] of contribution) {
|
|
45
|
+
const defends = everyKillerRecorded ? soleKills > 0 : totalKills > 0;
|
|
46
|
+
const inScope = suffixes.some((suffix) => fileName.endsWith(suffix));
|
|
47
|
+
if (!defends && inScope && !coversUnattributedKill) toothless.push(fileName);
|
|
48
|
+
}
|
|
49
|
+
return toothless.sort();
|
|
50
|
+
};
|
|
51
|
+
const suffixesToRequire = (value) => {
|
|
52
|
+
if (!Array.isArray(value)) return void 0;
|
|
53
|
+
const suffixes = value.filter((entry) => typeof entry === "string");
|
|
54
|
+
return suffixes.length === 0 ? void 0 : suffixes;
|
|
55
|
+
};
|
|
56
|
+
const PRECISION = "every killing test was recorded";
|
|
57
|
+
const judgeTestContribution = (report, requireTestContribution, everyKillerRecorded) => {
|
|
58
|
+
const suffixes = suffixesToRequire(requireTestContribution);
|
|
59
|
+
if (suffixes === void 0) return void 0;
|
|
60
|
+
const matches = suffixes.join(", ");
|
|
61
|
+
const contribution = contributionByTestFile(report);
|
|
62
|
+
const inScope = [...contribution.keys()].filter((fileName) => suffixes.some((suffix) => fileName.endsWith(suffix)));
|
|
63
|
+
if (inScope.length === 0) return {
|
|
64
|
+
failed: false,
|
|
65
|
+
message: `No test file matching ${matches} ran, so none was judged.`
|
|
66
|
+
};
|
|
67
|
+
if (!everyKillerRecorded) return {
|
|
68
|
+
failed: true,
|
|
69
|
+
message: `This run used Stryker's bail mode, which stops each mutant at its first killing test. A test file's contribution therefore cannot be measured on this evidence. Set \`disableBail: true\` to record every killing test, or remove \`${matches}\` from \`requireTestContribution\` (set it to \`null\` to disable the check) to narrow the gate out of scope for this run.`
|
|
70
|
+
};
|
|
71
|
+
if (![...contribution.values()].some(({ totalKills }) => totalKills > 0)) return {
|
|
72
|
+
failed: true,
|
|
73
|
+
message: `This run credited no kill to any test file, so no test file's contribution to it can be measured. Until that is fixed the ${inScope.length} file(s) matching ${matches} are unjudged, not cleared.`
|
|
74
|
+
};
|
|
75
|
+
const toothless = toothlessTestFiles(contribution, {
|
|
76
|
+
suffixes,
|
|
77
|
+
everyKillerRecorded
|
|
78
|
+
});
|
|
79
|
+
if (toothless.length === 0) return {
|
|
80
|
+
failed: false,
|
|
81
|
+
message: `Every test file matching ${matches} kills a mutant nothing else kills (${PRECISION}).`
|
|
82
|
+
};
|
|
83
|
+
const listed = toothless.map((fileName) => ` - ${fileName}`).join("\n");
|
|
84
|
+
return {
|
|
85
|
+
failed: true,
|
|
86
|
+
message: `Deleting these ${toothless.length} test file(s) would leave every mutant just as dead (${PRECISION}):\n${listed}`
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/verdict-envelope.ts
|
|
91
|
+
/**
|
|
92
|
+
* U4 — the verdict envelope (R5, R11, R20): the single JSON document machine
|
|
93
|
+
* mode prints to stdout at the end of a run. Everything an agent needs to
|
|
94
|
+
* act without opening the report file, including the survivor re-run
|
|
95
|
+
* matching key per actionable mutant (R20 bounds the list). All functions
|
|
96
|
+
* here are pure over the report — no I/O side effects, no randomness except
|
|
97
|
+
* inside `generateRunId`.
|
|
98
|
+
*/
|
|
99
|
+
const VERDICT_ENVELOPE_SCHEMA_VERSION = "1.0";
|
|
100
|
+
/**
|
|
101
|
+
* The statuses a `verdict.mutants` entry (and a `mutant` stream line, U7) is
|
|
102
|
+
* recorded for (R20). `Killed`, `Ignored`, and `CompileError` are reported
|
|
103
|
+
* as counts only: the full per-mutant record stays in the report file, and
|
|
104
|
+
* enumerating killed mutants served no consumer while pushing the terminal
|
|
105
|
+
* line past the 64 KB limit of `bufio.Scanner`-class readers. Measured:
|
|
106
|
+
* `oxlint-plugins/effect-workflow` produced a 2164-entry, ~440 KB line with
|
|
107
|
+
* zero actionable entries. This is the single definition of the R20 filter,
|
|
108
|
+
* shared with the progress stream.
|
|
109
|
+
*/
|
|
110
|
+
const ACTIONABLE_STATUSES = [
|
|
111
|
+
"Survived",
|
|
112
|
+
"NoCoverage",
|
|
113
|
+
"Timeout",
|
|
114
|
+
"RuntimeError"
|
|
115
|
+
];
|
|
116
|
+
/**
|
|
117
|
+
* Whether `status` is actionable (R20) — one of `ACTIONABLE_STATUSES`.
|
|
118
|
+
*/
|
|
119
|
+
function isActionableStatus(status) {
|
|
120
|
+
return ACTIONABLE_STATUSES.some((actionable) => actionable === status);
|
|
121
|
+
}
|
|
122
|
+
const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
123
|
+
/**
|
|
124
|
+
* A ULID-shaped run identifier: 48 bits of millisecond time followed by 80
|
|
125
|
+
* random bits, Crockford base32-encoded into exactly 26 characters. The time
|
|
126
|
+
* prefix keeps ids roughly sortable; the randomness makes collisions
|
|
127
|
+
* negligible.
|
|
128
|
+
*/
|
|
129
|
+
function generateRunId() {
|
|
130
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
bytes[0] = now / 1099511627776 % 256;
|
|
133
|
+
bytes[1] = now / 4294967296 % 256;
|
|
134
|
+
bytes[2] = now / 16777216 % 256;
|
|
135
|
+
bytes[3] = now / 65536 % 256;
|
|
136
|
+
bytes[4] = now / 256 % 256;
|
|
137
|
+
bytes[5] = now % 256;
|
|
138
|
+
randomFillSync(bytes.subarray(6));
|
|
139
|
+
let chars = "";
|
|
140
|
+
let value = 0;
|
|
141
|
+
let bits = 0;
|
|
142
|
+
for (const byte of bytes) {
|
|
143
|
+
value = value << 8 | byte;
|
|
144
|
+
bits += 8;
|
|
145
|
+
while (bits >= 5) {
|
|
146
|
+
chars += CROCKFORD_BASE32[value >>> bits - 5 & 31];
|
|
147
|
+
bits -= 5;
|
|
148
|
+
value &= (1 << bits) - 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (bits > 0) chars += CROCKFORD_BASE32[value << 5 - bits & 31];
|
|
152
|
+
return chars;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The resolved options the report helper embeds as `report.config` (it writes
|
|
156
|
+
* `config: this.options`). Read through `in`-narrowing because the report
|
|
157
|
+
* schema types `config` as `{}`, while the embedded value is our own resolved
|
|
158
|
+
* options with their index signature.
|
|
159
|
+
*/
|
|
160
|
+
function embeddedConfig(report) {
|
|
161
|
+
const config = report.config;
|
|
162
|
+
let jsonReporterFileName;
|
|
163
|
+
let requireTestContribution;
|
|
164
|
+
let disableBail = false;
|
|
165
|
+
if (typeof config === "object" && config !== null) {
|
|
166
|
+
if ("jsonReporter" in config && typeof config.jsonReporter === "object" && config.jsonReporter !== null && "fileName" in config.jsonReporter && typeof config.jsonReporter.fileName === "string") jsonReporterFileName = config.jsonReporter.fileName;
|
|
167
|
+
if ("requireTestContribution" in config) requireTestContribution = config.requireTestContribution;
|
|
168
|
+
if ("disableBail" in config && typeof config.disableBail === "boolean") disableBail = config.disableBail;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
jsonReporterFileName,
|
|
172
|
+
requireTestContribution,
|
|
173
|
+
disableBail
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function breakThreshold(thresholds) {
|
|
177
|
+
if ("break" in thresholds) {
|
|
178
|
+
const breakValue = thresholds.break;
|
|
179
|
+
if (typeof breakValue === "number") return breakValue;
|
|
180
|
+
if (breakValue === null) return null;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function buildVerdictEnvelope(report, mode, signal, runId) {
|
|
185
|
+
const { jsonReporterFileName, requireTestContribution, disableBail } = embeddedConfig(report);
|
|
186
|
+
const metrics = calculateMutationTestMetrics(report).systemUnderTestMetrics.metrics;
|
|
187
|
+
const hasMutants = metrics.totalMutants > 0;
|
|
188
|
+
const score = hasMutants && Number.isFinite(metrics.mutationScore) ? metrics.mutationScore : null;
|
|
189
|
+
const reportFile = hasMutants && jsonReporterFileName !== void 0 ? normalizeFileName(path.relative(process.cwd(), jsonReporterFileName)) : null;
|
|
190
|
+
const mutants = [];
|
|
191
|
+
for (const [file, fileResult] of Object.entries(report.files)) for (const mutant of fileResult.mutants) {
|
|
192
|
+
if (!isActionableStatus(mutant.status)) continue;
|
|
193
|
+
mutants.push({
|
|
194
|
+
id: mutant.id,
|
|
195
|
+
file,
|
|
196
|
+
location: mutant.location,
|
|
197
|
+
mutator: mutant.mutatorName,
|
|
198
|
+
replacement: mutant.replacement ?? null,
|
|
199
|
+
status: mutant.status
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
schemaVersion: "1.0",
|
|
204
|
+
runId,
|
|
205
|
+
mode,
|
|
206
|
+
signal,
|
|
207
|
+
score,
|
|
208
|
+
thresholds: {
|
|
209
|
+
high: report.thresholds.high,
|
|
210
|
+
low: report.thresholds.low,
|
|
211
|
+
break: breakThreshold(report.thresholds)
|
|
212
|
+
},
|
|
213
|
+
counts: {
|
|
214
|
+
killed: metrics.killed,
|
|
215
|
+
timeout: metrics.timeout,
|
|
216
|
+
survived: metrics.survived,
|
|
217
|
+
noCoverage: metrics.noCoverage,
|
|
218
|
+
runtimeErrors: metrics.runtimeErrors,
|
|
219
|
+
compileErrors: metrics.compileErrors,
|
|
220
|
+
ignored: metrics.ignored,
|
|
221
|
+
pending: metrics.pending
|
|
222
|
+
},
|
|
223
|
+
testContribution: judgeTestContribution(report, requireTestContribution, disableBail) ?? null,
|
|
224
|
+
reportFile,
|
|
225
|
+
mutants
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { isActionableStatus as a, generateRunId as i, VERDICT_ENVELOPE_SCHEMA_VERSION as n, judgeTestContribution as o, buildVerdictEnvelope as r, ACTIONABLE_STATUSES as t };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { n as OutputMode, t as ModeSignal } from "./output-mode-CfGHDpi5.mjs";
|
|
2
|
+
import { MutantStatus, schema } from "@systemfsoftware/stryker-js-plugin-api/core";
|
|
3
|
+
//#region src/test-contribution.d.ts
|
|
4
|
+
interface TestContributionVerdict {
|
|
5
|
+
readonly failed: boolean;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/verdict-envelope.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* U4 — the verdict envelope (R5, R11, R20): the single JSON document machine
|
|
12
|
+
* mode prints to stdout at the end of a run. Everything an agent needs to
|
|
13
|
+
* act without opening the report file, including the survivor re-run
|
|
14
|
+
* matching key per actionable mutant (R20 bounds the list). All functions
|
|
15
|
+
* here are pure over the report — no I/O side effects, no randomness except
|
|
16
|
+
* inside `generateRunId`.
|
|
17
|
+
*/
|
|
18
|
+
declare const VERDICT_ENVELOPE_SCHEMA_VERSION = "1.0";
|
|
19
|
+
/**
|
|
20
|
+
* The statuses a `verdict.mutants` entry (and a `mutant` stream line, U7) is
|
|
21
|
+
* recorded for (R20). `Killed`, `Ignored`, and `CompileError` are reported
|
|
22
|
+
* as counts only: the full per-mutant record stays in the report file, and
|
|
23
|
+
* enumerating killed mutants served no consumer while pushing the terminal
|
|
24
|
+
* line past the 64 KB limit of `bufio.Scanner`-class readers. Measured:
|
|
25
|
+
* `oxlint-plugins/effect-workflow` produced a 2164-entry, ~440 KB line with
|
|
26
|
+
* zero actionable entries. This is the single definition of the R20 filter,
|
|
27
|
+
* shared with the progress stream.
|
|
28
|
+
*/
|
|
29
|
+
declare const ACTIONABLE_STATUSES: readonly ['Survived', 'NoCoverage', 'Timeout', 'RuntimeError'];
|
|
30
|
+
/**
|
|
31
|
+
* Whether `status` is actionable (R20) — one of `ACTIONABLE_STATUSES`.
|
|
32
|
+
*/
|
|
33
|
+
declare function isActionableStatus(status: MutantStatus): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* One mutant as the envelope reports it. `file` is the report's relative file
|
|
36
|
+
* key; `location`/`mutator`/`replacement` are exactly the survivor re-run
|
|
37
|
+
* matching key (R10/R11).
|
|
38
|
+
*/
|
|
39
|
+
interface VerdictMutant {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly file: string;
|
|
42
|
+
readonly location: schema.Location;
|
|
43
|
+
readonly mutator: string;
|
|
44
|
+
readonly replacement: string | null;
|
|
45
|
+
readonly status: MutantStatus;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The configured thresholds. `break` rides along even though the report
|
|
49
|
+
* schema does not declare it — it is the threshold the exit code depends on.
|
|
50
|
+
*/
|
|
51
|
+
interface VerdictThresholds {
|
|
52
|
+
readonly high: number;
|
|
53
|
+
readonly low: number;
|
|
54
|
+
readonly break: number | null;
|
|
55
|
+
}
|
|
56
|
+
interface VerdictCounts {
|
|
57
|
+
readonly killed: number;
|
|
58
|
+
readonly timeout: number;
|
|
59
|
+
readonly survived: number;
|
|
60
|
+
readonly noCoverage: number;
|
|
61
|
+
readonly runtimeErrors: number;
|
|
62
|
+
readonly compileErrors: number;
|
|
63
|
+
readonly ignored: number;
|
|
64
|
+
readonly pending: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The full verdict document. `score` and `reportFile` are `null` for a run
|
|
68
|
+
* with zero mutants (AE3): there is no score to report and no report file was
|
|
69
|
+
* written. `testContribution` is `null` when the check is not configured.
|
|
70
|
+
* `mutants` is bounded to `ACTIONABLE_STATUSES` (R20) — see that definition
|
|
71
|
+
* for why the remaining statuses are counts only.
|
|
72
|
+
*/
|
|
73
|
+
interface VerdictEnvelope {
|
|
74
|
+
readonly schemaVersion: string;
|
|
75
|
+
readonly runId: string;
|
|
76
|
+
readonly mode: OutputMode;
|
|
77
|
+
readonly signal: ModeSignal;
|
|
78
|
+
readonly score: number | null;
|
|
79
|
+
readonly thresholds: VerdictThresholds;
|
|
80
|
+
readonly counts: VerdictCounts;
|
|
81
|
+
readonly testContribution: TestContributionVerdict | null;
|
|
82
|
+
readonly reportFile: string | null;
|
|
83
|
+
readonly mutants: readonly VerdictMutant[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A ULID-shaped run identifier: 48 bits of millisecond time followed by 80
|
|
87
|
+
* random bits, Crockford base32-encoded into exactly 26 characters. The time
|
|
88
|
+
* prefix keeps ids roughly sortable; the randomness makes collisions
|
|
89
|
+
* negligible.
|
|
90
|
+
*/
|
|
91
|
+
declare function generateRunId(): string;
|
|
92
|
+
declare function buildVerdictEnvelope(report: schema.MutationTestResult, mode: OutputMode, signal: ModeSignal, runId: string): VerdictEnvelope;
|
|
93
|
+
//#endregion
|
|
94
|
+
export { VerdictMutant as a, generateRunId as c, VerdictEnvelope as i, isActionableStatus as l, VERDICT_ENVELOPE_SCHEMA_VERSION as n, VerdictThresholds as o, VerdictCounts as r, buildVerdictEnvelope as s, ACTIONABLE_STATUSES as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as VerdictMutant, c as generateRunId, i as VerdictEnvelope, l as isActionableStatus, n as VERDICT_ENVELOPE_SCHEMA_VERSION, o as VerdictThresholds, r as VerdictCounts, s as buildVerdictEnvelope, t as ACTIONABLE_STATUSES } from "./verdict-envelope-CKLZPjxC.mjs";
|
|
2
|
+
export { ACTIONABLE_STATUSES, VERDICT_ENVELOPE_SCHEMA_VERSION, VerdictCounts, VerdictEnvelope, VerdictMutant, VerdictThresholds, buildVerdictEnvelope, generateRunId, isActionableStatus };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as isActionableStatus, i as generateRunId, n as VERDICT_ENVELOPE_SCHEMA_VERSION, r as buildVerdictEnvelope, t as ACTIONABLE_STATUSES } from "./verdict-envelope-BWc61mHm.mjs";
|
|
2
|
+
export { ACTIONABLE_STATUSES, VERDICT_ENVELOPE_SCHEMA_VERSION, buildVerdictEnvelope, generateRunId, isActionableStatus };
|