@t09tanaka/test-gates 0.4.0 → 0.4.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/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/dist/cli.js +47 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `test-gates mutation --affected` reuses every result after no change also where it cannot tell what a gate depends on (a file a gate or a spec loads reads files at run time, for one). Before, such a subproject ran every mutant on every run with the option, while a run without it reused the results after no change. The first line says so; a change still runs every mutant.
|
|
8
|
+
- `test-gates doctor` reports what keeps `--affected` from telling the gates apart: as a problem when a hook relies on `--affected`, else as a note. In such a subproject it no longer asks for `--affected` in a hook. Before, it said OK although a run with `--affected` was always one of every mutant.
|
|
9
|
+
|
|
3
10
|
## 0.4.0
|
|
4
11
|
|
|
5
12
|
Most of this release is about runs that were full ones although little had changed. `test-gates doctor` tells why a project's runs are full ones. `mutation --affected` runs again only the gates a change can affect. The comparison before a run covers only what the run loads. Two changes of behaviour: Stryker's `concurrency` now defaults to half of the cores, and the record of a run has a new format, so the first run after the update is one of every mutant.
|
package/README.md
CHANGED
|
@@ -318,7 +318,7 @@ A change elsewhere decides for every gate:
|
|
|
318
318
|
- **Every mutant is run** after a change of a config (the Stryker config and the config of the test runner, what they import, the setup files and other files they name, `package.json`, `test-gates.json`, `tsconfig*.json`, `*.config.*`, Babel configs, dotfiles, declaration files `.d.ts`, anything below `__mocks__`), of a lock file or of what is installed, of the tools or of the arguments; after a change of a file at the top of the subproject or outside of it that no gate depends on; and after a change of a file whose kind is not known (`.yaml`, `.html` and the like) that no gate depends on. Such a file that a gate does depend on runs that gate again, as any other file does.
|
|
319
319
|
- **Nothing is run again** for a change of a source, data, a snapshot, a style or an image below the subproject that no gate depends on, or of a document (`.md`, `.txt` and the like).
|
|
320
320
|
|
|
321
|
-
When what a gate depends on cannot be told from the text, every mutant
|
|
321
|
+
When what a gate depends on cannot be told from the text, a change runs every mutant, and the first line says why. After no change at all the results are reused as they are without the option, so `--affected` is never slower than a run without it. Gate by gate it cannot tell when it meets an import whose path is not written out (`import(name)`, `require(name)`), an import that leads to no file, a specifier that is neither a file nor an installed package (an alias defined only in a config: name it in `settings.importAliases`), a file read at run time (`node:fs`, `readFileSync`, `__dirname`, `import.meta.url`, `process.cwd`, `toMatchFileSnapshot`), a file that is loaded but not compared (git ignores it), a gate loaded for every spec by a setup file, a package of the same repository linked into `node_modules` (`@acme/shared`, also when it lies in the subproject), a component (`.vue`, `.svelte`, `.astro`, `.mdx`), which can use others that it does not import (registered by a plugin), and a test runner other than Jest and Vitest. Every mutant is also run when there is no record of a run with `--affected`, when the record cannot be read, with `--force`, and **when `CI` is set**: CI keeps running every mutant, and is the final check.
|
|
322
322
|
|
|
323
323
|
A gate with a survivor or a timeout that the allow lists do not cover is not reused: it runs again until it is clean. A mutant that timed out in the last run is never taken over either, also not in a gate whose other results are, and also not in a run without `--affected` after no change: on a loaded machine a mutant that would survive can time out instead, and a reused timeout would keep that. Those mutants alone are run again (`--mutate` with their ranges); the first lines count them. The run is judged as one of every gate, and its first lines say what was reused:
|
|
324
324
|
|
|
@@ -347,7 +347,7 @@ problem: .husky/pre-push runs test-gates mutation without --affected: a change o
|
|
|
347
347
|
test-gates doctor: 1 problem(s) that make runs full ones
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
-
A **problem** makes runs full ones that need not be: Stryker not in incremental mode, a path that turns reuse off (one that leads outside of the repository to something, or a Stryker config that cannot be loaded), a path that makes every file of the repository part of the comparison (it names the file and the path), a Stryker config that sets `force`,
|
|
350
|
+
A **problem** makes runs full ones that need not be: Stryker not in incremental mode, a path that turns reuse off (one that leads outside of the repository to something, or a Stryker config that cannot be loaded), a path that makes every file of the repository part of the comparison (it names the file and the path), a Stryker config that sets `force`, a hook that runs `test-gates mutation` of the subproject without `--affected`, also through its `test:gates:mutation` script, and, when a hook relies on `--affected`, what keeps it from telling the gates apart (a file a gate or a spec loads that reads files at run time, for one). Where it cannot tell them apart, a hook without `--affected` is not taken for a problem, and what keeps it from doing so is a note. A run in a hook is taken to be one of the subproject that its line names (`--dir`, `--prefix`, `-w`, `cd`), else the nearest `cd` before it; a name with a variable in it (`cd "$project"` in a loop over the subprojects), or none, may be one of any subproject. Each run is judged on its own line, and comments are left out. A **note** is worth knowing: that every file of the subproject is searched for paths, and why (a runner config that is not made with the helper, an import whose path is not written out), and that no hook runs a mutation run. The exit code is 1 when there is a problem, so that the doctor can run in CI next to the other checks.
|
|
351
351
|
|
|
352
352
|
### `test-gates mutation-result [--report <path>]`
|
|
353
353
|
|
package/dist/cli.js
CHANGED
|
@@ -1697,6 +1697,13 @@ function diagnose(facts) {
|
|
|
1697
1697
|
fix: "write such a path from the directory of its file (path.resolve(__dirname, \u2026)) so that it can be followed, or keep it out of the files the run loads"
|
|
1698
1698
|
});
|
|
1699
1699
|
}
|
|
1700
|
+
if (facts.blind !== null) {
|
|
1701
|
+
findings.push({
|
|
1702
|
+
level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
1703
|
+
text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
|
|
1704
|
+
fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1700
1707
|
if (facts.scope !== null && "every" in facts.scope) {
|
|
1701
1708
|
findings.push({
|
|
1702
1709
|
level: "note",
|
|
@@ -1711,7 +1718,7 @@ function diagnose(facts) {
|
|
|
1711
1718
|
fix: null
|
|
1712
1719
|
});
|
|
1713
1720
|
}
|
|
1714
|
-
for (const hook of facts.hooks.filter((one) => !one.affected)) {
|
|
1721
|
+
for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
|
|
1715
1722
|
findings.push({
|
|
1716
1723
|
level: "problem",
|
|
1717
1724
|
text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
|
|
@@ -2297,12 +2304,6 @@ function snapshotOf(input) {
|
|
|
2297
2304
|
return { snapshot: { mode, environment, args: input.args, files }, root: listRoot, widened };
|
|
2298
2305
|
}
|
|
2299
2306
|
|
|
2300
|
-
// src/commands/mutation.ts
|
|
2301
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
2302
|
-
import fs8 from "fs";
|
|
2303
|
-
import path8 from "path";
|
|
2304
|
-
import { pathToFileURL } from "url";
|
|
2305
|
-
|
|
2306
2307
|
// src/affected-files.ts
|
|
2307
2308
|
import fs6 from "fs";
|
|
2308
2309
|
import { builtinModules as builtinModules2 } from "module";
|
|
@@ -2762,6 +2763,12 @@ function withoutComments(text) {
|
|
|
2762
2763
|
return out;
|
|
2763
2764
|
}
|
|
2764
2765
|
|
|
2766
|
+
// src/commands/mutation.ts
|
|
2767
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2768
|
+
import fs8 from "fs";
|
|
2769
|
+
import path8 from "path";
|
|
2770
|
+
import { pathToFileURL } from "url";
|
|
2771
|
+
|
|
2765
2772
|
// src/commands/mutation-result.ts
|
|
2766
2773
|
import fs7 from "fs";
|
|
2767
2774
|
import path7 from "path";
|
|
@@ -3186,6 +3193,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3186
3193
|
plan: null,
|
|
3187
3194
|
last: void 0,
|
|
3188
3195
|
ci: affected ? affectedRefused(process.env) : null,
|
|
3196
|
+
unchanged: null,
|
|
3189
3197
|
configs
|
|
3190
3198
|
};
|
|
3191
3199
|
if (loaded !== void 0 && !options.incremental) {
|
|
@@ -3287,7 +3295,12 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3287
3295
|
plan = { full: `what a gate depends on cannot be told: ${error.message}` };
|
|
3288
3296
|
}
|
|
3289
3297
|
if ("full" in plan) {
|
|
3290
|
-
|
|
3298
|
+
if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
3299
|
+
decision.unchanged = plan.full;
|
|
3300
|
+
plan = null;
|
|
3301
|
+
} else {
|
|
3302
|
+
force = plan.full;
|
|
3303
|
+
}
|
|
3291
3304
|
}
|
|
3292
3305
|
}
|
|
3293
3306
|
}
|
|
@@ -3342,6 +3355,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
3342
3355
|
said = describePlan({ full: force });
|
|
3343
3356
|
} else if (plan !== null) {
|
|
3344
3357
|
said = describePlan(plan, 5, ranges.length);
|
|
3358
|
+
} else if (decision.unchanged !== null) {
|
|
3359
|
+
said = [
|
|
3360
|
+
`test-gates mutation --affected: nothing changed since the last run, every result is reused (gate by gate, ${decision.unchanged.replace(/^what a gate/, "what each gate")})`,
|
|
3361
|
+
...ranges.length > 0 ? [` and ${ranges.length} mutant(s) that timed out in the last run are run again`] : []
|
|
3362
|
+
];
|
|
3345
3363
|
}
|
|
3346
3364
|
} else if (ranges.length > 0) {
|
|
3347
3365
|
said = [
|
|
@@ -3562,11 +3580,30 @@ function runDoctor(dir, io2) {
|
|
|
3562
3580
|
` next run with --affected: ${first.replace(/^test-gates mutation --affected: /, "")}`
|
|
3563
3581
|
);
|
|
3564
3582
|
rest.forEach((line) => io2.out(` ${line}`));
|
|
3583
|
+
} else if (affected.unchanged !== null) {
|
|
3584
|
+
io2.out(" next run with --affected: reuses the results of the last run (nothing changed)");
|
|
3565
3585
|
} else {
|
|
3566
3586
|
const why = affected.forced ? "the Stryker config sets force" : affected.force;
|
|
3567
3587
|
io2.out(` next run with --affected: every mutant, ${why ?? "there is nothing to reuse"}`);
|
|
3568
3588
|
}
|
|
3569
3589
|
}
|
|
3590
|
+
let blind = null;
|
|
3591
|
+
if (plain2.snapshot !== null) {
|
|
3592
|
+
try {
|
|
3593
|
+
affectedInputs(manifest, {
|
|
3594
|
+
root: plain2.root,
|
|
3595
|
+
files: plain2.snapshot.files,
|
|
3596
|
+
configs: plain2.configs(),
|
|
3597
|
+
tests: [],
|
|
3598
|
+
covering: {}
|
|
3599
|
+
});
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
if (!(error instanceof AffectedError)) {
|
|
3602
|
+
throw error;
|
|
3603
|
+
}
|
|
3604
|
+
blind = error.message;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3570
3607
|
const { lines, status } = formatFindings(
|
|
3571
3608
|
diagnose({
|
|
3572
3609
|
incremental: plain2.incremental,
|
|
@@ -3574,7 +3611,8 @@ function runDoctor(dir, io2) {
|
|
|
3574
3611
|
failure: plain2.failure,
|
|
3575
3612
|
widened: plain2.widened,
|
|
3576
3613
|
scope: plain2.scope,
|
|
3577
|
-
hooks: hooksOf(fs9.realpathSync(manifest.dir))
|
|
3614
|
+
hooks: hooksOf(fs9.realpathSync(manifest.dir)),
|
|
3615
|
+
blind
|
|
3578
3616
|
})
|
|
3579
3617
|
);
|
|
3580
3618
|
lines.forEach((line) => io2.out(line));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t09tanaka/test-gates",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Test gates for the code that must not be wrong: per-file 100% coverage as the floor, mutation testing with a per-mutant allow list as the real check.",
|
|
5
5
|
"author": "t09tanaka",
|
|
6
6
|
"license": "MIT",
|