@systemfsoftware/stryker-js-mutation-run 2.0.0 → 4.0.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +1 -1
  3. package/dist/{checker-C4tGiXtZ.mjs → checker-C0Vh51o8.mjs} +3 -3
  4. package/dist/{checker-resource-DTYEKXdg.d.mts → checker-resource-LZCpsbpn.d.mts} +1 -1
  5. package/dist/checker-worker.d.mts +2 -2
  6. package/dist/checker-worker.mjs +1 -1
  7. package/dist/{child-process-proxy-worker-lx1M5Mz3.mjs → child-process-proxy-worker-CmAwSjYi.mjs} +2 -2
  8. package/dist/child-process-proxy-worker-main.mjs +1 -1
  9. package/dist/{child-process-test-runner-worker-BTWRYb8V.mjs → child-process-test-runner-worker-DPXWNPqZ.mjs} +2 -2
  10. package/dist/child-process-test-runner-worker.d.mts +1 -1
  11. package/dist/child-process-test-runner-worker.mjs +1 -1
  12. package/dist/config/base.mjs +2 -1
  13. package/dist/config/config-resolution.d.mts +2 -2
  14. package/dist/config/config-resolution.mjs +1 -1
  15. package/dist/config/fork-schema.mjs +1 -1
  16. package/dist/{errors-BkjxkY_S.d.mts → errors-B8Vx4QIQ.d.mts} +1 -1
  17. package/dist/errors.d.mts +1 -1
  18. package/dist/errors.mjs +1 -1
  19. package/dist/{fork-schema-BHuTbRBR.mjs → fork-schema-K8Ab5szC.mjs} +1 -14
  20. package/dist/{incremental-differ-DY8AA09Q.mjs → incremental-differ-DAz2XlBK.mjs} +2 -2
  21. package/dist/{incremental-differ-DDVwtXBn.d.mts → incremental-differ-DtBnr6fJ.d.mts} +1 -1
  22. package/dist/{index-CyOSz6LC.d.mts → index-C19nE7Ww.d.mts} +23 -4
  23. package/dist/index.d.mts +6 -6
  24. package/dist/index.mjs +21 -34
  25. package/dist/mutants/incremental-differ.d.mts +1 -1
  26. package/dist/mutants/incremental-differ.mjs +1 -1
  27. package/dist/{plugins-Nx86IJQW.mjs → plugins-Bdy2g7fR.mjs} +69 -24
  28. package/dist/plugins.d.mts +1 -1
  29. package/dist/plugins.mjs +1 -1
  30. package/dist/{run-event-CzRz9Gtk.d.mts → run-event-Dx_Q5jrc.d.mts} +1 -1
  31. package/dist/run-event.d.mts +1 -1
  32. package/dist/{stryker-package-CTcsIZ4S.mjs → stryker-package-JIsmRMux.mjs} +1 -1
  33. package/dist/stryker-package.d.mts +1 -1
  34. package/dist/stryker-package.mjs +1 -1
  35. package/dist/{verdict-envelope-CKLZPjxC.d.mts → verdict-envelope-BJz-h7K-.d.mts} +2 -10
  36. package/dist/verdict-envelope.d.mts +1 -1
  37. package/dist/verdict-envelope.mjs +135 -1
  38. package/package.json +13 -14
  39. package/schema/stryker-schema.json +6 -20
  40. package/dist/verdict-envelope-BWc61mHm.mjs +0 -229
@@ -1,229 +0,0 @@
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 };