@t09tanaka/test-gates 0.3.2 → 0.3.3

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 CHANGED
@@ -1,11 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3
4
+
5
+ Documentation only; the commands and the helpers behave as in 0.3.2.
6
+
7
+ - The paths and outputs shown in the README are those of a small example written for it and of this repository's own gates.
8
+
3
9
  ## 0.3.2
4
10
 
5
11
  ### Changed
6
12
 
7
- - `createStrykerGatesConfig` sets Stryker's `maxTestRunnerReuse` to 50 unless the project passes its own (Stryker's default is 0: one test runner process for the whole run). In a project with mutants that never end, the runner became about three times slower from the first of them on, and mutants that are killed in seconds on their own timed out. Starting a new process every 50 mutants kept the speed: a full run of 781 mutants went from 14 to 18 minutes with stray timeouts to under 7 minutes with none.
8
- - `timeoutMS` is no longer set by the helper; Stryker's default (5000) applies again. The 30000 of 0.3.1 was the wrong fix. It rested on the idea that a restarted runner makes the next mutant slow, when in fact a runner that is not restarted stays slow. With 30000 the same full run took 47 minutes and still failed, and mutants that exhaust the heap crashed the runner and were reported as `RuntimeError` instead of `Timeout`.
13
+ - `createStrykerGatesConfig` sets Stryker's `maxTestRunnerReuse` to 50 unless the project passes its own (Stryker's default is 0: one test runner process for the whole run). In a project with mutants that never end, the runner became about three times slower from the first of them on, and mutants that are killed in seconds on their own timed out. Starting a new process every 50 mutants kept the speed: a full run of about 800 mutants went from about a quarter of an hour with stray timeouts to less than half of that with none.
14
+ - `timeoutMS` is no longer set by the helper; Stryker's default (5000) applies again. The 30000 of 0.3.1 was the wrong fix. It rested on the idea that a restarted runner makes the next mutant slow, when in fact a runner that is not restarted stays slow. With 30000 the same full run took about three times as long and still failed, and mutants that exhaust the heap crashed the runner and were reported as `RuntimeError` instead of `Timeout`.
9
15
 
10
16
  A project that set `timeoutMS` itself to work around stray timeouts can remove it.
11
17
 
@@ -13,7 +19,7 @@ A project that set `timeoutMS` itself to work around stray timeouts can remove i
13
19
 
14
20
  ### Changed
15
21
 
16
- - `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.
22
+ - `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 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.
17
23
 
18
24
  ## 0.3.0
19
25
 
@@ -44,7 +50,7 @@ A manifest that uses the new settings needs 0.2.0 or later, because 0.1.0 reject
44
50
  - `test-gates check` warns when the gate run uses Vitest below 4 with the v8 provider, which does not count the skipped side of an `if` without `else`.
45
51
  - `test-gates selfcheck` names the gates that import a gate whose spec could be left out without the gate failing.
46
52
  - CI for this repository (lint, tests, gates, negative control, full mutation run; build and tests on Node 20).
47
- - README: when the package is worth using, timeouts, allowlist mode, the Vitest v8 / istanbul measurements, cost and incremental mode, settings for Dart, and notes from migrating six repositories.
53
+ - README: when the package is worth using, timeouts, allowlist mode, the Vitest v8 / istanbul measurements, cost and incremental mode, settings for Dart, and notes from adopting it in several repositories.
48
54
 
49
55
  ### Changed
50
56
 
package/README.md CHANGED
@@ -44,22 +44,22 @@ Everything below is per subproject (the directory that has a `package.json`: `ba
44
44
  "$schema": "./node_modules/@t09tanaka/test-gates/schema/test-gates.schema.json",
45
45
  "gates": [
46
46
  {
47
- "path": "src/common/utils/money.ts",
48
- "decides": "Validation and rounding of amounts",
49
- "impact": "Wrong invoice and refund amounts"
47
+ "path": "src/shipping-fee.ts",
48
+ "decides": "The fee for sending a parcel, by weight, and its maximum",
49
+ "impact": "A heavy parcel is sent for too little, or the customer pays more than the advertised maximum"
50
50
  }
51
51
  ],
52
52
  "candidates": [
53
53
  {
54
- "path": "src/admin/guards/admin-role.guard.ts",
55
- "decides": "Who is an administrator",
54
+ "path": "src/warehouse/dispatch.guard.ts",
55
+ "decides": "Who may release an order for dispatch",
56
56
  "blocker": "Has decorators. The decision has to be extracted into a pure module"
57
57
  }
58
58
  ]
59
59
  }
60
60
  ```
61
61
 
62
- - `gates`: pure modules that hold an important decision. Each one needs a spec next to it (`money.spec.ts` or `money.test.ts`), 100% on statements, branches, functions and lines, and no surviving mutant outside its allow list.
62
+ - `gates`: pure modules that hold an important decision. Each one needs a spec next to it (`shipping-fee.spec.ts` or `shipping-fee.test.ts`), 100% on statements, branches, functions and lines, and no surviving mutant outside its allow list.
63
63
  - `candidates`: important logic that cannot be a gate yet, with what blocks it. Nothing is enforced for these except that the path exists.
64
64
  - `settings`: optional project conventions, see [Settings](#settings).
65
65
 
@@ -234,10 +234,10 @@ The run also fails when
234
234
 
235
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.
236
236
 
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:
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. For the gates of this repository:
238
238
 
239
239
  ```
240
- test-gates mutation: OK (mutants 317 / detected 308 (timeout 0) / allowed equivalent 8 / not evaluable 1 / unallowed survivors 0)
240
+ test-gates mutation: OK (mutants 1017 / detected 1017 (timeout 2) / allowed equivalent 0 / not evaluable 0 / unallowed survivors 0)
241
241
  ```
242
242
 
243
243
  #### Timeouts
@@ -280,9 +280,9 @@ How an `expectedTimeouts` entry is judged:
280
280
 
281
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
282
 
283
- `createStrykerGatesConfig` sets Stryker's `maxTestRunnerReuse` to 50 (since 0.3.2; Stryker's own default is 0, one test runner process for the whole run). A mutant that would be killed can also time out for a reason that has nothing to do with the mutant: in a Jest project with three mutants that never end, the test runner handled about a third as many mutants per minute from the first of them on, for the rest of the run. Mutants that are killed in seconds on their own then timed out, a different set on each full run, and the run took 14 to 18 minutes. With a new process every 50 mutants the speed stayed the same to the end, nothing timed out that should not, and the run took under 7 minutes. Why the runner slows down was not found; the restart only cuts it short. Pass `maxTestRunnerReuse` to the helper to change it.
283
+ `createStrykerGatesConfig` sets Stryker's `maxTestRunnerReuse` to 50 (since 0.3.2; Stryker's own default is 0, one test runner process for the whole run). A mutant that would be killed can also time out for a reason that has nothing to do with the mutant: in a Jest project with a few mutants that never end, the test runner handled about a third as many mutants per minute from the first of them on, for the rest of the run. Mutants that are killed in seconds on their own then timed out, a different set on each full run, and the run took about a quarter of an hour. With a new process every 50 mutants the speed stayed the same to the end, nothing timed out that should not, and the run took less than half as long. Why the runner slows down was not found; the restart only cuts it short. Pass `maxTestRunnerReuse` to the helper to change it.
284
284
 
285
- 0.3.1 set `timeoutMS` to 30000 instead. That was the wrong fix and 0.3.2 takes it back: the longer limit let most slow mutants finish, but the run above took 47 minutes, and in another project mutants that exhaust the heap crashed the test runner before the limit and came back as `RuntimeError`, which is only counted, not failed. `timeoutMS` is Stryker's default (5000) again unless the project sets it.
285
+ 0.3.1 set `timeoutMS` to 30000 instead. That was the wrong fix and 0.3.2 takes it back: the longer limit let most slow mutants finish, but the same full run then took about three times as long as without it, and in another project mutants that exhaust the heap crashed the test runner before the limit and came back as `RuntimeError`, which is only counted, not failed. `timeoutMS` is Stryker's default (5000) again unless the project sets it.
286
286
 
287
287
  ### `test-gates selfcheck [--all | --first] [-- <gate command>]`
288
288
 
@@ -290,17 +290,17 @@ Negative control. For each gate, runs the gate with that gate's spec left out an
290
290
 
291
291
  The spec to leave out is passed in the environment variable `TEST_GATES_EXCLUDE_SPEC`. The config helpers read it. The gate command is `jest --config jest.gates.config.*` or `vitest run --config vitest.gates.config.*`, whichever config exists; override it with `settings.gateCommand` or after `--`. `--all` (default) checks every gate in turn, `--first` only the first.
292
292
 
293
- **When a gate imports another gate.** If `api-key-auth.ts` imports `safe-compare.ts` and both are gates, the spec of `api-key-auth` executes every line of `safe-compare` as well. Leaving out `safe-compare.spec.ts` then does not lower its coverage, and `selfcheck` fails for `safe-compare` ("the gate passed although the spec … was left out"), naming the importing gate in a hint. The coverage floor of `safe-compare` is being held up by somebody else's spec. Fix it in the importer's spec: mock the imported gate and assert **how it is called**, so that each spec covers only its own gate:
293
+ **When a gate imports another gate.** If `delivery-price.ts` imports `shipping-fee.ts` and both are gates, the spec of `delivery-price` executes every line of `shipping-fee` as well. Leaving out `shipping-fee.spec.ts` then does not lower its coverage, and `selfcheck` fails for `shipping-fee` ("the gate passed although the spec … was left out"), naming the importing gate in a hint. The coverage floor of `shipping-fee` is being held up by somebody else's spec. Fix it in the importer's spec: mock the imported gate and assert **how it is called**, so that each spec covers only its own gate:
294
294
 
295
295
  ```ts
296
- jest.mock('./safe-compare');
296
+ jest.mock('./shipping-fee');
297
297
  // …
298
- expect(safeCompare).toHaveBeenCalledWith(presentedKey, storedKey); // right arguments, right order
298
+ expect(shippingFeeCents).toHaveBeenCalledWith(weightGrams); // called with the weight, not with the price
299
299
  ```
300
300
 
301
301
  ### `test-gates lcov --file <lcov.info>`
302
302
 
303
- For stacks without mutation testing (Flutter / Dart): requires `LH == LF` for every gate in an lcov tracefile. A gate without a record, or with no instrumented line, fails. Paths are compared as written in the tracefile (`lib/models/coupon.dart`); absolute paths under the subproject are made relative.
303
+ For stacks without mutation testing (Flutter / Dart): requires `LH == LF` for every gate in an lcov tracefile. A gate without a record, or with no instrumented line, fails. Paths are compared as written in the tracefile (`lib/shipping_fee.dart`); absolute paths under the subproject are made relative.
304
304
 
305
305
  ```bash
306
306
  flutter test --coverage && npx test-gates lcov --file coverage/lcov.info
@@ -310,7 +310,7 @@ Settings for a Dart package that uses only `lcov`:
310
310
 
311
311
  ```json
312
312
  {
313
- "gates": [{ "path": "lib/models/coupon.dart", "decides": "…", "impact": "…" }],
313
+ "gates": [{ "path": "lib/shipping_fee.dart", "decides": "…", "impact": "…" }],
314
314
  "candidates": [],
315
315
  "settings": {
316
316
  "gateExtensions": [".dart"],
@@ -326,19 +326,47 @@ Lines are counted from the `DA:` entries, not taken from `LH:` / `LF:`. A tracef
326
326
 
327
327
  ## Allowing an equivalent mutant
328
328
 
329
- An equivalent mutant changes the code without changing anything observable: same return value, same exception, same side effects, for every input. Only those may be allowed, in `test-gates.json`, with the reason:
329
+ An equivalent mutant changes the code without changing anything observable: same return value, same exception, same side effects, for every input. Only those may be allowed, in `test-gates.json`, with the reason.
330
+
331
+ An example. A gate limits a fee to a maximum:
332
+
333
+ ```ts
334
+ let fee = Math.ceil(weightGrams / 1000) * 500;
335
+ if (fee > MAX_FEE_CENTS) {
336
+ fee = MAX_FEE_CENTS;
337
+ }
338
+ return fee;
339
+ ```
340
+
341
+ The specs check every boundary, and one mutant still survives:
342
+
343
+ ```
344
+ test-gates mutation: 1 surviving mutant(s) not in the allow list
345
+ - src/shipping-fee.ts:9:7 / EqualityOperator / fee > MAX_FEE_CENTS → fee >= MAX_FEE_CENTS (Survived)
346
+
347
+ Fix the test so that it detects the mutant. Only when the mutant cannot be observed from
348
+ outside (same return value, exception and side effects for every input), add it to
349
+ "equivalentMutants" of the gate in test-gates.json and fill in "reason":
350
+
351
+ src/shipping-fee.ts
352
+ {"mutator":"EqualityOperator","original":"fee > MAX_FEE_CENTS","replacement":"fee >= MAX_FEE_CENTS","reason":""}
353
+
354
+ test-gates mutation: FAILED (mutants 14 / detected 13 (timeout 0) / allowed equivalent 0 / not evaluable 0 / unallowed survivors 1)
355
+ ```
356
+
357
+ With `>=` the only input that is treated differently is a fee equal to the maximum, and that fee is set to the value it already has. No test can tell the two apart, so the gate lists the mutant:
330
358
 
331
359
  ```json
332
360
  {
333
- "path": "src/lib/auth/safe-compare.ts",
334
- "decides": "Constant-time comparison of tokens",
335
- "impact": "Token comparison leaks timing or accepts a wrong token",
361
+ "path": "src/shipping-fee.ts",
362
+ "decides": "The fee for sending a parcel, by weight, and its maximum",
363
+ "impact": "A heavy parcel is sent for too little, or the customer pays more than the advertised maximum",
336
364
  "equivalentMutants": [
337
365
  {
338
- "mutator": "ConditionalExpression",
339
- "original": "a.length !== b.length",
340
- "replacement": "false",
341
- "reason": "timingSafeEqual below throws on a length mismatch and the catch returns false, so the result is the same"
366
+ "mutator": "EqualityOperator",
367
+ "original": "fee > MAX_FEE_CENTS",
368
+ "replacement": "fee >= MAX_FEE_CENTS",
369
+ "reason": "A fee that is equal to the maximum is set to the maximum, which is the value it has"
342
370
  }
343
371
  ]
344
372
  }
@@ -392,17 +420,17 @@ Project-specific layers go into `add`:
392
420
  "impureImports": {
393
421
  "add": [
394
422
  {
395
- "pattern": "(^|/)(repository|wrapper|handler)(/|$)",
396
- "reason": "DB / external API layer"
423
+ "pattern": "(^|/)(persistence|gateways)(/|$)",
424
+ "reason": "talks to the database or to a remote API"
397
425
  },
398
- { "pattern": "\\.(service|guard|controller|module)$", "reason": "class managed by DI" }
426
+ { "pattern": "\\.(service|controller)$", "reason": "class created by the DI container" }
399
427
  ]
400
428
  },
401
429
  "impurePaths": {
402
430
  "add": [
403
431
  {
404
- "pattern": "\\.(service|guard|controller|module|dto)\\.ts$",
405
- "reason": "NestJS class file"
432
+ "pattern": "\\.(service|controller|module)\\.ts$",
433
+ "reason": "a class with decorators"
406
434
  }
407
435
  ]
408
436
  }
@@ -6,7 +6,7 @@ interface StrykerGatesOptions {
6
6
  }
7
7
  /**
8
8
  * Stryker's own default is 0: one test runner process for the whole run. Measured on a Jest
9
- * project with three mutants that never end (`Hit limit reached`): from the first of them on
9
+ * project with a few mutants that never end (`Hit limit reached`): from the first of them on
10
10
  * the runner handled about a third as many mutants per minute for the rest of the run, and
11
11
  * mutants that are killed in seconds on their own ran out of time. With a new process every 50
12
12
  * mutants the speed stayed the same to the end.
package/dist/stryker.d.ts CHANGED
@@ -6,7 +6,7 @@ interface StrykerGatesOptions {
6
6
  }
7
7
  /**
8
8
  * Stryker's own default is 0: one test runner process for the whole run. Measured on a Jest
9
- * project with three mutants that never end (`Hit limit reached`): from the first of them on
9
+ * project with a few mutants that never end (`Hit limit reached`): from the first of them on
10
10
  * the runner handled about a third as many mutants per minute for the rest of the run, and
11
11
  * mutants that are killed in seconds on their own ran out of time. With a new process every 50
12
12
  * mutants the speed stayed the same to the end.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t09tanaka/test-gates",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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",