@mark1russell7/rigor-mutation 0.0.0-stage → 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/rigor-mutation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mutation tests with [Stryker](https://stryker-mutator.io) and Vitest for each package of a repository. A mutation test changes the source a little (a mutant), and starts the tests. A good test fails on the mutant, so it detects the mutant. The score is the percent of the mutants that the tests detect.
4
+
5
+ This package gives three things:
6
+
7
+ - The Stryker config of each package, or of each part of a big package, from one lane file.
8
+ - The score of each package, with its own gate. A score under the gate fails, and the message names the package.
9
+ - A check of the disabled mutants: each `Stryker disable` comment is also in a list, with its reason.
10
+
11
+ ```sh
12
+ npm install --save-dev @mark1russell7/rigor-mutation @stryker-mutator/core @stryker-mutator/vitest-runner
13
+ ```
14
+
15
+ Use Stryker 10.1.0 or later with Vitest 5. Stryker 10.0 started no test on each mutant with Vitest 5 ([stryker-js#6210](https://github.com/stryker-mutator/stryker-js/issues/6210)). The score then fails, because the tests detect no mutant.
16
+
17
+ ## The lane file
18
+
19
+ The lane file `mutation.json` is at the root of the repository:
20
+
21
+ ```json
22
+ {
23
+ "packages": {
24
+ "collections": {
25
+ "dir": "packages/collections",
26
+ "mutate": ["src/**/*.ts", "!src/**/*.test.ts", "!src/index.ts"],
27
+ "gate": 80,
28
+ "parts": { "impl": ["src/impl/**/*.ts"], "core": ["src/core/**/*.ts"] }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ - `dir` is the folder of the package. Stryker starts in this folder.
35
+ - `mutate` has the globs of the files to mutate, from `dir`. A glob with `!` removes the files of the globs before it.
36
+ - `gate` is the lowest score in percent that passes. Without a gate, the lane only reports the score.
37
+ - `parts` divides a big package. A part has its own globs, and the `!` globs of `mutate` also apply to it. Each file of `mutate` must be in exactly one part.
38
+ - `vitestConfig` names the Vitest config of the package, from `dir`. Without it, Vitest finds its config.
39
+ - `timeoutMS` is the time in milliseconds that the clock adds to the time of the tests for each mutant. The default is 120000. The default of Stryker (5000) is too short on a loaded CI runner.
40
+ - `dryRunTimeoutMinutes` is the time in minutes for the run of the tests without mutants. The default is 15.
41
+ - `stryker` has more options for the Stryker config of each selection of the package, for example `{ "ignoreStatic": true }`. Its keys replace the keys of the config, but `--concurrency` replaces its `concurrency`. The lane owns `mutate`, `thresholds`, `jsonReporter`, `htmlReporter`, `timeoutMS` and `dryRunTimeoutMinutes`, so `stryker` cannot have them.
42
+ - `disables` (at the root of the lane) names the list of the disabled mutants. The default is `mutation-disables.json`.
43
+
44
+ A selection is one Stryker run: a package (`collections`) or a part (`collections.impl`). `rigor-mutation list --runs` gives the selections of a full run: the parts of each package with parts, else the package.
45
+
46
+ A full run mutates only the files of the parts. Thus a file in no part has no score, and a file in two parts has two scores. `list --runs`, and `config` and `scores` of a part, examine the source files of the package. They fail and name each file that is in no part, in two parts, or in a part but not in `mutate`. In the example, a file `src/util.ts` is in no part, so the lane fails.
47
+
48
+ ## A run
49
+
50
+ ```sh
51
+ pnpm exec rigor-mutation config collections --out reports/stryker.json
52
+ cd "$(pnpm exec rigor-mutation dir collections)"
53
+ pnpm exec stryker run ../../reports/stryker.json
54
+ cd -
55
+ pnpm exec rigor-mutation scores collections
56
+ ```
57
+
58
+ Each run starts the tests of `dir` only. A test in another folder, for example a test of the repository root, does not detect a mutant of the package.
59
+
60
+ `rigor-mutation scores` reads `<dir>/reports/mutation/mutation.json` and applies the gate. It exits with 1 when a selection fails. With `--summary "$GITHUB_STEP_SUMMARY"`, it adds a table of the scores to the summary of the job. The shared workflow `reusable-mutation.yml` of the template starts these steps on one runner for each selection.
61
+
62
+ A run of mutation tests is long and it uses much memory. Start it in CI, and on a local computer start one part with `--concurrency 2`.
63
+
64
+ Two options make a run shorter:
65
+
66
+ - `config --dry-run` gives a config that starts only the tests without mutants. Stryker does not stop at the first failed test. Use it to see if the tests pass under Stryker before a long run.
67
+ - `--changed <file>` reads a file with one changed path on each line, from the folder of the lane file. Make the file with `git diff --name-only`. Then `list --runs` gives only the runs with a changed file, and `config` mutates only the changed files of the selection. `config` fails for a selection with no changed file.
68
+
69
+ ## The score
70
+
71
+ The score counts the detected mutants, out of the mutants that a test covers or that survive. Compile errors, runtime errors and ignored mutants do not count.
72
+
73
+ A detected mutant has the status `Killed`, or a `Timeout` of the hit limit. Stryker gives the status `Timeout` for two events:
74
+
75
+ - The hit limit stopped the mutant. The mutant has an endless loop, and its `statusReason` starts with `Hit limit`. The mutant is detected.
76
+ - The clock stopped the mutant. The load of the runner can cause this, so the mutant is not detected for certain. It counts as not detected.
77
+
78
+ The score line gives the count of each kind of time-out. It also gives the score with the clock time-outs counted as detected, as Stryker counts them. The gate does not use that second score.
79
+
80
+ A selection fails in these conditions:
81
+
82
+ - The report has no mutants that count. Stryker found no files, or it did not start.
83
+ - The report has mutants that count, but the tests detect none. The test runner started no test on the mutants.
84
+ - The selection has a gate, and the score is under it.
85
+
86
+ The gate of a package with parts applies to each part. Each part must have a score at the gate or above it. A part under the gate fails, also when the score of all the parts together is above the gate.
87
+
88
+ ## The disabled mutants
89
+
90
+ A comment `// Stryker disable next-line <Mutator>: <reason>` disables a mutant that is equivalent. The disable raises the score, so each one is also in the list, with its reason. `rigor-mutation disables` compares the comments of the sources of each package with the list. It fails when they are not the same, or when a comment has no reason. After you add or remove a disable, start `rigor-mutation disables --write` and examine the diff of the list.
91
+
92
+ ## The API
93
+
94
+ ```ts
95
+ import { parseLane, select, strykerConfig, parseReport, scoreReport, formatScore } from "@mark1russell7/rigor-mutation";
96
+
97
+ const lane = parseLane(JSON.parse(laneText));
98
+ const selection = select(lane, "collections.impl");
99
+ const config = strykerConfig(selection, { concurrency: 2 });
100
+ const score = scoreReport(parseReport(JSON.parse(reportText)), selection);
101
+ console.log(formatScore(score)); // collections.impl: 91.20 % (456 of 500, 44 survived), gate 80 %: pass
102
+ ```
103
+
104
+ The package also exports `selectionNames`, `runSelections`, `summaryMarkdown`, `findDisables`, `compareDisables` and `runCli`.
package/build/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/build/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { n as runCli } from "./run-CveACYFE.js";
3
+ // src/cli.ts
4
+ try {
5
+ process.exitCode = runCli(process.argv.slice(2), {
6
+ cwd: process.cwd(),
7
+ stdout: (text) => process.stdout.write(text),
8
+ stderr: (text) => process.stderr.write(text)
9
+ });
10
+ } catch (error) {
11
+ process.stderr.write(`rigor-mutation: ${error instanceof Error ? error.message : String(error)}\n`);
12
+ process.exitCode = 1;
13
+ }
14
+ export {};
15
+
16
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// The bin `rigor-mutation`. The work is in run.ts, so a test can start the same CLI in a fixture folder.\nimport { runCli } from \"./run.ts\";\n\ntry {\n process.exitCode = runCli(process.argv.slice(2), {\n cwd: process.cwd(),\n stdout: (text) => process.stdout.write(text),\n stderr: (text) => process.stderr.write(text),\n });\n} catch (error) {\n process.stderr.write(`rigor-mutation: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;AAIA,IAAI;CACF,QAAQ,WAAW,OAAO,QAAQ,KAAK,MAAM,CAAC,GAAG;EAC/C,KAAK,QAAQ,IAAI;EACjB,SAAS,SAAS,QAAQ,OAAO,MAAM,IAAI;EAC3C,SAAS,SAAS,QAAQ,OAAO,MAAM,IAAI;CAC7C,CAAC;AACH,SAAS,OAAO;CACd,QAAQ,OAAO,MAAM,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClG,QAAQ,WAAW;AACrB"}
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The lane file of a repository (`mutation.json`) names the packages that the mutation tests mutate. A package has
3
+ * its folder, its globs and an optional gate. A big package has parts, so that each part has its own runner.
4
+ */
5
+ /** The mutation tests of one package. */
6
+ interface PackageLane {
7
+ /** This field gives the folder of the package, from the folder of the lane file. Stryker starts in this folder. */
8
+ readonly dir: string;
9
+ /** These globs give the files to mutate, from `dir`. A glob with `!` removes the files of the globs before it. */
10
+ readonly mutate: readonly string[];
11
+ /**
12
+ * Each part has its own globs. The `!` globs of `mutate` also apply to each part. Each file of `mutate` must be in
13
+ * exactly one part, so that a full run mutates each file one time. `partErrors` examines this.
14
+ */
15
+ readonly parts?: Readonly<Record<string, readonly string[]>>;
16
+ /** This field gives the lowest mutation score in percent that passes. Without a gate, the lane only reports the score. */
17
+ readonly gate?: number;
18
+ /** This field gives the Vitest config file, from `dir`. Without it, Vitest finds its config in `dir`. */
19
+ readonly vitestConfig?: string;
20
+ /**
21
+ * This field gives the time in milliseconds that the clock adds to the time of the tests for each mutant. The
22
+ * default is 120000. A short time on a loaded runner stops mutants that the tests detect later.
23
+ */
24
+ readonly timeoutMS?: number;
25
+ /** This field gives the time in minutes for the run of the tests without mutants. The default is 15. */
26
+ readonly dryRunTimeoutMinutes?: number;
27
+ /**
28
+ * These options go into the Stryker config of each selection of the package, for example `ignoreStatic`. The lane
29
+ * owns the keys of `STRYKER_OWNED`, so the field cannot have them.
30
+ */
31
+ readonly stryker?: Readonly<Record<string, unknown>>;
32
+ }
33
+ /** The keys of the Stryker config that the lane owns. The field `stryker` of a package cannot have them. */
34
+ export declare const STRYKER_OWNED: readonly string[];
35
+ /** The lane file: the packages of the mutation tests, and the list of the disabled mutants. */
36
+ interface Lane {
37
+ readonly packages: Readonly<Record<string, PackageLane>>;
38
+ /** This field gives the list of the disabled mutants, from the folder of the lane file. The default is `mutation-disables.json`. */
39
+ readonly disables?: string;
40
+ }
41
+ /** A selection is one Stryker run: a package (`collections`) or a part of a package (`collections.impl`). */
42
+ interface Selection {
43
+ readonly name: string;
44
+ readonly pkg: string;
45
+ readonly dir: string;
46
+ readonly mutate: readonly string[];
47
+ readonly gate: number | undefined;
48
+ readonly vitestConfig: string | undefined;
49
+ readonly timeoutMS: number | undefined;
50
+ readonly dryRunTimeoutMinutes: number | undefined;
51
+ readonly stryker: Readonly<Record<string, unknown>> | undefined;
52
+ }
53
+ /**
54
+ * This function examines the JSON of a lane file and gives the lane.
55
+ *
56
+ * @throws Error with each problem of the file
57
+ */
58
+ export declare function parseLane(json: unknown): Lane;
59
+ /** This function gives the names of all the selections of a lane: each package, then each part of the package. */
60
+ export declare function selectionNames(lane: Lane): string[];
61
+ /** This function gives the selections of a full run: the parts of each package with parts, else the package. Each file is in one run. */
62
+ export declare function runSelections(lane: Lane): string[];
63
+ /**
64
+ * This function gives the selection of a name: a package, or a part of a package.
65
+ *
66
+ * @throws Error when the lane has no package or part of that name
67
+ */
68
+ export declare function select(lane: Lane, name: string): Selection;
69
+ /**
70
+ * This function gives the changed files of a selection, from the folder of its package. The globs of the selection
71
+ * select the files.
72
+ *
73
+ * @param changed - the changed paths, from the folder of the lane file, with `/`
74
+ */
75
+ export declare function changedFiles(selection: Selection, changed: readonly string[]): string[];
76
+ /**
77
+ * This function examines the parts of a package against its files. It gives an error for each file of `mutate` in no
78
+ * part. It also gives an error for each file in two parts, and for each file of a part that is not in `mutate`. A
79
+ * package with no parts has no errors.
80
+ *
81
+ * @param files - the source files of the package, from its folder, with `/`
82
+ */
83
+ export declare function partErrors(lane: Lane, pkg: string, files: readonly string[]): string[];
84
+ /** The options of `strykerConfig`. */
85
+ interface ConfigOptions {
86
+ /** This option gives the number of test processes. The default is 2. */
87
+ readonly concurrency?: number;
88
+ /** This option gives the folder of the reports, from the folder of the package. The default is `reports/mutation`. */
89
+ readonly reportDir?: string;
90
+ /**
91
+ * When this option is true, Stryker starts only the tests without mutants, and it does not stop at the first failed
92
+ * test. Use it to see if the tests pass under Stryker before a long run.
93
+ */
94
+ readonly dryRun?: boolean;
95
+ }
96
+ /** The Stryker config of one selection, as JSON. Stryker starts in the folder of the package. The field `stryker` of the lane adds keys. */
97
+ interface StrykerConfig {
98
+ readonly [key: string]: unknown;
99
+ readonly testRunner: "vitest";
100
+ readonly plugins: readonly string[];
101
+ readonly vitest?: {
102
+ readonly configFile: string;
103
+ };
104
+ readonly mutate: readonly string[];
105
+ readonly coverageAnalysis: "perTest";
106
+ readonly reporters: readonly string[];
107
+ readonly htmlReporter: {
108
+ readonly fileName: string;
109
+ };
110
+ readonly jsonReporter: {
111
+ readonly fileName: string;
112
+ };
113
+ readonly thresholds: {
114
+ readonly high: number;
115
+ readonly low: number;
116
+ readonly break: null;
117
+ };
118
+ readonly concurrency: number;
119
+ readonly timeoutMS: number;
120
+ readonly dryRunTimeoutMinutes: number;
121
+ readonly dryRunOnly?: true;
122
+ readonly disableBail?: true;
123
+ }
124
+ /** The default folder of the reports, from the folder of the package. */
125
+ export declare const REPORT_DIR = "reports/mutation";
126
+ /**
127
+ * The default time in milliseconds that the clock adds to the time of the tests for each mutant. The default of
128
+ * Stryker is 5000. On a loaded CI runner that time stopped mutants that the tests detected later.
129
+ */
130
+ export declare const TIMEOUT_MS = 12e4;
131
+ /** The default time in minutes for the run of the tests without mutants. The default of Stryker is 5. */
132
+ export declare const DRY_RUN_TIMEOUT_MINUTES = 15;
133
+ /**
134
+ * This function gives the Stryker config of a selection. The break of Stryker is off, because the gate of each
135
+ * package is in `scoreReport`. Stryker 10.1.0 or later is necessary with Vitest 5: version 10.0 started no test on each
136
+ * mutant (stryker-js issue 6210).
137
+ */
138
+ export declare function strykerConfig(selection: Selection, options?: ConfigOptions): StrykerConfig;
139
+ /**
140
+ * The score of a Stryker report, with the gate of the package. The score counts the detected mutants, out of the
141
+ * mutants that a test covers or that survive. Compile errors, runtime errors and ignored mutants do not count.
142
+ *
143
+ * Stryker gives the status `Timeout` for two events. The hit limit stops a mutant with an endless loop: its
144
+ * `statusReason` starts with `Hit limit`, and the mutant is detected. The clock stops a mutant that took too long. The
145
+ * load of the runner can cause this. Thus the mutant is not detected for certain, and it counts as not detected.
146
+ */
147
+ /** A mutant of the JSON report of Stryker (the schema of mutation-testing-elements). */
148
+ interface Mutant {
149
+ readonly status: string;
150
+ readonly mutatorName: string;
151
+ /** Stryker gives the reason of some statuses here. A `Timeout` of the hit limit has a reason that starts with `Hit limit`. */
152
+ readonly statusReason?: string | undefined;
153
+ readonly location: {
154
+ readonly start: {
155
+ readonly line: number;
156
+ };
157
+ };
158
+ }
159
+ /** The JSON report of Stryker: the mutants of each mutated file. */
160
+ interface MutationReport {
161
+ readonly files: Readonly<Record<string, {
162
+ readonly mutants: readonly Mutant[];
163
+ }>>;
164
+ }
165
+ /** The score of one selection. */
166
+ interface Score {
167
+ readonly name: string;
168
+ readonly gate: number | undefined;
169
+ /** The mutants with the status `Killed`, and the mutants that the hit limit stopped. */
170
+ readonly detected: number;
171
+ /** The mutants that the hit limit stopped. They are in `detected`. */
172
+ readonly hitLimit: number;
173
+ /** The mutants that the clock stopped. They count as not detected, and they are in `survived`. */
174
+ readonly clockTimeouts: number;
175
+ /** The mutants that count: detected, survived or with no coverage. */
176
+ readonly valid: number;
177
+ /** Each mutant that survived, had no coverage or was stopped by the clock, as `file:line Mutator`. */
178
+ readonly survived: readonly string[];
179
+ /** The score in percent. It is 0 when no mutant counts. */
180
+ readonly percent: number;
181
+ /** The score in percent with the clock time-outs counted as detected, as Stryker counts them. The gate does not read it. */
182
+ readonly percentWithClock: number;
183
+ readonly pass: boolean;
184
+ /** The reason of a failure. */
185
+ readonly reason: string | undefined;
186
+ }
187
+ /** This function tells if the hit limit of Stryker stopped a mutant. Then the mutant has an endless loop, and it is detected. */
188
+ export declare const isHitLimit: (m: Mutant) => boolean;
189
+ /**
190
+ * This function examines the JSON of a Stryker report and gives the report.
191
+ *
192
+ * @throws Error when the JSON has no files with mutants
193
+ */
194
+ export declare function parseReport(json: unknown): MutationReport;
195
+ /**
196
+ * This function gives the score of a report and applies the gate. A report with no mutants that count fails, also
197
+ * without a gate: Stryker found no files. A report with mutants that count and none detected also fails: the test
198
+ * runner started no test on the mutants. Stryker 10.0 with Vitest 5 gave this result.
199
+ */
200
+ export declare function scoreReport(report: MutationReport, selection: {
201
+ readonly name: string;
202
+ readonly gate: number | undefined;
203
+ }): Score;
204
+ /** This function gives one line for a score, with the name of the selection and its result. The time-outs are in it when the report has some. */
205
+ export declare function formatScore(s: Score): string;
206
+ /** This function gives a Markdown table of scores, for the summary of a CI job. */
207
+ export declare function summaryMarkdown(scores: readonly Score[]): string;
208
+ /**
209
+ * A comment `// Stryker disable next-line <Mutator>: <reason>` disables a mutant that is equivalent. The disable
210
+ * raises the score, so each one is also in a list with its reason. These functions find the comments in the
211
+ * sources and compare them with the list.
212
+ */
213
+ /** One disable comment of a source file. */
214
+ interface Disable {
215
+ readonly file: string;
216
+ readonly comment: string;
217
+ }
218
+ /** The result of `compareDisables`. */
219
+ interface DisableCheck {
220
+ /** The comments of the sources that are not in the list, as `file: comment`. */
221
+ readonly added: readonly string[];
222
+ /** The comments of the list that are not in the sources, as `file: comment`. */
223
+ readonly removed: readonly string[];
224
+ /** The comments with no reason after a colon. */
225
+ readonly noReason: readonly Disable[];
226
+ readonly ok: boolean;
227
+ }
228
+ /** This function finds each `Stryker disable` comment of the files. A file name uses `/`. */
229
+ export declare function findDisables(files: readonly {
230
+ readonly file: string;
231
+ readonly text: string;
232
+ }[]): Disable[];
233
+ /** This function compares the comments of the sources with the list. A file with the same comment two times needs it two times in the list. */
234
+ export declare function compareDisables(found: readonly Disable[], listed: readonly Disable[]): DisableCheck;
235
+ /** The input and the output of the CLI. */
236
+ interface CliIo {
237
+ readonly cwd: string;
238
+ readonly stdout: (text: string) => void;
239
+ readonly stderr: (text: string) => void;
240
+ }
241
+ /** The help text of the CLI. */
242
+ export declare const HELP: string;
243
+ /** This function starts the CLI with its arguments and gives the exit code. */
244
+ export declare function runCli(argv: readonly string[], io: CliIo): number;
245
+ export type { CliIo, ConfigOptions, Disable, DisableCheck, Lane, Mutant, MutationReport, PackageLane, Score, Selection, StrykerConfig };
246
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/lane.ts","../src/config.ts","../src/score.ts","../src/disables.ts","../src/run.ts"],"mappings":";;;;;UAQiB;;WAEN;;WAEA;;;;;WAKA,QAAQ,SAAS;;WAEjB;;WAEA;;;;;WAKA;;WAEA;;;;;WAKA,UAAU,SAAS;;;qBAIjB;;UAGI;WACN,UAAU,SAAS,eAAe;;WAElC;;;UAIM;WACN;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,SAAS,SAAS;;;;;;;wBAkDb,UAAU,gBAAgB;;wBAU1B,eAAe,MAAM;;wBAKrB,cAAc,MAAM;;;;;;wBASpB,OAAO,MAAM,MAAM,eAAe;;;;;;;wBAgClC,aAAa,WAAW,WAAW;;;;;;;;wBAcnC,WAAW,MAAM,MAAM,aAAa;;UC7KnC;;WAEN;;WAEA;;;;;WAKA;;;UAIM;;WAEN;WACA;WACA;aAAoB;;WACpB;WACA;WACA;WACA;aAAyB;;WACzB;aAAyB;;WACzB;aAAuB;aAAuB;aAAsB;;WACpE;WACA;WACA;WACA;WACA;;;qBAIE;;;;;qBAMA;;qBAGA;;;;;;wBAOG,cAAc,WAAW,WAAW,UAAS,gBAAqB;;;;;;;;;;UCzCjE;WACN;WACA;;WAEA;WACA;aAAqB;eAAkB;;;;;UAIjC;WACN,OAAO,SAAS;aAA0B,kBAAkB;;;;UAItD;WACN;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;WACA;;WAEA;;;qBAME,aAAc,GAAG;;;;;;wBAOd,YAAY,gBAAgB;;;;;;wBAc5B,YAAY,QAAQ,gBAAgB;WAAsB;WAAuB;IAA6B;;wBA0C9G,YAAY,GAAG;;wBAQf,gBAAgB,iBAAiB;;;;;;;UCjHhC;WACN;WACA;;;UAIM;;WAEN;;WAEA;;WAEA,mBAAmB;WACnB;;;wBAIK,aAAa;WAA2B;WAAuB;MAAmB;;wBA+BlF,gBAAgB,gBAAgB,WAAW,iBAAiB,YAAY;;UC3CvE;WACN;WACA,SAAS;WACT,SAAS;;;qBAIP;;wBA6FG,OAAO,yBAAyB,IAAI"}
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ export { u as DRY_RUN_TIMEOUT_MINUTES, t as HELP, d as REPORT_DIR, m as STRYKER_OWNED, f as TIMEOUT_MS, h as changedFiles, r as compareDisables, i as findDisables, a as formatScore, o as isHitLimit, g as parseLane, s as parseReport, _ as partErrors, n as runCli, v as runSelections, c as scoreReport, y as select, b as selectionNames, p as strykerConfig, l as summaryMarkdown } from "./run-CveACYFE.js";
@@ -0,0 +1,521 @@
1
+ import { dirname, matchesGlob, posix, relative, resolve } from "node:path";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { parseArgs } from "node:util";
4
+ // src/lane.ts
5
+ /** The keys of the Stryker config that the lane owns. The field `stryker` of a package cannot have them. */
6
+ const STRYKER_OWNED = [
7
+ "mutate",
8
+ "thresholds",
9
+ "jsonReporter",
10
+ "htmlReporter",
11
+ "timeoutMS",
12
+ "dryRunTimeoutMinutes"
13
+ ];
14
+ const NAME = /^[\w-]+$/;
15
+ const isRecord = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
16
+ const isGlobs = (x) => Array.isArray(x) && x.length > 0 && x.every((g) => typeof g === "string" && g !== "");
17
+ /** This function examines a package of the lane file. It gives the errors, with the name of the package. */
18
+ function packageErrors(name, p) {
19
+ if (!NAME.test(name)) return [`the package name "${name}" has a character that is not a letter, a digit, "_" or "-"`];
20
+ if (!isRecord(p)) return [`${name}: the package is not an object`];
21
+ const errors = [];
22
+ if (typeof p["dir"] !== "string" || p["dir"] === "") errors.push(`${name}: "dir" is not a folder name`);
23
+ if (!isGlobs(p["mutate"])) errors.push(`${name}: "mutate" is not a list of globs`);
24
+ else if (p["mutate"].every((g) => g.startsWith("!"))) errors.push(`${name}: "mutate" has only "!" globs`);
25
+ const gate = p["gate"];
26
+ if (gate !== void 0 && (typeof gate !== "number" || !(gate >= 0 && gate <= 100))) errors.push(`${name}: "gate" is not a percent from 0 to 100`);
27
+ if (p["vitestConfig"] !== void 0 && typeof p["vitestConfig"] !== "string") errors.push(`${name}: "vitestConfig" is not a file name`);
28
+ for (const field of ["timeoutMS", "dryRunTimeoutMinutes"]) {
29
+ const v = p[field];
30
+ if (v !== void 0 && !(typeof v === "number" && Number.isInteger(v) && v > 0)) errors.push(`${name}: "${field}" is not a positive integer`);
31
+ }
32
+ const stryker = p["stryker"];
33
+ if (stryker !== void 0) {
34
+ if (!isRecord(stryker)) errors.push(`${name}: "stryker" is not an object`);
35
+ else {
36
+ const owned = Object.keys(stryker).filter((k) => STRYKER_OWNED.includes(k));
37
+ if (owned.length > 0) errors.push(`${name}: "stryker" has ${owned.map((k) => `"${k}"`).join(", ")}, but the lane owns these keys`);
38
+ }
39
+ }
40
+ const parts = p["parts"];
41
+ if (parts !== void 0) {
42
+ if (!isRecord(parts) || Object.keys(parts).length === 0) errors.push(`${name}: "parts" is not an object with parts`);
43
+ else for (const [part, globs] of Object.entries(parts)) {
44
+ if (!NAME.test(part)) errors.push(`${name}: the part name "${part}" has a character that is not a letter, a digit, "_" or "-"`);
45
+ if (!isGlobs(globs) || globs.some((g) => g.startsWith("!"))) errors.push(`${name}.${part}: the part is not a list of globs without "!"`);
46
+ }
47
+ }
48
+ return errors;
49
+ }
50
+ /**
51
+ * This function examines the JSON of a lane file and gives the lane.
52
+ *
53
+ * @throws Error with each problem of the file
54
+ */
55
+ function parseLane(json) {
56
+ if (!isRecord(json) || !isRecord(json["packages"])) throw new Error(`the lane has no object "packages"`);
57
+ const errors = Object.entries(json["packages"]).flatMap(([name, p]) => packageErrors(name, p));
58
+ if (Object.keys(json["packages"]).length === 0) errors.push(`the lane has no packages`);
59
+ if (json["disables"] !== void 0 && typeof json["disables"] !== "string") errors.push(`"disables" is not a file name`);
60
+ if (errors.length > 0) throw new Error(`the lane is not valid:\n ${errors.join("\n ")}`);
61
+ return json;
62
+ }
63
+ /** This function gives the names of all the selections of a lane: each package, then each part of the package. */
64
+ function selectionNames(lane) {
65
+ return Object.entries(lane.packages).flatMap(([name, p]) => [name, ...Object.keys(p.parts ?? {}).map((part) => `${name}.${part}`)]);
66
+ }
67
+ /** This function gives the selections of a full run: the parts of each package with parts, else the package. Each file is in one run. */
68
+ function runSelections(lane) {
69
+ return Object.entries(lane.packages).flatMap(([name, p]) => p.parts === void 0 ? [name] : Object.keys(p.parts).map((part) => `${name}.${part}`));
70
+ }
71
+ /**
72
+ * This function gives the selection of a name: a package, or a part of a package.
73
+ *
74
+ * @throws Error when the lane has no package or part of that name
75
+ */
76
+ function select(lane, name) {
77
+ const [pkg = "", part, ...rest] = name.split(".");
78
+ const p = lane.packages[pkg];
79
+ const globs = part === void 0 ? void 0 : p?.parts?.[part];
80
+ if (p === void 0 || rest.length > 0 || part !== void 0 && globs === void 0) throw new Error(`the lane has no package or part "${name}". It has: ${selectionNames(lane).join(", ")}`);
81
+ const mutate = globs === void 0 ? p.mutate : [...globs, ...p.mutate.filter((g) => g.startsWith("!"))];
82
+ return {
83
+ name,
84
+ pkg,
85
+ dir: p.dir,
86
+ mutate,
87
+ gate: p.gate,
88
+ vitestConfig: p.vitestConfig,
89
+ timeoutMS: p.timeoutMS,
90
+ dryRunTimeoutMinutes: p.dryRunTimeoutMinutes,
91
+ stryker: p.stryker
92
+ };
93
+ }
94
+ /**
95
+ * This function tells if globs select a file. It reads the globs as Stryker does. A glob adds its files. A glob with
96
+ * `!` removes its files from the files of the globs before it.
97
+ */
98
+ function globsSelect(globs, file) {
99
+ let selected = false;
100
+ for (const g of globs) if (g.startsWith("!")) {
101
+ if (selected && matchesGlob(file, g.slice(1))) selected = false;
102
+ } else if (!selected && matchesGlob(file, g)) selected = true;
103
+ return selected;
104
+ }
105
+ /**
106
+ * This function gives the changed files of a selection, from the folder of its package. The globs of the selection
107
+ * select the files.
108
+ *
109
+ * @param changed - the changed paths, from the folder of the lane file, with `/`
110
+ */
111
+ function changedFiles(selection, changed) {
112
+ const dir = posix.normalize(selection.dir.replaceAll("\\", "/"));
113
+ return changed.map((f) => posix.relative(dir, posix.normalize(f.replaceAll("\\", "/")))).filter((f) => f !== "" && !f.startsWith("../") && !posix.isAbsolute(f) && globsSelect(selection.mutate, f));
114
+ }
115
+ /**
116
+ * This function examines the parts of a package against its files. It gives an error for each file of `mutate` in no
117
+ * part. It also gives an error for each file in two parts, and for each file of a part that is not in `mutate`. A
118
+ * package with no parts has no errors.
119
+ *
120
+ * @param files - the source files of the package, from its folder, with `/`
121
+ */
122
+ function partErrors(lane, pkg, files) {
123
+ const p = lane.packages[pkg];
124
+ if (p?.parts === void 0) return [];
125
+ const parts = Object.keys(p.parts).map((part) => ({
126
+ part,
127
+ mutate: select(lane, `${pkg}.${part}`).mutate
128
+ }));
129
+ const errors = [];
130
+ for (const file of files) {
131
+ const inMutate = globsSelect(p.mutate, file);
132
+ const inParts = parts.filter((x) => globsSelect(x.mutate, file)).map((x) => x.part);
133
+ if (inMutate && inParts.length === 0) errors.push(`${pkg}: the file ${file} is in no part, so a full run does not mutate it`);
134
+ else if (inParts.length > 1) errors.push(`${pkg}: the file ${file} is in more than one part: ${inParts.join(", ")}`);
135
+ else if (!inMutate && inParts.length === 1) errors.push(`${pkg}: the file ${file} is in the part ${inParts.join("")}, but not in "mutate"`);
136
+ }
137
+ return errors;
138
+ }
139
+ // src/config.ts
140
+ /** The default folder of the reports, from the folder of the package. */
141
+ const REPORT_DIR = "reports/mutation";
142
+ /**
143
+ * The default time in milliseconds that the clock adds to the time of the tests for each mutant. The default of
144
+ * Stryker is 5000. On a loaded CI runner that time stopped mutants that the tests detected later.
145
+ */
146
+ const TIMEOUT_MS = 12e4;
147
+ /** The default time in minutes for the run of the tests without mutants. The default of Stryker is 5. */
148
+ const DRY_RUN_TIMEOUT_MINUTES = 15;
149
+ /**
150
+ * This function gives the Stryker config of a selection. The break of Stryker is off, because the gate of each
151
+ * package is in `scoreReport`. Stryker 10.1.0 or later is necessary with Vitest 5: version 10.0 started no test on each
152
+ * mutant (stryker-js issue 6210).
153
+ */
154
+ function strykerConfig(selection, options = {}) {
155
+ const reportDir = options.reportDir ?? "reports/mutation";
156
+ const low = selection.gate ?? 60;
157
+ return {
158
+ testRunner: "vitest",
159
+ plugins: ["@stryker-mutator/vitest-runner"],
160
+ ...selection.vitestConfig === void 0 ? {} : { vitest: { configFile: selection.vitestConfig } },
161
+ mutate: selection.mutate,
162
+ coverageAnalysis: "perTest",
163
+ reporters: [
164
+ "clear-text",
165
+ "progress",
166
+ "html",
167
+ "json"
168
+ ],
169
+ htmlReporter: { fileName: `${reportDir}/index.html` },
170
+ jsonReporter: { fileName: `${reportDir}/mutation.json` },
171
+ thresholds: {
172
+ high: Math.max(low, 90),
173
+ low,
174
+ break: null
175
+ },
176
+ concurrency: options.concurrency ?? 2,
177
+ timeoutMS: selection.timeoutMS ?? 12e4,
178
+ dryRunTimeoutMinutes: selection.dryRunTimeoutMinutes ?? 15,
179
+ ...selection.stryker,
180
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
181
+ ...options.dryRun === true ? {
182
+ dryRunOnly: true,
183
+ disableBail: true
184
+ } : {}
185
+ };
186
+ }
187
+ // src/score.ts
188
+ const UNDETECTED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
189
+ /** This function tells if the hit limit of Stryker stopped a mutant. Then the mutant has an endless loop, and it is detected. */
190
+ const isHitLimit = (m) => m.status === "Timeout" && (m.statusReason ?? "").startsWith("Hit limit");
191
+ /**
192
+ * This function examines the JSON of a Stryker report and gives the report.
193
+ *
194
+ * @throws Error when the JSON has no files with mutants
195
+ */
196
+ function parseReport(json) {
197
+ const files = typeof json === "object" && json !== null ? json.files : void 0;
198
+ if (typeof files !== "object" || files === null) throw new Error(`the report has no object "files"`);
199
+ for (const [file, data] of Object.entries(files)) if (!Array.isArray(data?.mutants)) throw new Error(`the report has no list "mutants" for ${file}`);
200
+ return json;
201
+ }
202
+ /**
203
+ * This function gives the score of a report and applies the gate. A report with no mutants that count fails, also
204
+ * without a gate: Stryker found no files. A report with mutants that count and none detected also fails: the test
205
+ * runner started no test on the mutants. Stryker 10.0 with Vitest 5 gave this result.
206
+ */
207
+ function scoreReport(report, selection) {
208
+ let detected = 0;
209
+ let valid = 0;
210
+ let hitLimit = 0;
211
+ let clockTimeouts = 0;
212
+ const survived = [];
213
+ for (const [file, data] of Object.entries(report.files)) for (const m of data.mutants) {
214
+ const at = `${file}:${String(m.location.start.line)} ${m.mutatorName}`;
215
+ if (m.status === "Killed" || isHitLimit(m)) {
216
+ detected++;
217
+ valid++;
218
+ if (m.status === "Timeout") hitLimit++;
219
+ } else if (m.status === "Timeout") {
220
+ clockTimeouts++;
221
+ valid++;
222
+ survived.push(`${at} (the clock stopped it)`);
223
+ } else if (UNDETECTED.has(m.status)) {
224
+ valid++;
225
+ survived.push(at);
226
+ }
227
+ }
228
+ const percent = valid === 0 ? 0 : 100 * detected / valid;
229
+ const percentWithClock = valid === 0 ? 0 : 100 * (detected + clockTimeouts) / valid;
230
+ const { name, gate } = selection;
231
+ const reason = valid === 0 ? "the report has no mutants that count: Stryker found no files to mutate, or it did not start" : detected === 0 ? `the tests detected none of the ${String(valid)} mutants: the test runner started no test on the mutants` : gate !== void 0 && percent < gate ? `the score ${percent.toFixed(2)} % is under the gate ${String(gate)} %` : void 0;
232
+ return {
233
+ name,
234
+ gate,
235
+ detected,
236
+ hitLimit,
237
+ clockTimeouts,
238
+ valid,
239
+ survived,
240
+ percent,
241
+ percentWithClock,
242
+ pass: reason === void 0,
243
+ reason
244
+ };
245
+ }
246
+ /** This function gives the time-outs of a score as text: the count of each kind, and the score that counts the clock time-outs as detected. */
247
+ const timeouts = (s) => `time-outs: ${String(s.hitLimit)} hit limit, ${String(s.clockTimeouts)} clock (${s.percentWithClock.toFixed(2)} % with the clock time-outs as detected)`;
248
+ /** This function gives one line for a score, with the name of the selection and its result. The time-outs are in it when the report has some. */
249
+ function formatScore(s) {
250
+ const gate = s.gate === void 0 ? "no gate" : `gate ${String(s.gate)} %`;
251
+ const result = s.pass ? "pass" : `FAIL: ${s.reason ?? ""}`;
252
+ const time = s.hitLimit + s.clockTimeouts === 0 ? "" : `, ${timeouts(s)}`;
253
+ return `${s.name}: ${s.percent.toFixed(2)} % (${String(s.detected)} of ${String(s.valid)}, ${String(s.survived.length)} survived${time}), ${gate}: ${result}`;
254
+ }
255
+ /** This function gives a Markdown table of scores, for the summary of a CI job. */
256
+ function summaryMarkdown(scores) {
257
+ return [
258
+ "| Selection | Score | Detected | Valid | Survived | Clock time-outs | Score with them | Gate | Result |",
259
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
260
+ ...scores.map((s) => `| ${s.name} | ${s.percent.toFixed(2)} % | ${String(s.detected)} | ${String(s.valid)} | ${String(s.survived.length)} | ${String(s.clockTimeouts)} | ${s.percentWithClock.toFixed(2)} % | ${s.gate === void 0 ? "none" : `${String(s.gate)} %`} | ${s.pass ? "pass" : `fail: ${s.reason ?? ""}`} |`),
261
+ ""
262
+ ].join("\n");
263
+ }
264
+ // src/disables.ts
265
+ /** This function finds each `Stryker disable` comment of the files. A file name uses `/`. */
266
+ function findDisables(files) {
267
+ return files.flatMap(({ file, text }) => text.split("\n").filter((line) => line.includes("Stryker disable")).map((line) => ({
268
+ file,
269
+ comment: line.trim()
270
+ })));
271
+ }
272
+ const counts = (xs) => {
273
+ const m = /* @__PURE__ */ new Map();
274
+ for (const x of xs) {
275
+ const k = `${x.file}: ${x.comment}`;
276
+ m.set(k, (m.get(k) ?? 0) + 1);
277
+ }
278
+ return m;
279
+ };
280
+ /**
281
+ * This function tells if a comment has a reason: a mutator name, a colon, a space and a text. It reads the text by
282
+ * index, so its time is linear in the length of the comment (a regular expression here was quadratic).
283
+ */
284
+ function hasReason(comment) {
285
+ const start = comment.indexOf("Stryker disable ");
286
+ if (start < 0) return false;
287
+ const rest = comment.slice(start + 16);
288
+ const colon = rest.indexOf(":");
289
+ return colon > 0 && rest[colon + 1] === " " && /\S/.test(rest[colon + 2] ?? "");
290
+ }
291
+ /** This function compares the comments of the sources with the list. A file with the same comment two times needs it two times in the list. */
292
+ function compareDisables(found, listed) {
293
+ const noReason = found.filter((d) => !hasReason(d.comment));
294
+ const a = counts(found);
295
+ const b = counts(listed);
296
+ const added = [...a].filter(([k, n]) => n > (b.get(k) ?? 0)).map(([k]) => k);
297
+ const removed = [...b].filter(([k, n]) => n > (a.get(k) ?? 0)).map(([k]) => k);
298
+ return {
299
+ added,
300
+ removed,
301
+ noReason,
302
+ ok: added.length === 0 && removed.length === 0 && noReason.length === 0
303
+ };
304
+ }
305
+ // src/run.ts
306
+ /**
307
+ * The CLI `rigor-mutation`. The work is in `runCli`, so a test can start the CLI in a fixture folder.
308
+ */
309
+ /** The help text of the CLI. */
310
+ const HELP = `rigor-mutation: the mutation tests of a repository, with Stryker and a gate for each package.
311
+
312
+ rigor-mutation list [--runs] [--changed <file>]
313
+ the packages and the parts of the lane. With --runs, the
314
+ selections of a full run: the parts of a package with parts.
315
+ With --changed, only the runs with a changed file.
316
+ rigor-mutation dir <selection> the folder of the package of a selection
317
+ rigor-mutation config <selection> [--out <file>] [--concurrency <n>] [--dry-run] [--changed <file>]
318
+ the Stryker config of a selection, as JSON. With --dry-run,
319
+ Stryker starts only the tests. With --changed, it mutates only
320
+ the changed files of the selection.
321
+ rigor-mutation scores <selection>... [--report <file>] [--summary <file>]
322
+ the score of each selection, with its gate
323
+ rigor-mutation disables [--write] compare the disable comments with their list
324
+
325
+ --lane <file> the lane file. The default is mutation.json.
326
+ --changed <file> a file with one changed path on each line, from the folder of the lane file,
327
+ for example the output of git diff --name-only.
328
+
329
+ A selection is a package ("collections") or a part of a package ("collections.impl").
330
+ Each file of a package with parts must be in exactly one part. list --runs, and config and scores
331
+ of a part, fail when it is not.
332
+ The score of a selection reads <dir>/${REPORT_DIR}/mutation.json, or the file of --report.
333
+ `;
334
+ const SOURCE = /\.(?:[cm]?[jt]s|tsx)$/;
335
+ const TEST = /\.(?:test|spec)\.[^.]+$/;
336
+ const SKIP = /* @__PURE__ */ new Set([
337
+ "node_modules",
338
+ "build",
339
+ "dist",
340
+ "reports",
341
+ "coverage"
342
+ ]);
343
+ /** This function gives each source file of a folder, from `root`, with `/`. It skips the tests, the outputs and the dot folders. */
344
+ function sourcesOf(root, dir) {
345
+ return readdirSync(dir, { withFileTypes: true }).toSorted((x, y) => x.name.localeCompare(y.name)).flatMap((e) => {
346
+ const path = resolve(dir, e.name);
347
+ if (e.isDirectory()) return e.name.startsWith(".") || SKIP.has(e.name) ? [] : sourcesOf(root, path);
348
+ return e.isFile() && SOURCE.test(e.name) && !TEST.test(e.name) ? [relative(root, path).replaceAll("\\", "/")] : [];
349
+ });
350
+ }
351
+ const readJson = (file) => JSON.parse(readFileSync(file, "utf8"));
352
+ /**
353
+ * This function examines the parts of the packages against their source files.
354
+ *
355
+ * @throws Error with each file that is in no part, in more than one part, or in a part and not in `mutate`
356
+ */
357
+ function checkParts(lane, laneDir, pkgs) {
358
+ const errors = pkgs.flatMap((pkg) => {
359
+ const p = lane.packages[pkg];
360
+ if (p?.parts === void 0) return [];
361
+ const dir = resolve(laneDir, p.dir);
362
+ if (!existsSync(dir)) return [`${pkg}: the folder ${p.dir} does not exist`];
363
+ return partErrors(lane, pkg, sourcesOf(dir, dir));
364
+ });
365
+ if (errors.length > 0) throw new Error(`the parts do not cover each file exactly one time:\n ${errors.join("\n ")}`);
366
+ }
367
+ /** This function examines the parts of the package of each selection that is a part. */
368
+ const checkPartsOf = (lane, laneDir, names) => checkParts(lane, laneDir, [...new Set(names.map((n) => select(lane, n)).filter((s) => s.name !== s.pkg).map((s) => s.pkg))]);
369
+ function disables(lane, laneDir, write, io) {
370
+ const files = /* @__PURE__ */ new Set();
371
+ for (const [name, p] of Object.entries(lane.packages)) {
372
+ const dir = resolve(laneDir, p.dir);
373
+ if (!existsSync(dir)) throw new Error(`${name}: the folder ${p.dir} does not exist`);
374
+ for (const f of sourcesOf(laneDir, dir)) files.add(f);
375
+ }
376
+ const found = findDisables([...files].map((file) => ({
377
+ file,
378
+ text: readFileSync(resolve(laneDir, file), "utf8")
379
+ })));
380
+ const listFile = resolve(laneDir, lane.disables ?? "mutation-disables.json");
381
+ const check = compareDisables(found, existsSync(listFile) ? readJson(listFile) : []);
382
+ for (const d of check.noReason) io.stderr(`a disable without a reason after a colon: ${d.file}: ${d.comment}\n`);
383
+ if (write) {
384
+ if (check.noReason.length > 0) return 1;
385
+ writeFileSync(listFile, `${JSON.stringify(found, null, 2)}\n`);
386
+ io.stdout(`disables: wrote ${String(found.length)} comments to ${relative(io.cwd, listFile).replaceAll("\\", "/")}\n`);
387
+ return 0;
388
+ }
389
+ const perFile = /* @__PURE__ */ new Map();
390
+ for (const d of found) perFile.set(d.file, (perFile.get(d.file) ?? 0) + 1);
391
+ io.stdout(`disables: ${String(found.length)} in ${String(perFile.size)} files\n`);
392
+ for (const [file, n] of perFile) io.stdout(` ${file}: ${String(n)}\n`);
393
+ for (const k of check.added) io.stderr(`not in the list: ${k}\n`);
394
+ for (const k of check.removed) io.stderr(`in the list, not in the sources: ${k}\n`);
395
+ if (check.added.length > 0 || check.removed.length > 0) io.stderr("disables: the sources and the list are not the same. Add each new disable with its reason, or start with --write and examine the diff.\n");
396
+ return check.ok ? 0 : 1;
397
+ }
398
+ /** This function starts the CLI with its arguments and gives the exit code. */
399
+ function runCli(argv, io) {
400
+ const { values, positionals } = parseArgs({
401
+ args: [...argv],
402
+ allowPositionals: true,
403
+ options: {
404
+ lane: {
405
+ type: "string",
406
+ default: "mutation.json"
407
+ },
408
+ out: { type: "string" },
409
+ concurrency: { type: "string" },
410
+ report: { type: "string" },
411
+ summary: { type: "string" },
412
+ write: {
413
+ type: "boolean",
414
+ default: false
415
+ },
416
+ runs: {
417
+ type: "boolean",
418
+ default: false
419
+ },
420
+ "dry-run": {
421
+ type: "boolean",
422
+ default: false
423
+ },
424
+ changed: { type: "string" },
425
+ help: {
426
+ type: "boolean",
427
+ default: false
428
+ }
429
+ }
430
+ });
431
+ const [command, ...names] = positionals;
432
+ if (values.help || command === void 0) {
433
+ io.stdout(HELP);
434
+ return command === void 0 && !values.help ? 1 : 0;
435
+ }
436
+ const laneFile = resolve(io.cwd, values.lane);
437
+ const laneDir = dirname(laneFile);
438
+ const lane = parseLane(readJson(laneFile));
439
+ const changed = values.changed === void 0 ? void 0 : readFileSync(resolve(io.cwd, values.changed), "utf8").split("\n").map((line) => line.trim()).filter((line) => line !== "");
440
+ const one = () => {
441
+ if (names.length !== 1 || names[0] === void 0) throw new Error(`"${command}" takes one selection`);
442
+ return names[0];
443
+ };
444
+ switch (command) {
445
+ case "list": {
446
+ if (values.runs) checkParts(lane, laneDir, Object.keys(lane.packages));
447
+ if (changed !== void 0 && !values.runs) throw new Error(`--changed of "list" takes --runs`);
448
+ const runs = values.runs ? runSelections(lane) : selectionNames(lane);
449
+ const shown = changed === void 0 ? runs : runs.filter((n) => changedFiles(select(lane, n), changed).length > 0);
450
+ io.stdout(shown.length === 0 ? "" : `${shown.join("\n")}\n`);
451
+ return 0;
452
+ }
453
+ case "dir":
454
+ io.stdout(`${relative(io.cwd, resolve(laneDir, select(lane, one()).dir)).replaceAll("\\", "/") || "."}\n`);
455
+ return 0;
456
+ case "config": {
457
+ const concurrency = values.concurrency === void 0 ? void 0 : Number(values.concurrency);
458
+ if (concurrency !== void 0 && !(Number.isInteger(concurrency) && concurrency > 0)) throw new Error(`--concurrency is not a positive integer`);
459
+ checkPartsOf(lane, laneDir, [one()]);
460
+ const all = select(lane, one());
461
+ const files = changed === void 0 ? void 0 : changedFiles(all, changed);
462
+ if (files?.length === 0) throw new Error(`the selection ${all.name} has no changed file`);
463
+ const selection = files === void 0 ? all : {
464
+ ...all,
465
+ mutate: files
466
+ };
467
+ const options = {
468
+ ...concurrency === void 0 ? {} : { concurrency },
469
+ dryRun: values["dry-run"]
470
+ };
471
+ const text = `${JSON.stringify(strykerConfig(selection, options), null, 2)}\n`;
472
+ if (values.out === void 0) io.stdout(text);
473
+ else {
474
+ mkdirSync(dirname(resolve(io.cwd, values.out)), { recursive: true });
475
+ writeFileSync(resolve(io.cwd, values.out), text);
476
+ }
477
+ return 0;
478
+ }
479
+ case "scores": {
480
+ if (names.length === 0) throw new Error(`"scores" takes one or more selections`);
481
+ if (values.report !== void 0 && names.length > 1) throw new Error(`--report takes one selection`);
482
+ checkPartsOf(lane, laneDir, names);
483
+ const scores = names.map((name) => {
484
+ const s = select(lane, name);
485
+ const file = values.report === void 0 ? resolve(laneDir, s.dir, REPORT_DIR, "mutation.json") : resolve(io.cwd, values.report);
486
+ if (!existsSync(file)) {
487
+ const reason = `the report ${relative(io.cwd, file)} does not exist`;
488
+ return {
489
+ name,
490
+ gate: s.gate,
491
+ detected: 0,
492
+ hitLimit: 0,
493
+ clockTimeouts: 0,
494
+ valid: 0,
495
+ survived: [],
496
+ percent: 0,
497
+ percentWithClock: 0,
498
+ pass: false,
499
+ reason
500
+ };
501
+ }
502
+ return scoreReport(parseReport(readJson(file)), s);
503
+ });
504
+ for (const s of scores) {
505
+ io.stdout(`${formatScore(s)}\n`);
506
+ if (!s.pass) for (const m of s.survived) io.stdout(` survived: ${m}\n`);
507
+ }
508
+ if (values.summary !== void 0) appendFileSync(resolve(io.cwd, values.summary), `### Mutation scores\n\n${summaryMarkdown(scores)}\n`);
509
+ const failed = scores.filter((s) => !s.pass).map((s) => s.name);
510
+ if (failed.length > 0) io.stderr(`The gate failed for ${failed.join(", ")}.\n`);
511
+ return failed.length > 0 ? 1 : 0;
512
+ }
513
+ case "disables": return disables(lane, laneDir, values.write, io);
514
+ default:
515
+ io.stderr(`unknown command "${command}"\n\n${HELP}`);
516
+ return 1;
517
+ }
518
+ }
519
+ export { partErrors as _, formatScore as a, selectionNames as b, scoreReport as c, REPORT_DIR as d, TIMEOUT_MS as f, parseLane as g, changedFiles as h, findDisables as i, summaryMarkdown as l, STRYKER_OWNED as m, runCli as n, isHitLimit as o, strykerConfig as p, compareDisables as r, parseReport as s, HELP as t, DRY_RUN_TIMEOUT_MINUTES as u, runSelections as v, select as y };
520
+
521
+ //# sourceMappingURL=run-CveACYFE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-CveACYFE.js","names":[],"sources":["../src/lane.ts","../src/config.ts","../src/score.ts","../src/disables.ts","../src/run.ts"],"sourcesContent":["import { matchesGlob, posix } from \"node:path\";\n\n/**\n * The lane file of a repository (`mutation.json`) names the packages that the mutation tests mutate. A package has\n * its folder, its globs and an optional gate. A big package has parts, so that each part has its own runner.\n */\n\n/** The mutation tests of one package. */\nexport interface PackageLane {\n /** This field gives the folder of the package, from the folder of the lane file. Stryker starts in this folder. */\n readonly dir: string;\n /** These globs give the files to mutate, from `dir`. A glob with `!` removes the files of the globs before it. */\n readonly mutate: readonly string[];\n /**\n * Each part has its own globs. The `!` globs of `mutate` also apply to each part. Each file of `mutate` must be in\n * exactly one part, so that a full run mutates each file one time. `partErrors` examines this.\n */\n readonly parts?: Readonly<Record<string, readonly string[]>>;\n /** This field gives the lowest mutation score in percent that passes. Without a gate, the lane only reports the score. */\n readonly gate?: number;\n /** This field gives the Vitest config file, from `dir`. Without it, Vitest finds its config in `dir`. */\n readonly vitestConfig?: string;\n /**\n * This field gives the time in milliseconds that the clock adds to the time of the tests for each mutant. The\n * default is 120000. A short time on a loaded runner stops mutants that the tests detect later.\n */\n readonly timeoutMS?: number;\n /** This field gives the time in minutes for the run of the tests without mutants. The default is 15. */\n readonly dryRunTimeoutMinutes?: number;\n /**\n * These options go into the Stryker config of each selection of the package, for example `ignoreStatic`. The lane\n * owns the keys of `STRYKER_OWNED`, so the field cannot have them.\n */\n readonly stryker?: Readonly<Record<string, unknown>>;\n}\n\n/** The keys of the Stryker config that the lane owns. The field `stryker` of a package cannot have them. */\nexport const STRYKER_OWNED: readonly string[] = [\"mutate\", \"thresholds\", \"jsonReporter\", \"htmlReporter\", \"timeoutMS\", \"dryRunTimeoutMinutes\"];\n\n/** The lane file: the packages of the mutation tests, and the list of the disabled mutants. */\nexport interface Lane {\n readonly packages: Readonly<Record<string, PackageLane>>;\n /** This field gives the list of the disabled mutants, from the folder of the lane file. The default is `mutation-disables.json`. */\n readonly disables?: string;\n}\n\n/** A selection is one Stryker run: a package (`collections`) or a part of a package (`collections.impl`). */\nexport interface Selection {\n readonly name: string;\n readonly pkg: string;\n readonly dir: string;\n readonly mutate: readonly string[];\n readonly gate: number | undefined;\n readonly vitestConfig: string | undefined;\n readonly timeoutMS: number | undefined;\n readonly dryRunTimeoutMinutes: number | undefined;\n readonly stryker: Readonly<Record<string, unknown>> | undefined;\n}\n\nconst NAME = /^[\\w-]+$/;\n\nconst isRecord = (x: unknown): x is Record<string, unknown> => typeof x === \"object\" && x !== null && !Array.isArray(x);\n\nconst isGlobs = (x: unknown): x is string[] => Array.isArray(x) && x.length > 0 && x.every((g) => typeof g === \"string\" && g !== \"\");\n\n/** This function examines a package of the lane file. It gives the errors, with the name of the package. */\nfunction packageErrors(name: string, p: unknown): string[] {\n if (!NAME.test(name)) return [`the package name \"${name}\" has a character that is not a letter, a digit, \"_\" or \"-\"`];\n if (!isRecord(p)) return [`${name}: the package is not an object`];\n const errors: string[] = [];\n if (typeof p[\"dir\"] !== \"string\" || p[\"dir\"] === \"\") errors.push(`${name}: \"dir\" is not a folder name`);\n if (!isGlobs(p[\"mutate\"])) errors.push(`${name}: \"mutate\" is not a list of globs`);\n else if (p[\"mutate\"].every((g) => g.startsWith(\"!\"))) errors.push(`${name}: \"mutate\" has only \"!\" globs`);\n const gate = p[\"gate\"];\n if (gate !== undefined && (typeof gate !== \"number\" || !(gate >= 0 && gate <= 100))) errors.push(`${name}: \"gate\" is not a percent from 0 to 100`);\n if (p[\"vitestConfig\"] !== undefined && typeof p[\"vitestConfig\"] !== \"string\") errors.push(`${name}: \"vitestConfig\" is not a file name`);\n for (const field of [\"timeoutMS\", \"dryRunTimeoutMinutes\"]) {\n const v = p[field];\n if (v !== undefined && !(typeof v === \"number\" && Number.isInteger(v) && v > 0)) errors.push(`${name}: \"${field}\" is not a positive integer`);\n }\n const stryker = p[\"stryker\"];\n if (stryker !== undefined) {\n if (!isRecord(stryker)) errors.push(`${name}: \"stryker\" is not an object`);\n else {\n const owned = Object.keys(stryker).filter((k) => STRYKER_OWNED.includes(k));\n if (owned.length > 0) errors.push(`${name}: \"stryker\" has ${owned.map((k) => `\"${k}\"`).join(\", \")}, but the lane owns these keys`);\n }\n }\n const parts = p[\"parts\"];\n if (parts !== undefined) {\n if (!isRecord(parts) || Object.keys(parts).length === 0) errors.push(`${name}: \"parts\" is not an object with parts`);\n else {\n for (const [part, globs] of Object.entries(parts)) {\n if (!NAME.test(part)) errors.push(`${name}: the part name \"${part}\" has a character that is not a letter, a digit, \"_\" or \"-\"`);\n if (!isGlobs(globs) || globs.some((g) => g.startsWith(\"!\"))) errors.push(`${name}.${part}: the part is not a list of globs without \"!\"`);\n }\n }\n }\n return errors;\n}\n\n/**\n * This function examines the JSON of a lane file and gives the lane.\n *\n * @throws Error with each problem of the file\n */\nexport function parseLane(json: unknown): Lane {\n if (!isRecord(json) || !isRecord(json[\"packages\"])) throw new Error(`the lane has no object \"packages\"`);\n const errors = Object.entries(json[\"packages\"]).flatMap(([name, p]) => packageErrors(name, p));\n if (Object.keys(json[\"packages\"]).length === 0) errors.push(`the lane has no packages`);\n if (json[\"disables\"] !== undefined && typeof json[\"disables\"] !== \"string\") errors.push(`\"disables\" is not a file name`);\n if (errors.length > 0) throw new Error(`the lane is not valid:\\n ${errors.join(\"\\n \")}`);\n return json as unknown as Lane;\n}\n\n/** This function gives the names of all the selections of a lane: each package, then each part of the package. */\nexport function selectionNames(lane: Lane): string[] {\n return Object.entries(lane.packages).flatMap(([name, p]) => [name, ...Object.keys(p.parts ?? {}).map((part) => `${name}.${part}`)]);\n}\n\n/** This function gives the selections of a full run: the parts of each package with parts, else the package. Each file is in one run. */\nexport function runSelections(lane: Lane): string[] {\n return Object.entries(lane.packages).flatMap(([name, p]) => (p.parts === undefined ? [name] : Object.keys(p.parts).map((part) => `${name}.${part}`)));\n}\n\n/**\n * This function gives the selection of a name: a package, or a part of a package.\n *\n * @throws Error when the lane has no package or part of that name\n */\nexport function select(lane: Lane, name: string): Selection {\n const [pkg = \"\", part, ...rest] = name.split(\".\");\n const p = lane.packages[pkg];\n const globs = part === undefined ? undefined : p?.parts?.[part];\n if (p === undefined || rest.length > 0 || (part !== undefined && globs === undefined)) {\n throw new Error(`the lane has no package or part \"${name}\". It has: ${selectionNames(lane).join(\", \")}`);\n }\n // Stryker applies a \"!\" glob to the globs before it, so the \"!\" globs of the package go after the globs of the part.\n const mutate = globs === undefined ? p.mutate : [...globs, ...p.mutate.filter((g) => g.startsWith(\"!\"))];\n return { name, pkg, dir: p.dir, mutate, gate: p.gate, vitestConfig: p.vitestConfig, timeoutMS: p.timeoutMS, dryRunTimeoutMinutes: p.dryRunTimeoutMinutes, stryker: p.stryker };\n}\n\n/**\n * This function tells if globs select a file. It reads the globs as Stryker does. A glob adds its files. A glob with\n * `!` removes its files from the files of the globs before it.\n */\nexport function globsSelect(globs: readonly string[], file: string): boolean {\n let selected = false;\n for (const g of globs) {\n if (g.startsWith(\"!\")) {\n if (selected && matchesGlob(file, g.slice(1))) selected = false;\n } else if (!selected && matchesGlob(file, g)) selected = true;\n }\n return selected;\n}\n\n/**\n * This function gives the changed files of a selection, from the folder of its package. The globs of the selection\n * select the files.\n *\n * @param changed - the changed paths, from the folder of the lane file, with `/`\n */\nexport function changedFiles(selection: Selection, changed: readonly string[]): string[] {\n const dir = posix.normalize(selection.dir.replaceAll(\"\\\\\", \"/\"));\n return changed\n .map((f) => posix.relative(dir, posix.normalize(f.replaceAll(\"\\\\\", \"/\"))))\n .filter((f) => f !== \"\" && !f.startsWith(\"../\") && !posix.isAbsolute(f) && globsSelect(selection.mutate, f));\n}\n\n/**\n * This function examines the parts of a package against its files. It gives an error for each file of `mutate` in no\n * part. It also gives an error for each file in two parts, and for each file of a part that is not in `mutate`. A\n * package with no parts has no errors.\n *\n * @param files - the source files of the package, from its folder, with `/`\n */\nexport function partErrors(lane: Lane, pkg: string, files: readonly string[]): string[] {\n const p = lane.packages[pkg];\n if (p?.parts === undefined) return [];\n const parts = Object.keys(p.parts).map((part) => ({ part, mutate: select(lane, `${pkg}.${part}`).mutate }));\n const errors: string[] = [];\n for (const file of files) {\n const inMutate = globsSelect(p.mutate, file);\n const inParts = parts.filter((x) => globsSelect(x.mutate, file)).map((x) => x.part);\n if (inMutate && inParts.length === 0) errors.push(`${pkg}: the file ${file} is in no part, so a full run does not mutate it`);\n else if (inParts.length > 1) errors.push(`${pkg}: the file ${file} is in more than one part: ${inParts.join(\", \")}`);\n else if (!inMutate && inParts.length === 1) errors.push(`${pkg}: the file ${file} is in the part ${inParts.join(\"\")}, but not in \"mutate\"`);\n }\n return errors;\n}\n","import type { Selection } from \"./lane.ts\";\n\n/** The options of `strykerConfig`. */\nexport interface ConfigOptions {\n /** This option gives the number of test processes. The default is 2. */\n readonly concurrency?: number;\n /** This option gives the folder of the reports, from the folder of the package. The default is `reports/mutation`. */\n readonly reportDir?: string;\n /**\n * When this option is true, Stryker starts only the tests without mutants, and it does not stop at the first failed\n * test. Use it to see if the tests pass under Stryker before a long run.\n */\n readonly dryRun?: boolean;\n}\n\n/** The Stryker config of one selection, as JSON. Stryker starts in the folder of the package. The field `stryker` of the lane adds keys. */\nexport interface StrykerConfig {\n readonly [key: string]: unknown;\n readonly testRunner: \"vitest\";\n readonly plugins: readonly string[];\n readonly vitest?: { readonly configFile: string };\n readonly mutate: readonly string[];\n readonly coverageAnalysis: \"perTest\";\n readonly reporters: readonly string[];\n readonly htmlReporter: { readonly fileName: string };\n readonly jsonReporter: { readonly fileName: string };\n readonly thresholds: { readonly high: number; readonly low: number; readonly break: null };\n readonly concurrency: number;\n readonly timeoutMS: number;\n readonly dryRunTimeoutMinutes: number;\n readonly dryRunOnly?: true;\n readonly disableBail?: true;\n}\n\n/** The default folder of the reports, from the folder of the package. */\nexport const REPORT_DIR = \"reports/mutation\";\n\n/**\n * The default time in milliseconds that the clock adds to the time of the tests for each mutant. The default of\n * Stryker is 5000. On a loaded CI runner that time stopped mutants that the tests detected later.\n */\nexport const TIMEOUT_MS = 120_000;\n\n/** The default time in minutes for the run of the tests without mutants. The default of Stryker is 5. */\nexport const DRY_RUN_TIMEOUT_MINUTES = 15;\n\n/**\n * This function gives the Stryker config of a selection. The break of Stryker is off, because the gate of each\n * package is in `scoreReport`. Stryker 10.1.0 or later is necessary with Vitest 5: version 10.0 started no test on each\n * mutant (stryker-js issue 6210).\n */\nexport function strykerConfig(selection: Selection, options: ConfigOptions = {}): StrykerConfig {\n const reportDir = options.reportDir ?? REPORT_DIR;\n const low = selection.gate ?? 60;\n return {\n testRunner: \"vitest\",\n plugins: [\"@stryker-mutator/vitest-runner\"],\n ...(selection.vitestConfig === undefined ? {} : { vitest: { configFile: selection.vitestConfig } }),\n mutate: selection.mutate,\n coverageAnalysis: \"perTest\",\n reporters: [\"clear-text\", \"progress\", \"html\", \"json\"],\n htmlReporter: { fileName: `${reportDir}/index.html` },\n jsonReporter: { fileName: `${reportDir}/mutation.json` },\n thresholds: { high: Math.max(low, 90), low, break: null },\n concurrency: options.concurrency ?? 2,\n timeoutMS: selection.timeoutMS ?? TIMEOUT_MS,\n dryRunTimeoutMinutes: selection.dryRunTimeoutMinutes ?? DRY_RUN_TIMEOUT_MINUTES,\n // The keys of the lane field \"stryker\" replace the keys above, but \"--concurrency\" replaces its \"concurrency\".\n ...selection.stryker,\n ...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }),\n ...(options.dryRun === true ? { dryRunOnly: true, disableBail: true } : {}),\n };\n}\n","/**\n * The score of a Stryker report, with the gate of the package. The score counts the detected mutants, out of the\n * mutants that a test covers or that survive. Compile errors, runtime errors and ignored mutants do not count.\n *\n * Stryker gives the status `Timeout` for two events. The hit limit stops a mutant with an endless loop: its\n * `statusReason` starts with `Hit limit`, and the mutant is detected. The clock stops a mutant that took too long. The\n * load of the runner can cause this. Thus the mutant is not detected for certain, and it counts as not detected.\n */\n\n/** A mutant of the JSON report of Stryker (the schema of mutation-testing-elements). */\nexport interface Mutant {\n readonly status: string;\n readonly mutatorName: string;\n /** Stryker gives the reason of some statuses here. A `Timeout` of the hit limit has a reason that starts with `Hit limit`. */\n readonly statusReason?: string | undefined;\n readonly location: { readonly start: { readonly line: number } };\n}\n\n/** The JSON report of Stryker: the mutants of each mutated file. */\nexport interface MutationReport {\n readonly files: Readonly<Record<string, { readonly mutants: readonly Mutant[] }>>;\n}\n\n/** The score of one selection. */\nexport interface Score {\n readonly name: string;\n readonly gate: number | undefined;\n /** The mutants with the status `Killed`, and the mutants that the hit limit stopped. */\n readonly detected: number;\n /** The mutants that the hit limit stopped. They are in `detected`. */\n readonly hitLimit: number;\n /** The mutants that the clock stopped. They count as not detected, and they are in `survived`. */\n readonly clockTimeouts: number;\n /** The mutants that count: detected, survived or with no coverage. */\n readonly valid: number;\n /** Each mutant that survived, had no coverage or was stopped by the clock, as `file:line Mutator`. */\n readonly survived: readonly string[];\n /** The score in percent. It is 0 when no mutant counts. */\n readonly percent: number;\n /** The score in percent with the clock time-outs counted as detected, as Stryker counts them. The gate does not read it. */\n readonly percentWithClock: number;\n readonly pass: boolean;\n /** The reason of a failure. */\n readonly reason: string | undefined;\n}\n\nconst UNDETECTED = new Set([\"Survived\", \"NoCoverage\"]);\n\n/** This function tells if the hit limit of Stryker stopped a mutant. Then the mutant has an endless loop, and it is detected. */\nexport const isHitLimit = (m: Mutant): boolean => m.status === \"Timeout\" && (m.statusReason ?? \"\").startsWith(\"Hit limit\");\n\n/**\n * This function examines the JSON of a Stryker report and gives the report.\n *\n * @throws Error when the JSON has no files with mutants\n */\nexport function parseReport(json: unknown): MutationReport {\n const files = typeof json === \"object\" && json !== null ? (json as { files?: unknown }).files : undefined;\n if (typeof files !== \"object\" || files === null) throw new Error(`the report has no object \"files\"`);\n for (const [file, data] of Object.entries(files)) {\n if (!Array.isArray((data as { mutants?: unknown } | null)?.mutants)) throw new Error(`the report has no list \"mutants\" for ${file}`);\n }\n return json as MutationReport;\n}\n\n/**\n * This function gives the score of a report and applies the gate. A report with no mutants that count fails, also\n * without a gate: Stryker found no files. A report with mutants that count and none detected also fails: the test\n * runner started no test on the mutants. Stryker 10.0 with Vitest 5 gave this result.\n */\nexport function scoreReport(report: MutationReport, selection: { readonly name: string; readonly gate: number | undefined }): Score {\n let detected = 0;\n let valid = 0;\n let hitLimit = 0;\n let clockTimeouts = 0;\n const survived: string[] = [];\n for (const [file, data] of Object.entries(report.files)) {\n for (const m of data.mutants) {\n const at = `${file}:${String(m.location.start.line)} ${m.mutatorName}`;\n if (m.status === \"Killed\" || isHitLimit(m)) {\n detected++;\n valid++;\n if (m.status === \"Timeout\") hitLimit++;\n } else if (m.status === \"Timeout\") {\n clockTimeouts++;\n valid++;\n survived.push(`${at} (the clock stopped it)`);\n } else if (UNDETECTED.has(m.status)) {\n valid++;\n survived.push(at);\n }\n }\n }\n const percent = valid === 0 ? 0 : (100 * detected) / valid;\n const percentWithClock = valid === 0 ? 0 : (100 * (detected + clockTimeouts)) / valid;\n const { name, gate } = selection;\n const reason =\n valid === 0\n ? \"the report has no mutants that count: Stryker found no files to mutate, or it did not start\"\n : detected === 0\n ? `the tests detected none of the ${String(valid)} mutants: the test runner started no test on the mutants`\n : gate !== undefined && percent < gate\n ? `the score ${percent.toFixed(2)} % is under the gate ${String(gate)} %`\n : undefined;\n return { name, gate, detected, hitLimit, clockTimeouts, valid, survived, percent, percentWithClock, pass: reason === undefined, reason };\n}\n\n/** This function gives the time-outs of a score as text: the count of each kind, and the score that counts the clock time-outs as detected. */\nconst timeouts = (s: Score): string =>\n `time-outs: ${String(s.hitLimit)} hit limit, ${String(s.clockTimeouts)} clock (${s.percentWithClock.toFixed(2)} % with the clock time-outs as detected)`;\n\n/** This function gives one line for a score, with the name of the selection and its result. The time-outs are in it when the report has some. */\nexport function formatScore(s: Score): string {\n const gate = s.gate === undefined ? \"no gate\" : `gate ${String(s.gate)} %`;\n const result = s.pass ? \"pass\" : `FAIL: ${s.reason ?? \"\"}`;\n const time = s.hitLimit + s.clockTimeouts === 0 ? \"\" : `, ${timeouts(s)}`;\n return `${s.name}: ${s.percent.toFixed(2)} % (${String(s.detected)} of ${String(s.valid)}, ${String(s.survived.length)} survived${time}), ${gate}: ${result}`;\n}\n\n/** This function gives a Markdown table of scores, for the summary of a CI job. */\nexport function summaryMarkdown(scores: readonly Score[]): string {\n const rows = scores.map(\n (s) =>\n `| ${s.name} | ${s.percent.toFixed(2)} % | ${String(s.detected)} | ${String(s.valid)} | ${String(s.survived.length)} | ${String(s.clockTimeouts)} | ${s.percentWithClock.toFixed(2)} % | ${s.gate === undefined ? \"none\" : `${String(s.gate)} %`} | ${s.pass ? \"pass\" : `fail: ${s.reason ?? \"\"}`} |`,\n );\n return [\n \"| Selection | Score | Detected | Valid | Survived | Clock time-outs | Score with them | Gate | Result |\",\n \"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |\",\n ...rows,\n \"\",\n ].join(\"\\n\");\n}\n","/**\n * A comment `// Stryker disable next-line <Mutator>: <reason>` disables a mutant that is equivalent. The disable\n * raises the score, so each one is also in a list with its reason. These functions find the comments in the\n * sources and compare them with the list.\n */\n\n/** One disable comment of a source file. */\nexport interface Disable {\n readonly file: string;\n readonly comment: string;\n}\n\n/** The result of `compareDisables`. */\nexport interface DisableCheck {\n /** The comments of the sources that are not in the list, as `file: comment`. */\n readonly added: readonly string[];\n /** The comments of the list that are not in the sources, as `file: comment`. */\n readonly removed: readonly string[];\n /** The comments with no reason after a colon. */\n readonly noReason: readonly Disable[];\n readonly ok: boolean;\n}\n\n/** This function finds each `Stryker disable` comment of the files. A file name uses `/`. */\nexport function findDisables(files: readonly { readonly file: string; readonly text: string }[]): Disable[] {\n return files.flatMap(({ file, text }) =>\n text\n .split(\"\\n\")\n .filter((line) => line.includes(\"Stryker disable\"))\n .map((line) => ({ file, comment: line.trim() })),\n );\n}\n\nconst counts = (xs: readonly Disable[]): Map<string, number> => {\n const m = new Map<string, number>();\n for (const x of xs) {\n const k = `${x.file}: ${x.comment}`;\n m.set(k, (m.get(k) ?? 0) + 1);\n }\n return m;\n};\n\n/**\n * This function tells if a comment has a reason: a mutator name, a colon, a space and a text. It reads the text by\n * index, so its time is linear in the length of the comment (a regular expression here was quadratic).\n */\nfunction hasReason(comment: string): boolean {\n const start = comment.indexOf(\"Stryker disable \");\n if (start < 0) return false;\n const rest = comment.slice(start + \"Stryker disable \".length);\n const colon = rest.indexOf(\":\");\n return colon > 0 && rest[colon + 1] === \" \" && /\\S/.test(rest[colon + 2] ?? \"\");\n}\n\n/** This function compares the comments of the sources with the list. A file with the same comment two times needs it two times in the list. */\nexport function compareDisables(found: readonly Disable[], listed: readonly Disable[]): DisableCheck {\n const noReason = found.filter((d) => !hasReason(d.comment));\n const a = counts(found);\n const b = counts(listed);\n const added = [...a].filter(([k, n]) => n > (b.get(k) ?? 0)).map(([k]) => k);\n const removed = [...b].filter(([k, n]) => n > (a.get(k) ?? 0)).map(([k]) => k);\n return { added, removed, noReason, ok: added.length === 0 && removed.length === 0 && noReason.length === 0 };\n}\n","/**\n * The CLI `rigor-mutation`. The work is in `runCli`, so a test can start the CLI in a fixture folder.\n */\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, appendFileSync } from \"node:fs\";\nimport { dirname, relative, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { REPORT_DIR, strykerConfig } from \"./config.ts\";\nimport { compareDisables, findDisables, type Disable } from \"./disables.ts\";\nimport { changedFiles, parseLane, partErrors, runSelections, select, selectionNames, type Lane } from \"./lane.ts\";\nimport { formatScore, parseReport, scoreReport, summaryMarkdown, type Score } from \"./score.ts\";\n\n/** The input and the output of the CLI. */\nexport interface CliIo {\n readonly cwd: string;\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n}\n\n/** The help text of the CLI. */\nexport const HELP: string = `rigor-mutation: the mutation tests of a repository, with Stryker and a gate for each package.\n\n rigor-mutation list [--runs] [--changed <file>]\n the packages and the parts of the lane. With --runs, the\n selections of a full run: the parts of a package with parts.\n With --changed, only the runs with a changed file.\n rigor-mutation dir <selection> the folder of the package of a selection\n rigor-mutation config <selection> [--out <file>] [--concurrency <n>] [--dry-run] [--changed <file>]\n the Stryker config of a selection, as JSON. With --dry-run,\n Stryker starts only the tests. With --changed, it mutates only\n the changed files of the selection.\n rigor-mutation scores <selection>... [--report <file>] [--summary <file>]\n the score of each selection, with its gate\n rigor-mutation disables [--write] compare the disable comments with their list\n\n --lane <file> the lane file. The default is mutation.json.\n --changed <file> a file with one changed path on each line, from the folder of the lane file,\n for example the output of git diff --name-only.\n\nA selection is a package (\"collections\") or a part of a package (\"collections.impl\").\nEach file of a package with parts must be in exactly one part. list --runs, and config and scores\nof a part, fail when it is not.\nThe score of a selection reads <dir>/${REPORT_DIR}/mutation.json, or the file of --report.\n`;\n\nconst SOURCE = /\\.(?:[cm]?[jt]s|tsx)$/;\nconst TEST = /\\.(?:test|spec)\\.[^.]+$/;\nconst SKIP = new Set([\"node_modules\", \"build\", \"dist\", \"reports\", \"coverage\"]);\n\n/** This function gives each source file of a folder, from `root`, with `/`. It skips the tests, the outputs and the dot folders. */\nfunction sourcesOf(root: string, dir: string): string[] {\n return readdirSync(dir, { withFileTypes: true })\n .toSorted((x, y) => x.name.localeCompare(y.name))\n .flatMap((e) => {\n const path = resolve(dir, e.name);\n if (e.isDirectory()) return e.name.startsWith(\".\") || SKIP.has(e.name) ? [] : sourcesOf(root, path);\n return e.isFile() && SOURCE.test(e.name) && !TEST.test(e.name) ? [relative(root, path).replaceAll(\"\\\\\", \"/\")] : [];\n });\n}\n\nconst readJson = (file: string): unknown => JSON.parse(readFileSync(file, \"utf8\"));\n\n/**\n * This function examines the parts of the packages against their source files.\n *\n * @throws Error with each file that is in no part, in more than one part, or in a part and not in `mutate`\n */\nfunction checkParts(lane: Lane, laneDir: string, pkgs: readonly string[]): void {\n const errors = pkgs.flatMap((pkg) => {\n const p = lane.packages[pkg];\n if (p?.parts === undefined) return [];\n const dir = resolve(laneDir, p.dir);\n if (!existsSync(dir)) return [`${pkg}: the folder ${p.dir} does not exist`];\n return partErrors(lane, pkg, sourcesOf(dir, dir));\n });\n if (errors.length > 0) throw new Error(`the parts do not cover each file exactly one time:\\n ${errors.join(\"\\n \")}`);\n}\n\n/** This function examines the parts of the package of each selection that is a part. */\nconst checkPartsOf = (lane: Lane, laneDir: string, names: readonly string[]): void =>\n checkParts(lane, laneDir, [...new Set(names.map((n) => select(lane, n)).filter((s) => s.name !== s.pkg).map((s) => s.pkg))]);\n\nfunction disables(lane: Lane, laneDir: string, write: boolean, io: CliIo): number {\n const files = new Set<string>();\n for (const [name, p] of Object.entries(lane.packages)) {\n const dir = resolve(laneDir, p.dir);\n if (!existsSync(dir)) throw new Error(`${name}: the folder ${p.dir} does not exist`);\n for (const f of sourcesOf(laneDir, dir)) files.add(f);\n }\n const found = findDisables([...files].map((file) => ({ file, text: readFileSync(resolve(laneDir, file), \"utf8\") })));\n const listFile = resolve(laneDir, lane.disables ?? \"mutation-disables.json\");\n const listed = existsSync(listFile) ? (readJson(listFile) as Disable[]) : [];\n const check = compareDisables(found, listed);\n for (const d of check.noReason) io.stderr(`a disable without a reason after a colon: ${d.file}: ${d.comment}\\n`);\n if (write) {\n if (check.noReason.length > 0) return 1;\n writeFileSync(listFile, `${JSON.stringify(found, null, 2)}\\n`);\n io.stdout(`disables: wrote ${String(found.length)} comments to ${relative(io.cwd, listFile).replaceAll(\"\\\\\", \"/\")}\\n`);\n return 0;\n }\n const perFile = new Map<string, number>();\n for (const d of found) perFile.set(d.file, (perFile.get(d.file) ?? 0) + 1);\n io.stdout(`disables: ${String(found.length)} in ${String(perFile.size)} files\\n`);\n for (const [file, n] of perFile) io.stdout(` ${file}: ${String(n)}\\n`);\n for (const k of check.added) io.stderr(`not in the list: ${k}\\n`);\n for (const k of check.removed) io.stderr(`in the list, not in the sources: ${k}\\n`);\n if (check.added.length > 0 || check.removed.length > 0) {\n io.stderr(\"disables: the sources and the list are not the same. Add each new disable with its reason, or start with --write and examine the diff.\\n\");\n }\n return check.ok ? 0 : 1;\n}\n\n/** This function starts the CLI with its arguments and gives the exit code. */\nexport function runCli(argv: readonly string[], io: CliIo): number {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n options: {\n lane: { type: \"string\", default: \"mutation.json\" },\n out: { type: \"string\" },\n concurrency: { type: \"string\" },\n report: { type: \"string\" },\n summary: { type: \"string\" },\n write: { type: \"boolean\", default: false },\n runs: { type: \"boolean\", default: false },\n \"dry-run\": { type: \"boolean\", default: false },\n changed: { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n });\n const [command, ...names] = positionals;\n if (values.help || command === undefined) {\n io.stdout(HELP);\n return command === undefined && !values.help ? 1 : 0;\n }\n const laneFile = resolve(io.cwd, values.lane);\n const laneDir = dirname(laneFile);\n const lane = parseLane(readJson(laneFile));\n const changed =\n values.changed === undefined\n ? undefined\n : readFileSync(resolve(io.cwd, values.changed), \"utf8\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\");\n const one = (): string => {\n if (names.length !== 1 || names[0] === undefined) throw new Error(`\"${command}\" takes one selection`);\n return names[0];\n };\n switch (command) {\n case \"list\": {\n if (values.runs) checkParts(lane, laneDir, Object.keys(lane.packages));\n if (changed !== undefined && !values.runs) throw new Error(`--changed of \"list\" takes --runs`);\n const runs = values.runs ? runSelections(lane) : selectionNames(lane);\n const shown = changed === undefined ? runs : runs.filter((n) => changedFiles(select(lane, n), changed).length > 0);\n io.stdout(shown.length === 0 ? \"\" : `${shown.join(\"\\n\")}\\n`);\n return 0;\n }\n case \"dir\":\n io.stdout(`${relative(io.cwd, resolve(laneDir, select(lane, one()).dir)).replaceAll(\"\\\\\", \"/\") || \".\"}\\n`);\n return 0;\n case \"config\": {\n const concurrency = values.concurrency === undefined ? undefined : Number(values.concurrency);\n if (concurrency !== undefined && !(Number.isInteger(concurrency) && concurrency > 0)) throw new Error(`--concurrency is not a positive integer`);\n checkPartsOf(lane, laneDir, [one()]);\n const all = select(lane, one());\n const files = changed === undefined ? undefined : changedFiles(all, changed);\n if (files?.length === 0) throw new Error(`the selection ${all.name} has no changed file`);\n const selection = files === undefined ? all : { ...all, mutate: files };\n const options = { ...(concurrency === undefined ? {} : { concurrency }), dryRun: values[\"dry-run\"] };\n const text = `${JSON.stringify(strykerConfig(selection, options), null, 2)}\\n`;\n if (values.out === undefined) io.stdout(text);\n else {\n mkdirSync(dirname(resolve(io.cwd, values.out)), { recursive: true });\n writeFileSync(resolve(io.cwd, values.out), text);\n }\n return 0;\n }\n case \"scores\": {\n if (names.length === 0) throw new Error(`\"scores\" takes one or more selections`);\n if (values.report !== undefined && names.length > 1) throw new Error(`--report takes one selection`);\n checkPartsOf(lane, laneDir, names);\n const scores: Score[] = names.map((name) => {\n const s = select(lane, name);\n const file = values.report === undefined ? resolve(laneDir, s.dir, REPORT_DIR, \"mutation.json\") : resolve(io.cwd, values.report);\n if (!existsSync(file)) {\n const reason = `the report ${relative(io.cwd, file)} does not exist`;\n return { name, gate: s.gate, detected: 0, hitLimit: 0, clockTimeouts: 0, valid: 0, survived: [], percent: 0, percentWithClock: 0, pass: false, reason };\n }\n return scoreReport(parseReport(readJson(file)), s);\n });\n for (const s of scores) {\n io.stdout(`${formatScore(s)}\\n`);\n if (!s.pass) for (const m of s.survived) io.stdout(` survived: ${m}\\n`);\n }\n if (values.summary !== undefined) appendFileSync(resolve(io.cwd, values.summary), `### Mutation scores\\n\\n${summaryMarkdown(scores)}\\n`);\n const failed = scores.filter((s) => !s.pass).map((s) => s.name);\n if (failed.length > 0) io.stderr(`The gate failed for ${failed.join(\", \")}.\\n`);\n return failed.length > 0 ? 1 : 0;\n }\n case \"disables\":\n return disables(lane, laneDir, values.write, io);\n default:\n io.stderr(`unknown command \"${command}\"\\n\\n${HELP}`);\n return 1;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@mark1russell7/rigor-mutation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.1.1",
5
+ "description": "Mutation tests with Stryker and Vitest for each package of a repository: the Stryker config of each package or part, a score with a gate for each package, and a check of the disabled mutants.",
6
+ "keywords": [
7
+ "mutation",
8
+ "mutation-testing",
9
+ "stryker",
10
+ "vitest",
11
+ "gate",
12
+ "monorepo",
13
+ "testing",
14
+ "ci"
15
+ ],
16
+ "homepage": "https://github.com/mark1russell7/rigor/tree/main/packages/mutation#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/mark1russell7/rigor/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mark1russell7/rigor.git",
23
+ "directory": "packages/mutation"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Mark Russell (https://github.com/mark1russell7)",
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./build/index.js",
30
+ "bin": {
31
+ "rigor-mutation": "./build/cli.js"
32
+ },
33
+ "files": [
34
+ "build"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "devDependencies": {
43
+ "@mark1russell7/cue": "^2.0.0",
44
+ "@types/node": "^26.6.5",
45
+ "typescript": "^7.0.2",
46
+ "vitest": "^5.0.3",
47
+ "esbuild": "^0.28.2",
48
+ "tsdown": "^0.23.1"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
55
+ },
56
+ "types": "./build/index.d.ts",
57
+ "exports": {
58
+ ".": {
59
+ "types": "./build/index.d.ts",
60
+ "import": "./build/index.js"
61
+ },
62
+ "./package.json": "./package.json"
63
+ }
6
64
  }