@t09tanaka/test-gates 0.2.0 → 0.3.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 +20 -0
- package/README.md +15 -10
- package/dist/{chunk-XEKQKLKY.js → chunk-F7WBEFIL.js} +6 -1
- package/dist/cli.js +16 -12
- package/dist/index.cjs +52 -3
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +47 -3
- package/dist/jest.cjs +6 -1
- package/dist/jest.js +1 -1
- package/dist/stryker.cjs +10 -1
- package/dist/stryker.d.cts +7 -1
- package/dist/stryker.d.ts +7 -1
- package/dist/stryker.js +4 -1
- package/dist/vitest.cjs +6 -1
- package/dist/vitest.js +1 -1
- package/package.json +1 -1
- package/schema/test-gates.schema.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `createStrykerGatesConfig` sets Stryker's `timeoutMS` to 30000 unless the project passes its own (Stryker's default is 5000). With `maxTimeouts` at 0 since 0.3.0, a mutant that would be killed but ran out of time fails the run. That happened on every full run in two projects with mutants that really never end: after each of them Stryker starts a new test runner, and the next mutant timed out while it was starting. A full run takes longer by up to 25 seconds for each mutant that hangs without tripping Stryker's loop counter.
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
Three defaults move to the strict side. A project that passed with 0.2.0 can fail with 0.3.0 without any change of its own; each case below says what to do.
|
|
12
|
+
|
|
13
|
+
### Changed (breaking)
|
|
14
|
+
|
|
15
|
+
- `settings.mutation.maxTimeouts` defaults to `0` (was: no limit). A mutant that times out outside `expectedTimeouts` now fails `test-gates mutation`. Run again with less load (`-- --concurrency 1`) first. For a mutant that can never finish, add it to `expectedTimeouts` of the gate with the reason. To tolerate some for now, set `settings.mutation.maxTimeouts` to a number.
|
|
16
|
+
- `test-gates check` fails, instead of warning, when the gate run uses Vitest below 4 with the v8 provider and neither `istanbul` nor `experimentalAstAwareRemapping: true`. Use `createVitestGatesConfig({ coverageProvider: 'istanbul' })` (needs `@vitest/coverage-istanbul`), or move to Vitest 4.
|
|
17
|
+
- A gate that reads `process.env` fails `test-gates check` (new built-in `forbiddenSource` rule). Take the value as an argument and read the environment in the caller. `settings.forbiddenSource.defaults: false` turns the built-in rules off, including the `'use client'` / `'use server'` one.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- The `warning:` line of `test-gates check`. Nothing is reported as a warning any more.
|
|
22
|
+
|
|
3
23
|
## 0.2.0
|
|
4
24
|
|
|
5
25
|
Everything added here is opt-in. With an unchanged `test-gates.json` and unchanged config files, 0.2.0 gives the same verdicts, exit codes and summary line as 0.1.0. The one visible difference is a warning on stderr from `test-gates check` for Vitest 3 with the v8 provider (see below); it does not change the exit code.
|
package/README.md
CHANGED
|
@@ -199,12 +199,14 @@ Static checks, before the gate run. Reports every violation as `file:line: reaso
|
|
|
199
199
|
- Every gate exists, and so does its spec. **The check is case-sensitive** even on macOS and Windows, because CI on Linux is.
|
|
200
200
|
- No path is listed twice, or in both `gates` and `candidates`. Candidates exist.
|
|
201
201
|
- A gate has no decorator (`@Name(` at the start of a line) and no runtime import of a framework, database client or SDK. `import type` is ignored.
|
|
202
|
+
- A gate does not read `process.env` (since 0.3.0). What comes from the environment is an input: take it as an argument.
|
|
202
203
|
- A gate and its spec have no coverage ignore directive (`istanbul ignore`, `c8 ignore`, `v8 ignore`).
|
|
203
204
|
- A spec has no `.skip`, `.only`, `.todo`, `xit`, `xdescribe`, `fit`, `fdescribe`, `.skipIf`, `.runIf`, `.fails`.
|
|
204
205
|
- A spec does not `jest.mock` / `vi.mock` the module it is supposed to test.
|
|
205
206
|
- A gate has no `Stryker disable` / `Stryker restore` comment. Such a comment turns off a whole line for a whole mutator, which takes non-equivalent mutants out with it.
|
|
206
207
|
- `equivalentMutants` entries are well formed and each has a `reason`.
|
|
207
208
|
- The Stryker config does not use `excludedMutations`, `ignoreStatic` or `ignorers`.
|
|
209
|
+
- A Vitest gate run below Vitest 4 does not use the plain v8 coverage provider (since 0.3.0), see [Vitest: v8 or istanbul](#vitest-v8-or-istanbul).
|
|
208
210
|
|
|
209
211
|
### `test-gates mutation [-- <stryker arguments>]`
|
|
210
212
|
|
|
@@ -228,9 +230,9 @@ The run also fails when
|
|
|
228
230
|
- two allowances point at the same mutant, or an allowance has no `reason`,
|
|
229
231
|
- a gate is missing from the report or has no mutants, or no mutant was evaluated at all,
|
|
230
232
|
- the report was made from a different version of a gate than the one on disk.
|
|
231
|
-
-
|
|
233
|
+
- a mutant timed out outside `expectedTimeouts`, or more of them than `settings.mutation.maxTimeouts` allows (see [Timeouts](#timeouts)).
|
|
232
234
|
|
|
233
|
-
`Timeout` counts as detected, but its count is always printed on its own (`detected N (timeout M)`). On a loaded machine mutants time out that would otherwise survive, so a
|
|
235
|
+
`Timeout` counts as detected, but its count is always printed on its own (`detected N (timeout M)`). On a loaded machine mutants time out that would otherwise survive, so a timeout nobody has explained fails the run.
|
|
234
236
|
|
|
235
237
|
On failure every unallowed survivor is listed as `file:line:column / mutator / original → replacement / occurrence`, followed by a JSON fragment that can be pasted into `equivalentMutants`. The last line gives the real numbers, never a percentage:
|
|
236
238
|
|
|
@@ -240,7 +242,7 @@ test-gates mutation: OK (mutants 317 / detected 308 (timeout 0) / allowed equiva
|
|
|
240
242
|
|
|
241
243
|
#### Timeouts
|
|
242
244
|
|
|
243
|
-
A timeout is a detection only if the mutant really cannot finish. On a loaded machine a mutant that would have survived can time out instead, and the run looks better than it is.
|
|
245
|
+
A timeout is a detection only if the mutant really cannot finish. On a loaded machine a mutant that would have survived can time out instead, and the run looks better than it is. Since 0.3.0 a timeout therefore fails the run unless the gate lists it:
|
|
244
246
|
|
|
245
247
|
```json
|
|
246
248
|
{
|
|
@@ -258,13 +260,12 @@ A timeout is a detection only if the mutant really cannot finish. On a loaded ma
|
|
|
258
260
|
}
|
|
259
261
|
]
|
|
260
262
|
}
|
|
261
|
-
]
|
|
262
|
-
"settings": { "mutation": { "maxTimeouts": 0 } }
|
|
263
|
+
]
|
|
263
264
|
}
|
|
264
265
|
```
|
|
265
266
|
|
|
266
|
-
- `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are tolerated. Above it the run fails with exit code 1 and lists every such mutant as `file:line:column / mutator / original → replacement`, with a fragment to paste. Not set means no limit, which is the 0.1.0 behaviour.
|
|
267
267
|
- `expectedTimeouts` (per gate): mutants that turn a loop into one that never ends. Same key as `equivalentMutants` (`mutator` + `original` + `replacement`, `occurrence` when ambiguous, `reason` required).
|
|
268
|
+
- `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are tolerated, default `0`. Above it the run fails with exit code 1 and lists every such mutant as `file:line:column / mutator / original → replacement`, with a fragment to paste. Up to 0.2.0 the default was no limit; a project that cannot get its timeouts to zero yet sets a number here and lowers it.
|
|
268
269
|
|
|
269
270
|
How an `expectedTimeouts` entry is judged:
|
|
270
271
|
|
|
@@ -277,7 +278,9 @@ How an `expectedTimeouts` entry is judged:
|
|
|
277
278
|
|
|
278
279
|
`Killed` is accepted on purpose, unlike a stale `equivalentMutants` entry. A mutant that hangs is reported as `Timeout` when Stryker's timer fires first and as `Killed` when the test runner's own per-test timeout fires first; which one wins depends on the load. Failing on `Killed` would make the list flap between two runs of the same code. And nothing is hidden by accepting it: `Killed` is the best possible outcome.
|
|
279
280
|
|
|
280
|
-
When the
|
|
281
|
+
When the run fails on a timeout, first run again with less load (`npm run test:gates:mutation -- --concurrency 1`). Add an entry only for a mutant that can never finish.
|
|
282
|
+
|
|
283
|
+
`createStrykerGatesConfig` sets Stryker's `timeoutMS` to 30000 (since 0.3.1; Stryker's own default is 5000). After a mutant that really never ends, Stryker starts a new test runner, and with 5000 the mutant that comes next can run out of time while that runner is still starting: in one project with 11 such mutants, 2 to 7 others timed out on every full run, a different set each time, and none in a run with 30000. A longer limit hides nothing, because a timeout that is not listed fails the run anyway. The price is the wait for each mutant that hangs without tripping Stryker's loop counter. Pass `timeoutMS` to the helper to change it.
|
|
281
284
|
|
|
282
285
|
### `test-gates selfcheck [--all | --first] [-- <gate command>]`
|
|
283
286
|
|
|
@@ -360,7 +363,7 @@ All optional. Patterns are regular expressions written as JSON strings; a rule i
|
|
|
360
363
|
| `impureImports.allow` | none | Module patterns exempt from every rule (e.g. `^express$` where `express` is imported only for its types without `import type`) |
|
|
361
364
|
| `impureNamedImports` | `PrismaClient` from `@prisma/client` | `{ "defaults", "add": [{ "module", "names", "reason" }] }`: names banned from a module that is otherwise fine |
|
|
362
365
|
| `impurePaths` | `.vue` `.jsx` `.tsx`, `.d.ts` | `{ "defaults", "add" }`: gate paths that are rejected (`\\.service\\.ts$`, `^src/hooks/`) |
|
|
363
|
-
| `forbiddenSource` | `'use client'` / `'use server'`
|
|
366
|
+
| `forbiddenSource` | `'use client'` / `'use server'`, `process.env` (0.3.0) | `{ "defaults", "add" }`: lines that must not appear in a gate, tested one line at a time |
|
|
364
367
|
| `importAliases` | none | `[{ "prefix": "@/", "target": "src/" }]`, to recognise `jest.mock('@/x')` of the module under test |
|
|
365
368
|
| `gateCommand` | derived from `jest.gates.config.*` / `vitest.gates.config.*` | Command `selfcheck` runs, as an array (`["jest", "--config", "jest.gates.config.js", "--maxWorkers=2"]`) |
|
|
366
369
|
| `selfcheck.mode` | `all` | `all` or `first` |
|
|
@@ -371,7 +374,7 @@ All optional. Patterns are regular expressions written as JSON strings; a rule i
|
|
|
371
374
|
| `lcov.summaryExclude` | none | Paths left out of the reference total (`\\.g\\.dart$`) |
|
|
372
375
|
| `imports.mode` (0.2.0) | `blocklist` | `allowlist` turns the import check around, see [Allowlist mode](#allowlist-mode) |
|
|
373
376
|
| `imports.allow` (0.2.0) | none | Packages a gate may import in allowlist mode: `"decimal.js"`, `{ "pattern": "^@acme/pure-" }`, `{ "module": "@prisma/client", "names": ["Prisma"] }` |
|
|
374
|
-
| `mutation.maxTimeouts` (0.2.0) |
|
|
377
|
+
| `mutation.maxTimeouts` (0.2.0) | `0` (0.3.0; no limit before) | Timeouts tolerated outside `expectedTimeouts`, see [Timeouts](#timeouts) |
|
|
375
378
|
|
|
376
379
|
Built-in impure imports: `@nestjs/*`, `class-validator`, `class-transformer`, `typeorm`, `sequelize`, `mongoose`, `knex`, `pg`, `mysql`, `mysql2`, `ioredis`, `redis`, `@prisma/adapter-*`, `react`, `react-dom`, `next`, `server-only`, `client-only`, `vue`, `vue-router`, `vue-i18n`, `pinia`, `nuxt`, `#app`, `#imports`, `*.vue`, `express`, `express-jwt`, `fastify`, `koa`, `rxjs`, `passport`, `passport-*`, `axios`, `openapi-fetch`, `stripe`, `nodemailer`, `firebase-admin`, `@aws-sdk/*`, `@sentry/*`, `@slack/*`, `@stripe/*`, and the Node.js modules `fs`, `http`, `https`, `http2`, `net`, `tls`, `dgram`, `dns`, `child_process`, `worker_threads`.
|
|
377
380
|
|
|
@@ -459,6 +462,7 @@ Why these choices:
|
|
|
459
462
|
- Jest measures with istanbul (`babel`), because the v8 provider does not count the untaken side of an `if` without `else`.
|
|
460
463
|
- Jest fails on a `coverageThreshold` path key that matches no file, so a typo in `test-gates.json` cannot pass. Vitest does the opposite: a glob or path key in `thresholds` that matches nothing passes. The Vitest helper therefore narrows `coverage.include` to the gates and uses `perFile`.
|
|
461
464
|
- Stryker never fails on its own score (`break: null`). The verdict comes from `test-gates mutation`, which knows the allow list.
|
|
465
|
+
- The Stryker helper also sets defaults that can be overridden: `coverageAnalysis: 'perTest'`, `incremental` with its file under `reports/`, and `timeoutMS: 30000` (see [Timeouts](#timeouts)).
|
|
462
466
|
|
|
463
467
|
`loadGates(dir)` returns the validated manifest: `gates` (each with its resolved `spec`), `candidates` and the resolved `settings`. It throws when the manifest has any problem.
|
|
464
468
|
|
|
@@ -490,7 +494,7 @@ export default defineConfig(
|
|
|
490
494
|
);
|
|
491
495
|
```
|
|
492
496
|
|
|
493
|
-
The default stays `v8` because changing it would break every project that has only `@vitest/coverage-v8` installed. Instead `test-gates check`
|
|
497
|
+
The helper's default stays `v8` because changing it would break every project that has only `@vitest/coverage-v8` installed. Instead `test-gates check` fails (since 0.3.0; a warning in 0.2.0) when it finds a `vitest.gates.config.*`, an installed Vitest below 4, and neither `istanbul` nor `experimentalAstAwareRemapping: true` in that config. On Vitest 4 the v8 provider passes the check.
|
|
494
498
|
|
|
495
499
|
## Cost, and how to use Stryker's incremental mode
|
|
496
500
|
|
|
@@ -523,6 +527,7 @@ How the incremental file and this tool interact (covered by `test/incremental.te
|
|
|
523
527
|
- **Stryker 10 does not start on Node.js 22.10** (`ERR_REQUIRE_ESM`). It runs on 22.14 and 22.22. On Node.js below 22.12 use Stryker 9.
|
|
524
528
|
- **Do not call the gate while the spec file is being collected** (in the body of `describe`, or at module level). The mutants reached that way become _static_ mutants, and with Stryker 10 and Vitest 3 a spec file that throws during collection was reported as `Survived`, not `Killed`. Call the gate inside `it` / `beforeEach`.
|
|
525
529
|
- **Jest treats an empty `testMatch` as its default pattern** and would run every test of the project. The helpers never produce an empty list; if you write the gate config by hand, do not either.
|
|
530
|
+
- **`process.env` is found by text too.** A comment that mentions it is reported, and reading the environment through another name (`const { env } = process`) is not. The clock (`Date.now()`, `new Date()`) and `Math.random()` are not checked: a default argument such as `now = new Date()` is a legitimate way to pass them in, and text cannot tell it from a direct read. Add a `forbiddenSource` rule if the project wants one.
|
|
526
531
|
- **Imports are found by text, not by parsing.** The checks run without the project's dependencies installed, so an import inside a comment is reported too, and only the gate's own imports are looked at, not what those modules import in turn.
|
|
527
532
|
- **An ordinary `import { Request } from 'express'` used only as a type is reported.** Write `import type`, or exempt the module with `impureImports.allow`.
|
|
528
533
|
- **`test-gates lcov` checks lines only.** lcov from Flutter carries no branch data, and Dart has no established mutation testing tool; a Dart gate is guarded by coverage alone.
|
|
@@ -174,8 +174,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
174
174
|
{
|
|
175
175
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
176
176
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
180
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
177
181
|
}
|
|
178
182
|
];
|
|
183
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
179
184
|
var SettingsError = class extends Error {
|
|
180
185
|
};
|
|
181
186
|
function isRecord2(value) {
|
|
@@ -437,7 +442,7 @@ function resolveSettings(raw) {
|
|
|
437
442
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
438
443
|
importMode: importsSection.mode ?? "blocklist",
|
|
439
444
|
importAllow,
|
|
440
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
445
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
441
446
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
442
447
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
443
448
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
package/dist/cli.js
CHANGED
|
@@ -542,7 +542,7 @@ function looksLikeSpec(gatePath, settings) {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
// src/core/vitest-coverage.ts
|
|
545
|
-
function
|
|
545
|
+
function vitestCoverageViolation(input) {
|
|
546
546
|
if (input.vitestVersion === null || input.vitestVersion === "") {
|
|
547
547
|
return null;
|
|
548
548
|
}
|
|
@@ -649,8 +649,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
649
649
|
{
|
|
650
650
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
651
651
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
655
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
652
656
|
}
|
|
653
657
|
];
|
|
658
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
654
659
|
var SettingsError = class extends Error {
|
|
655
660
|
};
|
|
656
661
|
function isRecord2(value) {
|
|
@@ -912,7 +917,7 @@ function resolveSettings(raw) {
|
|
|
912
917
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
913
918
|
importMode: importsSection.mode ?? "blocklist",
|
|
914
919
|
importAllow,
|
|
915
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
920
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
916
921
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
917
922
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
918
923
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -1086,6 +1091,7 @@ function checkGates(manifest) {
|
|
|
1086
1091
|
violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
|
|
1087
1092
|
}
|
|
1088
1093
|
}
|
|
1094
|
+
violations.push(...checkVitestCoverage(manifest));
|
|
1089
1095
|
return violations;
|
|
1090
1096
|
}
|
|
1091
1097
|
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
@@ -1108,23 +1114,20 @@ function installedVitestVersion(startDir) {
|
|
|
1108
1114
|
}
|
|
1109
1115
|
}
|
|
1110
1116
|
}
|
|
1111
|
-
function
|
|
1117
|
+
function checkVitestCoverage(manifest) {
|
|
1112
1118
|
const config = VITEST_GATE_CONFIGS.find((name) => fs3.existsSync(path3.join(manifest.dir, name)));
|
|
1113
1119
|
if (!config) {
|
|
1114
1120
|
return [];
|
|
1115
1121
|
}
|
|
1116
|
-
const
|
|
1122
|
+
const message = vitestCoverageViolation({
|
|
1117
1123
|
vitestVersion: installedVitestVersion(manifest.dir),
|
|
1118
1124
|
configSource: readText(manifest.dir, config)
|
|
1119
1125
|
});
|
|
1120
|
-
return
|
|
1126
|
+
return message === null ? [] : [{ file: config, message }];
|
|
1121
1127
|
}
|
|
1122
1128
|
function runCheck(dir, io2) {
|
|
1123
1129
|
const manifest = readManifest(dir);
|
|
1124
1130
|
const violations = checkGates(manifest);
|
|
1125
|
-
for (const warning of checkWarnings(manifest)) {
|
|
1126
|
-
io2.err(`test-gates check: warning: ${warning}`);
|
|
1127
|
-
}
|
|
1128
1131
|
if (violations.length > 0) {
|
|
1129
1132
|
printViolations(io2, "check", violations);
|
|
1130
1133
|
return 1;
|
|
@@ -1531,8 +1534,8 @@ function judgeMutationReport(input) {
|
|
|
1531
1534
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
1532
1535
|
});
|
|
1533
1536
|
}
|
|
1534
|
-
const maxTimeouts = input.maxTimeouts ??
|
|
1535
|
-
if (
|
|
1537
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
1538
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
1536
1539
|
violations.push({
|
|
1537
1540
|
file: MANIFEST_FILE,
|
|
1538
1541
|
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
@@ -1614,7 +1617,7 @@ function runMutationResult(dir, reportOverride, io2) {
|
|
|
1614
1617
|
io2.err("");
|
|
1615
1618
|
}
|
|
1616
1619
|
const maxTimeouts = manifest.settings.maxTimeouts;
|
|
1617
|
-
if (
|
|
1620
|
+
if (verdict.unexpectedTimeouts.length > maxTimeouts) {
|
|
1618
1621
|
io2.err(
|
|
1619
1622
|
`test-gates mutation: ${verdict.unexpectedTimeouts.length} mutant(s) timed out (settings.mutation.maxTimeouts: ${maxTimeouts})`
|
|
1620
1623
|
);
|
|
@@ -1625,7 +1628,8 @@ function runMutationResult(dir, reportOverride, io2) {
|
|
|
1625
1628
|
io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
|
|
1626
1629
|
io2.err("can time out instead. Run again with less load (-- --concurrency 1). Only for a");
|
|
1627
1630
|
io2.err("mutant that can never finish (a loop that no longer ends), add it to");
|
|
1628
|
-
io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"
|
|
1631
|
+
io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
|
|
1632
|
+
io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
|
|
1629
1633
|
for (const mutant of verdict.unexpectedTimeouts) {
|
|
1630
1634
|
io2.err(` ${mutant.file}: ${JSON.stringify(mutant.allowance)}`);
|
|
1631
1635
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -236,8 +236,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
236
236
|
{
|
|
237
237
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
238
238
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
242
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
239
243
|
}
|
|
240
244
|
];
|
|
245
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
241
246
|
var SettingsError = class extends Error {
|
|
242
247
|
};
|
|
243
248
|
function isRecord2(value) {
|
|
@@ -499,7 +504,7 @@ function resolveSettings(raw) {
|
|
|
499
504
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
500
505
|
importMode: importsSection.mode ?? "blocklist",
|
|
501
506
|
importAllow,
|
|
502
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
507
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
503
508
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
504
509
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
505
510
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -918,6 +923,21 @@ function scanStrykerConfig(configPath, source) {
|
|
|
918
923
|
return violations;
|
|
919
924
|
}
|
|
920
925
|
|
|
926
|
+
// src/core/vitest-coverage.ts
|
|
927
|
+
function vitestCoverageViolation(input) {
|
|
928
|
+
if (input.vitestVersion === null || input.vitestVersion === "") {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
const major = Number(input.vitestVersion.split(".")[0]);
|
|
932
|
+
if (!Number.isInteger(major) || major >= 4) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
if (/\bistanbul\b/.test(input.configSource) || /\bexperimentalAstAwareRemapping\s*:\s*true\b/.test(input.configSource)) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
return `Vitest ${input.vitestVersion} with the v8 coverage provider does not count the skipped side of an "if" without "else", so a gate can show 100% branches with that side untested. Use coverageProvider: 'istanbul' (needs @vitest/coverage-istanbul), set coverage.experimentalAstAwareRemapping: true, or move to Vitest 4`;
|
|
939
|
+
}
|
|
940
|
+
|
|
921
941
|
// src/commands/check.ts
|
|
922
942
|
var STRYKER_CONFIG_NAMES = [
|
|
923
943
|
"stryker.gates.config.mjs",
|
|
@@ -1008,11 +1028,40 @@ function checkGates(manifest) {
|
|
|
1008
1028
|
violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
|
|
1009
1029
|
}
|
|
1010
1030
|
}
|
|
1031
|
+
violations.push(...checkVitestCoverage(manifest));
|
|
1011
1032
|
return violations;
|
|
1012
1033
|
}
|
|
1013
1034
|
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
1014
1035
|
(extension) => `vitest.gates.config.${extension}`
|
|
1015
1036
|
);
|
|
1037
|
+
function installedVitestVersion(startDir) {
|
|
1038
|
+
let current = startDir;
|
|
1039
|
+
for (; ; ) {
|
|
1040
|
+
try {
|
|
1041
|
+
const pkg = JSON.parse(
|
|
1042
|
+
import_node_fs3.default.readFileSync(import_node_path3.default.join(current, "node_modules", "vitest", "package.json"), "utf8")
|
|
1043
|
+
);
|
|
1044
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
1045
|
+
} catch {
|
|
1046
|
+
const parent = import_node_path3.default.dirname(current);
|
|
1047
|
+
if (parent === current) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
current = parent;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function checkVitestCoverage(manifest) {
|
|
1055
|
+
const config = VITEST_GATE_CONFIGS.find((name) => import_node_fs3.default.existsSync(import_node_path3.default.join(manifest.dir, name)));
|
|
1056
|
+
if (!config) {
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1059
|
+
const message = vitestCoverageViolation({
|
|
1060
|
+
vitestVersion: installedVitestVersion(manifest.dir),
|
|
1061
|
+
configSource: readText(manifest.dir, config)
|
|
1062
|
+
});
|
|
1063
|
+
return message === null ? [] : [{ file: config, message }];
|
|
1064
|
+
}
|
|
1016
1065
|
|
|
1017
1066
|
// src/core/mutation-result.ts
|
|
1018
1067
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -1243,8 +1292,8 @@ function judgeMutationReport(input) {
|
|
|
1243
1292
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
1244
1293
|
});
|
|
1245
1294
|
}
|
|
1246
|
-
const maxTimeouts = input.maxTimeouts ??
|
|
1247
|
-
if (
|
|
1295
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
1296
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
1248
1297
|
violations.push({
|
|
1249
1298
|
file: MANIFEST_FILE,
|
|
1250
1299
|
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
package/dist/index.d.cts
CHANGED
|
@@ -134,8 +134,8 @@ interface ResolvedSettings {
|
|
|
134
134
|
regex?: RegExp;
|
|
135
135
|
names?: string[];
|
|
136
136
|
}[];
|
|
137
|
-
/**
|
|
138
|
-
maxTimeouts: number
|
|
137
|
+
/** Timeouts tolerated outside `expectedTimeouts`. */
|
|
138
|
+
maxTimeouts: number;
|
|
139
139
|
}
|
|
140
140
|
/** One finding. Printed as `file:line: message`. */
|
|
141
141
|
interface Violation {
|
|
@@ -279,8 +279,8 @@ interface MutationVerdict {
|
|
|
279
279
|
violations: Violation[];
|
|
280
280
|
survivors: UnallowedSurvivor[];
|
|
281
281
|
/**
|
|
282
|
-
* Timed-out mutants that no `expectedTimeouts` entry covers.
|
|
283
|
-
* `maxTimeouts
|
|
282
|
+
* Timed-out mutants that no `expectedTimeouts` entry covers. When there are more of them
|
|
283
|
+
* than `maxTimeouts`, `violations` says so.
|
|
284
284
|
*/
|
|
285
285
|
unexpectedTimeouts: UnallowedSurvivor[];
|
|
286
286
|
summary: MutationSummary;
|
|
@@ -312,9 +312,9 @@ declare function judgeMutationReport(input: {
|
|
|
312
312
|
readSource: (gatePath: string) => string | undefined;
|
|
313
313
|
/**
|
|
314
314
|
* `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
|
|
315
|
-
* tolerated.
|
|
315
|
+
* tolerated. Absent means 0.
|
|
316
316
|
*/
|
|
317
|
-
maxTimeouts?: number
|
|
317
|
+
maxTimeouts?: number;
|
|
318
318
|
}): MutationVerdict;
|
|
319
319
|
/** `file:line:column / mutator / original → replacement / occurrence n of m`. */
|
|
320
320
|
declare function formatSurvivor(survivor: UnallowedSurvivor): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -134,8 +134,8 @@ interface ResolvedSettings {
|
|
|
134
134
|
regex?: RegExp;
|
|
135
135
|
names?: string[];
|
|
136
136
|
}[];
|
|
137
|
-
/**
|
|
138
|
-
maxTimeouts: number
|
|
137
|
+
/** Timeouts tolerated outside `expectedTimeouts`. */
|
|
138
|
+
maxTimeouts: number;
|
|
139
139
|
}
|
|
140
140
|
/** One finding. Printed as `file:line: message`. */
|
|
141
141
|
interface Violation {
|
|
@@ -279,8 +279,8 @@ interface MutationVerdict {
|
|
|
279
279
|
violations: Violation[];
|
|
280
280
|
survivors: UnallowedSurvivor[];
|
|
281
281
|
/**
|
|
282
|
-
* Timed-out mutants that no `expectedTimeouts` entry covers.
|
|
283
|
-
* `maxTimeouts
|
|
282
|
+
* Timed-out mutants that no `expectedTimeouts` entry covers. When there are more of them
|
|
283
|
+
* than `maxTimeouts`, `violations` says so.
|
|
284
284
|
*/
|
|
285
285
|
unexpectedTimeouts: UnallowedSurvivor[];
|
|
286
286
|
summary: MutationSummary;
|
|
@@ -312,9 +312,9 @@ declare function judgeMutationReport(input: {
|
|
|
312
312
|
readSource: (gatePath: string) => string | undefined;
|
|
313
313
|
/**
|
|
314
314
|
* `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
|
|
315
|
-
* tolerated.
|
|
315
|
+
* tolerated. Absent means 0.
|
|
316
316
|
*/
|
|
317
|
-
maxTimeouts?: number
|
|
317
|
+
maxTimeouts?: number;
|
|
318
318
|
}): MutationVerdict;
|
|
319
319
|
/** `file:line:column / mutator / original → replacement / occurrence n of m`. */
|
|
320
320
|
declare function formatSurvivor(survivor: UnallowedSurvivor): string;
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
readText,
|
|
21
21
|
resolveSettings,
|
|
22
22
|
validateEquivalentMutants
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-F7WBEFIL.js";
|
|
24
24
|
|
|
25
25
|
// src/commands/check.ts
|
|
26
26
|
import fs from "fs";
|
|
@@ -301,6 +301,21 @@ function scanStrykerConfig(configPath, source) {
|
|
|
301
301
|
return violations;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// src/core/vitest-coverage.ts
|
|
305
|
+
function vitestCoverageViolation(input) {
|
|
306
|
+
if (input.vitestVersion === null || input.vitestVersion === "") {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const major = Number(input.vitestVersion.split(".")[0]);
|
|
310
|
+
if (!Number.isInteger(major) || major >= 4) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (/\bistanbul\b/.test(input.configSource) || /\bexperimentalAstAwareRemapping\s*:\s*true\b/.test(input.configSource)) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return `Vitest ${input.vitestVersion} with the v8 coverage provider does not count the skipped side of an "if" without "else", so a gate can show 100% branches with that side untested. Use coverageProvider: 'istanbul' (needs @vitest/coverage-istanbul), set coverage.experimentalAstAwareRemapping: true, or move to Vitest 4`;
|
|
317
|
+
}
|
|
318
|
+
|
|
304
319
|
// src/commands/check.ts
|
|
305
320
|
var STRYKER_CONFIG_NAMES = [
|
|
306
321
|
"stryker.gates.config.mjs",
|
|
@@ -391,11 +406,40 @@ function checkGates(manifest) {
|
|
|
391
406
|
violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
|
|
392
407
|
}
|
|
393
408
|
}
|
|
409
|
+
violations.push(...checkVitestCoverage(manifest));
|
|
394
410
|
return violations;
|
|
395
411
|
}
|
|
396
412
|
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
397
413
|
(extension) => `vitest.gates.config.${extension}`
|
|
398
414
|
);
|
|
415
|
+
function installedVitestVersion(startDir) {
|
|
416
|
+
let current = startDir;
|
|
417
|
+
for (; ; ) {
|
|
418
|
+
try {
|
|
419
|
+
const pkg = JSON.parse(
|
|
420
|
+
fs.readFileSync(path.join(current, "node_modules", "vitest", "package.json"), "utf8")
|
|
421
|
+
);
|
|
422
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
423
|
+
} catch {
|
|
424
|
+
const parent = path.dirname(current);
|
|
425
|
+
if (parent === current) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
current = parent;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function checkVitestCoverage(manifest) {
|
|
433
|
+
const config = VITEST_GATE_CONFIGS.find((name) => fs.existsSync(path.join(manifest.dir, name)));
|
|
434
|
+
if (!config) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
const message = vitestCoverageViolation({
|
|
438
|
+
vitestVersion: installedVitestVersion(manifest.dir),
|
|
439
|
+
configSource: readText(manifest.dir, config)
|
|
440
|
+
});
|
|
441
|
+
return message === null ? [] : [{ file: config, message }];
|
|
442
|
+
}
|
|
399
443
|
|
|
400
444
|
// src/core/mutation-result.ts
|
|
401
445
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -626,8 +670,8 @@ function judgeMutationReport(input) {
|
|
|
626
670
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
627
671
|
});
|
|
628
672
|
}
|
|
629
|
-
const maxTimeouts = input.maxTimeouts ??
|
|
630
|
-
if (
|
|
673
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
674
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
631
675
|
violations.push({
|
|
632
676
|
file: MANIFEST_FILE,
|
|
633
677
|
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
package/dist/jest.cjs
CHANGED
|
@@ -212,8 +212,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
212
212
|
{
|
|
213
213
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
214
214
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
218
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
215
219
|
}
|
|
216
220
|
];
|
|
221
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
217
222
|
var SettingsError = class extends Error {
|
|
218
223
|
};
|
|
219
224
|
function isRecord2(value) {
|
|
@@ -475,7 +480,7 @@ function resolveSettings(raw) {
|
|
|
475
480
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
476
481
|
importMode: importsSection.mode ?? "blocklist",
|
|
477
482
|
importAllow,
|
|
478
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
483
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
479
484
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
480
485
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
481
486
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
package/dist/jest.js
CHANGED
package/dist/stryker.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/stryker.ts
|
|
31
31
|
var stryker_exports = {};
|
|
32
32
|
__export(stryker_exports, {
|
|
33
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
33
34
|
createStrykerGatesConfig: () => createStrykerGatesConfig
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(stryker_exports);
|
|
@@ -211,8 +212,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
211
212
|
{
|
|
212
213
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
213
214
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
218
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
214
219
|
}
|
|
215
220
|
];
|
|
221
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
216
222
|
var SettingsError = class extends Error {
|
|
217
223
|
};
|
|
218
224
|
function isRecord2(value) {
|
|
@@ -474,7 +480,7 @@ function resolveSettings(raw) {
|
|
|
474
480
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
475
481
|
importMode: importsSection.mode ?? "blocklist",
|
|
476
482
|
importAllow,
|
|
477
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
483
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
478
484
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
479
485
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
480
486
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -596,6 +602,7 @@ function resolveRootDir(rootDir) {
|
|
|
596
602
|
|
|
597
603
|
// src/stryker.ts
|
|
598
604
|
var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];
|
|
605
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
599
606
|
function createStrykerGatesConfig(options = {}) {
|
|
600
607
|
const { rootDir: rootDirOption, ...overrides } = options;
|
|
601
608
|
for (const option of WEAKENING_OPTIONS) {
|
|
@@ -620,6 +627,7 @@ function createStrykerGatesConfig(options = {}) {
|
|
|
620
627
|
incremental: true,
|
|
621
628
|
incrementalFile: "reports/stryker-incremental.json",
|
|
622
629
|
tempDirName: ".stryker-tmp",
|
|
630
|
+
timeoutMS: DEFAULT_TIMEOUT_MS,
|
|
623
631
|
clearTextReporter: { logTests: false, reportTests: false },
|
|
624
632
|
...overrides,
|
|
625
633
|
mutate: manifest.gates.map((gate) => gate.path),
|
|
@@ -630,5 +638,6 @@ function createStrykerGatesConfig(options = {}) {
|
|
|
630
638
|
}
|
|
631
639
|
// Annotate the CommonJS export names for ESM import in node:
|
|
632
640
|
0 && (module.exports = {
|
|
641
|
+
DEFAULT_TIMEOUT_MS,
|
|
633
642
|
createStrykerGatesConfig
|
|
634
643
|
});
|
package/dist/stryker.d.cts
CHANGED
|
@@ -4,6 +4,12 @@ interface StrykerGatesOptions {
|
|
|
4
4
|
/** Any other Stryker option: testRunner, jest / vitest, concurrency, ignorePatterns, … */
|
|
5
5
|
[option: string]: unknown;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Stryker's own default is 5000. After a mutant that really never ends, Stryker starts a new
|
|
9
|
+
* test runner, and the next mutant then pays that start-up inside its time limit: with 5000 a
|
|
10
|
+
* mutant that would be killed is reported as `Timeout`, which fails the run since 0.3.0.
|
|
11
|
+
*/
|
|
12
|
+
declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
13
|
/**
|
|
8
14
|
* Stryker config for the gates: mutate exactly the gates, never fail on the score
|
|
9
15
|
* (`thresholds.break: null`), and write the JSON report that `test-gates mutation` judges.
|
|
@@ -13,4 +19,4 @@ interface StrykerGatesOptions {
|
|
|
13
19
|
*/
|
|
14
20
|
declare function createStrykerGatesConfig(options?: StrykerGatesOptions): Record<string, unknown>;
|
|
15
21
|
|
|
16
|
-
export { type StrykerGatesOptions, createStrykerGatesConfig };
|
|
22
|
+
export { DEFAULT_TIMEOUT_MS, type StrykerGatesOptions, createStrykerGatesConfig };
|
package/dist/stryker.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ interface StrykerGatesOptions {
|
|
|
4
4
|
/** Any other Stryker option: testRunner, jest / vitest, concurrency, ignorePatterns, … */
|
|
5
5
|
[option: string]: unknown;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Stryker's own default is 5000. After a mutant that really never ends, Stryker starts a new
|
|
9
|
+
* test runner, and the next mutant then pays that start-up inside its time limit: with 5000 a
|
|
10
|
+
* mutant that would be killed is reported as `Timeout`, which fails the run since 0.3.0.
|
|
11
|
+
*/
|
|
12
|
+
declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
13
|
/**
|
|
8
14
|
* Stryker config for the gates: mutate exactly the gates, never fail on the score
|
|
9
15
|
* (`thresholds.break: null`), and write the JSON report that `test-gates mutation` judges.
|
|
@@ -13,4 +19,4 @@ interface StrykerGatesOptions {
|
|
|
13
19
|
*/
|
|
14
20
|
declare function createStrykerGatesConfig(options?: StrykerGatesOptions): Record<string, unknown>;
|
|
15
21
|
|
|
16
|
-
export { type StrykerGatesOptions, createStrykerGatesConfig };
|
|
22
|
+
export { DEFAULT_TIMEOUT_MS, type StrykerGatesOptions, createStrykerGatesConfig };
|
package/dist/stryker.js
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
} from "./chunk-4HAJIKDZ.js";
|
|
4
4
|
import {
|
|
5
5
|
loadGates
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-F7WBEFIL.js";
|
|
7
7
|
|
|
8
8
|
// src/stryker.ts
|
|
9
9
|
var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];
|
|
10
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
11
|
function createStrykerGatesConfig(options = {}) {
|
|
11
12
|
const { rootDir: rootDirOption, ...overrides } = options;
|
|
12
13
|
for (const option of WEAKENING_OPTIONS) {
|
|
@@ -31,6 +32,7 @@ function createStrykerGatesConfig(options = {}) {
|
|
|
31
32
|
incremental: true,
|
|
32
33
|
incrementalFile: "reports/stryker-incremental.json",
|
|
33
34
|
tempDirName: ".stryker-tmp",
|
|
35
|
+
timeoutMS: DEFAULT_TIMEOUT_MS,
|
|
34
36
|
clearTextReporter: { logTests: false, reportTests: false },
|
|
35
37
|
...overrides,
|
|
36
38
|
mutate: manifest.gates.map((gate) => gate.path),
|
|
@@ -40,5 +42,6 @@ function createStrykerGatesConfig(options = {}) {
|
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
export {
|
|
45
|
+
DEFAULT_TIMEOUT_MS,
|
|
43
46
|
createStrykerGatesConfig
|
|
44
47
|
};
|
package/dist/vitest.cjs
CHANGED
|
@@ -211,8 +211,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
211
211
|
{
|
|
212
212
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
213
213
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
217
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
214
218
|
}
|
|
215
219
|
];
|
|
220
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
216
221
|
var SettingsError = class extends Error {
|
|
217
222
|
};
|
|
218
223
|
function isRecord2(value) {
|
|
@@ -474,7 +479,7 @@ function resolveSettings(raw) {
|
|
|
474
479
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
475
480
|
importMode: importsSection.mode ?? "blocklist",
|
|
476
481
|
importAllow,
|
|
477
|
-
maxTimeouts: mutation.maxTimeouts ??
|
|
482
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
478
483
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
479
484
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
480
485
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
package/dist/vitest.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t09tanaka/test-gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
"$ref": "#/definitions/ruleList"
|
|
264
264
|
},
|
|
265
265
|
"forbiddenSource": {
|
|
266
|
-
"description": "Lines that must not appear in a gate (regular expressions, tested one line at a time). Default: 'use client' / 'use server'.",
|
|
266
|
+
"description": "Lines that must not appear in a gate (regular expressions, tested one line at a time). Default: 'use client' / 'use server', and process.env (since 0.3.0).",
|
|
267
267
|
"$ref": "#/definitions/ruleList"
|
|
268
268
|
},
|
|
269
269
|
"importAliases": {
|
|
@@ -397,7 +397,7 @@
|
|
|
397
397
|
"additionalProperties": false,
|
|
398
398
|
"properties": {
|
|
399
399
|
"maxTimeouts": {
|
|
400
|
-
"description": "How many timed-out mutants outside expectedTimeouts are tolerated. Above it the run fails.
|
|
400
|
+
"description": "How many timed-out mutants outside expectedTimeouts are tolerated. Above it the run fails. Default: 0 (since 0.3.0; before that, not set meant no limit).",
|
|
401
401
|
"type": "integer",
|
|
402
402
|
"minimum": 0
|
|
403
403
|
}
|