@t09tanaka/test-gates 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -514,6 +514,24 @@ function resolveSettings(raw) {
514
514
 
515
515
  // src/core/selfcheck.ts
516
516
  var EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
517
+ var EXCLUDE_ALL_ENV = "TEST_GATES_EXCLUDE_SPECS";
518
+ function excludedSpecs(env) {
519
+ const one = env[EXCLUDE_ENV];
520
+ const several = env[EXCLUDE_ALL_ENV];
521
+ const excluded = one === void 0 || one === "" ? [] : [one];
522
+ if (several === void 0 || several === "") {
523
+ return excluded;
524
+ }
525
+ let listed = null;
526
+ try {
527
+ listed = JSON.parse(several);
528
+ } catch {
529
+ }
530
+ if (!Array.isArray(listed) || !listed.every((spec) => typeof spec === "string")) {
531
+ throw new Error(`test-gates: ${EXCLUDE_ALL_ENV} must be a JSON list of spec paths`);
532
+ }
533
+ return [...excluded, ...listed];
534
+ }
517
535
  var THRESHOLD_MESSAGE = /threshold|coverage data for/i;
518
536
  function escapeRegExp(text) {
519
537
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -561,19 +579,22 @@ function looksLikeSpec(gatePath, settings) {
561
579
  // src/fs.ts
562
580
  var import_node_fs = __toESM(require("fs"), 1);
563
581
  var import_node_path = __toESM(require("path"), 1);
564
- function existsExact(rootDir, relativePath, kind = "file") {
582
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
565
583
  let current = rootDir;
566
584
  for (const segment of relativePath.split("/")) {
567
585
  if (segment === "" || segment === ".") {
568
586
  continue;
569
587
  }
570
- let entries;
571
- try {
572
- entries = import_node_fs.default.readdirSync(current);
573
- } catch {
574
- return false;
588
+ let entries = listing.get(current);
589
+ if (entries === void 0) {
590
+ try {
591
+ entries = import_node_fs.default.readdirSync(current);
592
+ } catch {
593
+ entries = null;
594
+ }
595
+ listing.set(current, entries);
575
596
  }
576
- if (!entries.includes(segment)) {
597
+ if (entries === null || !entries.includes(segment)) {
577
598
  return false;
578
599
  }
579
600
  current = import_node_path.default.join(current, segment);
@@ -611,9 +632,12 @@ function readManifest(dir) {
611
632
  }
612
633
  throw error;
613
634
  }
635
+ const listing = /* @__PURE__ */ new Map();
614
636
  const gates = parsed.gates.map((gate) => {
615
637
  const specCandidates = specCandidatesFor(gate.path, settings);
616
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
638
+ const existingSpecs = specCandidates.filter(
639
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
640
+ );
617
641
  return {
618
642
  ...gate,
619
643
  specCandidates,
@@ -640,8 +664,8 @@ function loadGates(dir = process.cwd()) {
640
664
  return manifest;
641
665
  }
642
666
  function gateSpecs(manifest, env = process.env) {
643
- const excluded = env[EXCLUDE_ENV];
644
- return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec !== excluded);
667
+ const excluded = excludedSpecs(env);
668
+ return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && !excluded.includes(spec));
645
669
  }
646
670
 
647
671
  // src/commands/check.ts
@@ -957,6 +981,7 @@ function isPlainRelativePath(file) {
957
981
  function checkGates(manifest) {
958
982
  const { dir, settings } = manifest;
959
983
  const violations = [...manifest.violations];
984
+ const listing = /* @__PURE__ */ new Map();
960
985
  for (const gate of manifest.gates) {
961
986
  if (!isPlainRelativePath(gate.path)) {
962
987
  violations.push({
@@ -984,7 +1009,7 @@ function checkGates(manifest) {
984
1009
  });
985
1010
  }
986
1011
  }
987
- if (!existsExact(dir, gate.path)) {
1012
+ if (!existsExact(dir, gate.path, "file", listing)) {
988
1013
  violations.push({
989
1014
  file: gate.path,
990
1015
  message: "file not found (the check is case-sensitive)"
@@ -1013,7 +1038,7 @@ function checkGates(manifest) {
1013
1038
  violations.push(...scanSpecSource(gate.path, specPath, readText(dir, specPath), settings));
1014
1039
  }
1015
1040
  for (const candidate of manifest.candidates) {
1016
- if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any")) {
1041
+ if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any", listing)) {
1017
1042
  violations.push({
1018
1043
  file: MANIFEST_FILE,
1019
1044
  message: `${candidate.path}: candidate not found. Remove it, or fix the path`
package/dist/index.d.cts CHANGED
@@ -233,6 +233,10 @@ interface ReportMutant {
233
233
  replacement?: string;
234
234
  status: string;
235
235
  location: ReportLocation;
236
+ /** Ids of the tests that ran the mutated code (`coverageAnalysis: perTest`). */
237
+ coveredBy?: string[];
238
+ /** Ids of the tests that failed with the mutant. */
239
+ killedBy?: string[];
236
240
  }
237
241
  interface ReportFile {
238
242
  source?: string;
@@ -241,6 +245,12 @@ interface ReportFile {
241
245
  /** The part of the mutation-testing-elements JSON report that the verdict depends on. */
242
246
  interface MutationReport {
243
247
  files?: Record<string, ReportFile>;
248
+ /** The spec files, with the ids of their tests (read by `mutation --affected`). */
249
+ testFiles?: Record<string, {
250
+ tests?: Array<{
251
+ id: string;
252
+ }>;
253
+ }>;
244
254
  }
245
255
  /** A survivor that no allowance covers. `allowance` can be pasted into `equivalentMutants`. */
246
256
  interface UnallowedSurvivor {
package/dist/index.d.ts CHANGED
@@ -233,6 +233,10 @@ interface ReportMutant {
233
233
  replacement?: string;
234
234
  status: string;
235
235
  location: ReportLocation;
236
+ /** Ids of the tests that ran the mutated code (`coverageAnalysis: perTest`). */
237
+ coveredBy?: string[];
238
+ /** Ids of the tests that failed with the mutant. */
239
+ killedBy?: string[];
236
240
  }
237
241
  interface ReportFile {
238
242
  source?: string;
@@ -241,6 +245,12 @@ interface ReportFile {
241
245
  /** The part of the mutation-testing-elements JSON report that the verdict depends on. */
242
246
  interface MutationReport {
243
247
  files?: Record<string, ReportFile>;
248
+ /** The spec files, with the ids of their tests (read by `mutation --affected`). */
249
+ testFiles?: Record<string, {
250
+ tests?: Array<{
251
+ id: string;
252
+ }>;
253
+ }>;
244
254
  }
245
255
  /** A survivor that no allowance covers. `allowance` can be pasted into `equivalentMutants`. */
246
256
  interface UnallowedSurvivor {
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  readText,
21
21
  resolveSettings,
22
22
  validateEquivalentMutants
23
- } from "./chunk-F7WBEFIL.js";
23
+ } from "./chunk-AK2NTFQO.js";
24
24
 
25
25
  // src/commands/check.ts
26
26
  import fs from "fs";
@@ -335,6 +335,7 @@ function isPlainRelativePath(file) {
335
335
  function checkGates(manifest) {
336
336
  const { dir, settings } = manifest;
337
337
  const violations = [...manifest.violations];
338
+ const listing = /* @__PURE__ */ new Map();
338
339
  for (const gate of manifest.gates) {
339
340
  if (!isPlainRelativePath(gate.path)) {
340
341
  violations.push({
@@ -362,7 +363,7 @@ function checkGates(manifest) {
362
363
  });
363
364
  }
364
365
  }
365
- if (!existsExact(dir, gate.path)) {
366
+ if (!existsExact(dir, gate.path, "file", listing)) {
366
367
  violations.push({
367
368
  file: gate.path,
368
369
  message: "file not found (the check is case-sensitive)"
@@ -391,7 +392,7 @@ function checkGates(manifest) {
391
392
  violations.push(...scanSpecSource(gate.path, specPath, readText(dir, specPath), settings));
392
393
  }
393
394
  for (const candidate of manifest.candidates) {
394
- if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any")) {
395
+ if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any", listing)) {
395
396
  violations.push({
396
397
  file: MANIFEST_FILE,
397
398
  message: `${candidate.path}: candidate not found. Remove it, or fix the path`
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/jest-touch-reporter.ts
31
+ var jest_touch_reporter_exports = {};
32
+ __export(jest_touch_reporter_exports, {
33
+ default: () => TouchReporter
34
+ });
35
+ module.exports = __toCommonJS(jest_touch_reporter_exports);
36
+ var import_node_module = require("module");
37
+ var import_node_path2 = __toESM(require("path"), 1);
38
+
39
+ // src/touch.ts
40
+ var import_node_crypto = require("crypto");
41
+ var import_node_fs = __toESM(require("fs"), 1);
42
+ var import_node_path = __toESM(require("path"), 1);
43
+ var import_node_url = require("url");
44
+ var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
45
+ function isObject(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function countersOf(coverage) {
49
+ return {
50
+ s: Object.values(coverage.s ?? {}),
51
+ f: Object.values(coverage.f ?? {}),
52
+ b: Object.values(coverage.b ?? {}).flat()
53
+ };
54
+ }
55
+ function countersOfAll(coverage) {
56
+ if (!isObject(coverage)) {
57
+ return null;
58
+ }
59
+ const files = {};
60
+ for (const [file, counters] of Object.entries(coverage)) {
61
+ if (!isObject(counters)) {
62
+ return null;
63
+ }
64
+ files[file] = countersOf(counters);
65
+ }
66
+ return files;
67
+ }
68
+ function add(before, now) {
69
+ return before.length === now.length ? before.map((count, index) => count + (now[index] ?? 0)) : [-1];
70
+ }
71
+ function writeSpecNote(directory, spec, coverage) {
72
+ const files = countersOfAll(coverage) ?? {};
73
+ const specs = import_node_path.default.join(directory, "specs");
74
+ import_node_fs.default.mkdirSync(specs, { recursive: true });
75
+ const file = import_node_path.default.join(specs, `${(0, import_node_crypto.createHash)("sha256").update(spec).digest("hex")}.json`);
76
+ let before = {};
77
+ try {
78
+ before = JSON.parse(import_node_fs.default.readFileSync(file, "utf8")).files;
79
+ } catch {
80
+ }
81
+ for (const [name, counters] of Object.entries(before)) {
82
+ const now = files[name];
83
+ files[name] = now === void 0 ? counters : { s: add(counters.s, now.s), f: add(counters.f, now.f), b: add(counters.b, now.b) };
84
+ }
85
+ const note = { spec, files };
86
+ import_node_fs.default.writeFileSync(file, JSON.stringify(note));
87
+ }
88
+ function writeRunInfo(directory, run) {
89
+ import_node_fs.default.mkdirSync(directory, { recursive: true });
90
+ import_node_fs.default.writeFileSync(import_node_path.default.join(directory, "run.json"), JSON.stringify(run));
91
+ }
92
+
93
+ // src/jest-touch-reporter.ts
94
+ function jestVersion(rootDir) {
95
+ for (const name of ["jest/package.json", "@jest/core/package.json"]) {
96
+ try {
97
+ const from = (0, import_node_module.createRequire)(import_node_path2.default.join(typeof rootDir === "string" ? rootDir : ".", "x.js"));
98
+ const { version } = from(name);
99
+ if (typeof version === "string") {
100
+ return version;
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ return "unknown";
106
+ }
107
+ var TouchReporter = class {
108
+ constructor(globalConfig = {}) {
109
+ const directory = process.env[TOUCH_ENV];
110
+ if (directory) {
111
+ writeRunInfo(directory, {
112
+ runner: "jest",
113
+ version: jestVersion(globalConfig.rootDir),
114
+ provider: typeof globalConfig.coverageProvider === "string" ? globalConfig.coverageProvider : "unknown",
115
+ pool: "default",
116
+ isolate: true,
117
+ // Without the `projects` option the list is empty, or names the one root.
118
+ projects: Array.isArray(globalConfig.projects) ? Math.max(1, globalConfig.projects.length) : 0,
119
+ problem: null
120
+ });
121
+ }
122
+ }
123
+ onTestResult(_test, result) {
124
+ const directory = process.env[TOUCH_ENV];
125
+ if (directory) {
126
+ writeSpecNote(directory, result.testFilePath, result.coverage);
127
+ }
128
+ }
129
+ };
@@ -0,0 +1,19 @@
1
+ interface GlobalConfig {
2
+ rootDir?: unknown;
3
+ coverageProvider?: unknown;
4
+ projects?: unknown;
5
+ }
6
+ /**
7
+ * Jest reporter that `createJestGatesConfig` adds while `test-gates selfcheck` asks for it:
8
+ * writes down what each spec file executed, from the coverage Jest itself collected for that
9
+ * file, and how the run ran.
10
+ */
11
+ declare class TouchReporter {
12
+ constructor(globalConfig?: GlobalConfig);
13
+ onTestResult(_test: unknown, result: {
14
+ testFilePath: string;
15
+ coverage?: unknown;
16
+ }): void;
17
+ }
18
+
19
+ export { TouchReporter as default };
@@ -0,0 +1,19 @@
1
+ interface GlobalConfig {
2
+ rootDir?: unknown;
3
+ coverageProvider?: unknown;
4
+ projects?: unknown;
5
+ }
6
+ /**
7
+ * Jest reporter that `createJestGatesConfig` adds while `test-gates selfcheck` asks for it:
8
+ * writes down what each spec file executed, from the coverage Jest itself collected for that
9
+ * file, and how the run ran.
10
+ */
11
+ declare class TouchReporter {
12
+ constructor(globalConfig?: GlobalConfig);
13
+ onTestResult(_test: unknown, result: {
14
+ testFilePath: string;
15
+ coverage?: unknown;
16
+ }): void;
17
+ }
18
+
19
+ export { TouchReporter as default };
@@ -0,0 +1,48 @@
1
+ import {
2
+ TOUCH_ENV,
3
+ writeRunInfo,
4
+ writeSpecNote
5
+ } from "./chunk-TOKHAUFX.js";
6
+
7
+ // src/jest-touch-reporter.ts
8
+ import { createRequire } from "module";
9
+ import path from "path";
10
+ function jestVersion(rootDir) {
11
+ for (const name of ["jest/package.json", "@jest/core/package.json"]) {
12
+ try {
13
+ const from = createRequire(path.join(typeof rootDir === "string" ? rootDir : ".", "x.js"));
14
+ const { version } = from(name);
15
+ if (typeof version === "string") {
16
+ return version;
17
+ }
18
+ } catch {
19
+ }
20
+ }
21
+ return "unknown";
22
+ }
23
+ var TouchReporter = class {
24
+ constructor(globalConfig = {}) {
25
+ const directory = process.env[TOUCH_ENV];
26
+ if (directory) {
27
+ writeRunInfo(directory, {
28
+ runner: "jest",
29
+ version: jestVersion(globalConfig.rootDir),
30
+ provider: typeof globalConfig.coverageProvider === "string" ? globalConfig.coverageProvider : "unknown",
31
+ pool: "default",
32
+ isolate: true,
33
+ // Without the `projects` option the list is empty, or names the one root.
34
+ projects: Array.isArray(globalConfig.projects) ? Math.max(1, globalConfig.projects.length) : 0,
35
+ problem: null
36
+ });
37
+ }
38
+ }
39
+ onTestResult(_test, result) {
40
+ const directory = process.env[TOUCH_ENV];
41
+ if (directory) {
42
+ writeSpecNote(directory, result.testFilePath, result.coverage);
43
+ }
44
+ }
45
+ };
46
+ export {
47
+ TouchReporter as default
48
+ };
package/dist/jest.cjs CHANGED
@@ -33,7 +33,13 @@ __export(jest_exports, {
33
33
  createJestGatesConfig: () => createJestGatesConfig
34
34
  });
35
35
  module.exports = __toCommonJS(jest_exports);
36
- var import_node_path4 = __toESM(require("path"), 1);
36
+
37
+ // node_modules/tsup/assets/cjs_shims.js
38
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
40
+
41
+ // src/jest.ts
42
+ var import_node_path5 = __toESM(require("path"), 1);
37
43
 
38
44
  // src/load.ts
39
45
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -490,6 +496,24 @@ function resolveSettings(raw) {
490
496
 
491
497
  // src/core/selfcheck.ts
492
498
  var EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
499
+ var EXCLUDE_ALL_ENV = "TEST_GATES_EXCLUDE_SPECS";
500
+ function excludedSpecs(env) {
501
+ const one = env[EXCLUDE_ENV];
502
+ const several = env[EXCLUDE_ALL_ENV];
503
+ const excluded = one === void 0 || one === "" ? [] : [one];
504
+ if (several === void 0 || several === "") {
505
+ return excluded;
506
+ }
507
+ let listed = null;
508
+ try {
509
+ listed = JSON.parse(several);
510
+ } catch {
511
+ }
512
+ if (!Array.isArray(listed) || !listed.every((spec) => typeof spec === "string")) {
513
+ throw new Error(`test-gates: ${EXCLUDE_ALL_ENV} must be a JSON list of spec paths`);
514
+ }
515
+ return [...excluded, ...listed];
516
+ }
493
517
 
494
518
  // src/core/spec-path.ts
495
519
  function gateExtensionOf(gatePath, gateExtensions) {
@@ -514,19 +538,22 @@ function specCandidatesFor(gatePath, settings) {
514
538
  // src/fs.ts
515
539
  var import_node_fs = __toESM(require("fs"), 1);
516
540
  var import_node_path = __toESM(require("path"), 1);
517
- function existsExact(rootDir, relativePath, kind = "file") {
541
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
518
542
  let current = rootDir;
519
543
  for (const segment of relativePath.split("/")) {
520
544
  if (segment === "" || segment === ".") {
521
545
  continue;
522
546
  }
523
- let entries;
524
- try {
525
- entries = import_node_fs.default.readdirSync(current);
526
- } catch {
527
- return false;
547
+ let entries = listing.get(current);
548
+ if (entries === void 0) {
549
+ try {
550
+ entries = import_node_fs.default.readdirSync(current);
551
+ } catch {
552
+ entries = null;
553
+ }
554
+ listing.set(current, entries);
528
555
  }
529
- if (!entries.includes(segment)) {
556
+ if (entries === null || !entries.includes(segment)) {
530
557
  return false;
531
558
  }
532
559
  current = import_node_path.default.join(current, segment);
@@ -561,9 +588,12 @@ function readManifest(dir) {
561
588
  }
562
589
  throw error;
563
590
  }
591
+ const listing = /* @__PURE__ */ new Map();
564
592
  const gates = parsed.gates.map((gate) => {
565
593
  const specCandidates = specCandidatesFor(gate.path, settings);
566
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
594
+ const existingSpecs = specCandidates.filter(
595
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
596
+ );
567
597
  return {
568
598
  ...gate,
569
599
  specCandidates,
@@ -590,8 +620,8 @@ function loadGates(dir = process.cwd()) {
590
620
  return manifest;
591
621
  }
592
622
  function gateSpecs(manifest, env = process.env) {
593
- const excluded = env[EXCLUDE_ENV];
594
- return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec !== excluded);
623
+ const excluded = excludedSpecs(env);
624
+ return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && !excluded.includes(spec));
595
625
  }
596
626
  var NO_SPEC_PATTERN = "__test_gates_no_spec_left__";
597
627
 
@@ -609,6 +639,21 @@ function resolveRootDir(rootDir) {
609
639
  return import_node_path3.default.resolve(rootDir);
610
640
  }
611
641
 
642
+ // src/touch.ts
643
+ var import_node_crypto = require("crypto");
644
+ var import_node_fs3 = __toESM(require("fs"), 1);
645
+ var import_node_path4 = __toESM(require("path"), 1);
646
+ var import_node_url2 = require("url");
647
+ var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
648
+ function jestReporterFile(moduleUrl) {
649
+ const directory = import_node_path4.default.dirname((0, import_node_url2.fileURLToPath)(moduleUrl));
650
+ const found = [".cjs", ".ts"].map((extension) => import_node_path4.default.join(directory, `jest-touch-reporter${extension}`)).find((file) => import_node_fs3.default.existsSync(file));
651
+ if (found === void 0) {
652
+ throw new Error(`test-gates: jest-touch-reporter is missing in ${directory}`);
653
+ }
654
+ return found;
655
+ }
656
+
612
657
  // src/jest.ts
613
658
  function createJestGatesConfig(options = {}) {
614
659
  const { rootDir: rootDirOption, ...overrides } = options;
@@ -620,6 +665,19 @@ function createJestGatesConfig(options = {}) {
620
665
  coverageReporters: ["text"],
621
666
  coverageDirectory: "<rootDir>/coverage/gates",
622
667
  ...overrides,
668
+ // While `test-gates selfcheck` asks for it: write down what each spec executed.
669
+ ...process.env[TOUCH_ENV] ? {
670
+ reporters: [
671
+ ...Array.isArray(overrides.reporters) ? overrides.reporters : ["default"],
672
+ jestReporterFile(importMetaUrl)
673
+ ],
674
+ // The runner's own total, to check the notes against.
675
+ coverageReporters: [
676
+ ...Array.isArray(overrides.coverageReporters) ? overrides.coverageReporters : ["text"],
677
+ "json"
678
+ ],
679
+ coverageDirectory: import_node_path5.default.join(process.env[TOUCH_ENV], "coverage")
680
+ } : {},
623
681
  rootDir,
624
682
  // Never an empty array: Jest would fall back to its default pattern and run every test.
625
683
  testMatch: (specs.length > 0 ? specs : [NO_SPEC_PATTERN]).map((spec) => `<rootDir>/${spec}`),
@@ -630,7 +688,7 @@ function createJestGatesConfig(options = {}) {
630
688
  coverageProvider: "babel",
631
689
  collectCoverageFrom: manifest.gates.map((gate) => `<rootDir>/${gate.path}`),
632
690
  coverageThreshold: Object.fromEntries(
633
- manifest.gates.map((gate) => [import_node_path4.default.join(rootDir, gate.path), { ...FULL_COVERAGE }])
691
+ manifest.gates.map((gate) => [import_node_path5.default.join(rootDir, gate.path), { ...FULL_COVERAGE }])
634
692
  )
635
693
  };
636
694
  }
package/dist/jest.js CHANGED
@@ -1,12 +1,16 @@
1
+ import {
2
+ TOUCH_ENV,
3
+ jestReporterFile
4
+ } from "./chunk-TOKHAUFX.js";
1
5
  import {
2
6
  FULL_COVERAGE,
3
7
  resolveRootDir
4
- } from "./chunk-4HAJIKDZ.js";
8
+ } from "./chunk-C4R3RDHF.js";
5
9
  import {
6
10
  NO_SPEC_PATTERN,
7
11
  gateSpecs,
8
12
  loadGates
9
- } from "./chunk-F7WBEFIL.js";
13
+ } from "./chunk-AK2NTFQO.js";
10
14
 
11
15
  // src/jest.ts
12
16
  import path from "path";
@@ -20,6 +24,19 @@ function createJestGatesConfig(options = {}) {
20
24
  coverageReporters: ["text"],
21
25
  coverageDirectory: "<rootDir>/coverage/gates",
22
26
  ...overrides,
27
+ // While `test-gates selfcheck` asks for it: write down what each spec executed.
28
+ ...process.env[TOUCH_ENV] ? {
29
+ reporters: [
30
+ ...Array.isArray(overrides.reporters) ? overrides.reporters : ["default"],
31
+ jestReporterFile(import.meta.url)
32
+ ],
33
+ // The runner's own total, to check the notes against.
34
+ coverageReporters: [
35
+ ...Array.isArray(overrides.coverageReporters) ? overrides.coverageReporters : ["text"],
36
+ "json"
37
+ ],
38
+ coverageDirectory: path.join(process.env[TOUCH_ENV], "coverage")
39
+ } : {},
23
40
  rootDir,
24
41
  // Never an empty array: Jest would fall back to its default pattern and run every test.
25
42
  testMatch: (specs.length > 0 ? specs : [NO_SPEC_PATTERN]).map((spec) => `<rootDir>/${spec}`),
package/dist/stryker.cjs CHANGED
@@ -31,9 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var stryker_exports = {};
32
32
  __export(stryker_exports, {
33
33
  DEFAULT_MAX_TEST_RUNNER_REUSE: () => DEFAULT_MAX_TEST_RUNNER_REUSE,
34
- createStrykerGatesConfig: () => createStrykerGatesConfig
34
+ createStrykerGatesConfig: () => createStrykerGatesConfig,
35
+ defaultConcurrency: () => defaultConcurrency
35
36
  });
36
37
  module.exports = __toCommonJS(stryker_exports);
38
+ var import_node_os = __toESM(require("os"), 1);
39
+
40
+ // src/core/selfcheck.ts
41
+ function inCi(env) {
42
+ const ci = (env.CI ?? "").trim().toLowerCase();
43
+ return ci !== "" && ci !== "false" && ci !== "0";
44
+ }
37
45
 
38
46
  // src/load.ts
39
47
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -511,19 +519,22 @@ function specCandidatesFor(gatePath, settings) {
511
519
  // src/fs.ts
512
520
  var import_node_fs = __toESM(require("fs"), 1);
513
521
  var import_node_path = __toESM(require("path"), 1);
514
- function existsExact(rootDir, relativePath, kind = "file") {
522
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
515
523
  let current = rootDir;
516
524
  for (const segment of relativePath.split("/")) {
517
525
  if (segment === "" || segment === ".") {
518
526
  continue;
519
527
  }
520
- let entries;
521
- try {
522
- entries = import_node_fs.default.readdirSync(current);
523
- } catch {
524
- return false;
528
+ let entries = listing.get(current);
529
+ if (entries === void 0) {
530
+ try {
531
+ entries = import_node_fs.default.readdirSync(current);
532
+ } catch {
533
+ entries = null;
534
+ }
535
+ listing.set(current, entries);
525
536
  }
526
- if (!entries.includes(segment)) {
537
+ if (entries === null || !entries.includes(segment)) {
527
538
  return false;
528
539
  }
529
540
  current = import_node_path.default.join(current, segment);
@@ -558,9 +569,12 @@ function readManifest(dir) {
558
569
  }
559
570
  throw error;
560
571
  }
572
+ const listing = /* @__PURE__ */ new Map();
561
573
  const gates = parsed.gates.map((gate) => {
562
574
  const specCandidates = specCandidatesFor(gate.path, settings);
563
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
575
+ const existingSpecs = specCandidates.filter(
576
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
577
+ );
564
578
  return {
565
579
  ...gate,
566
580
  specCandidates,
@@ -603,6 +617,9 @@ function resolveRootDir(rootDir) {
603
617
  // src/stryker.ts
604
618
  var WEAKENING_OPTIONS = ["ignoreStatic", "ignorers"];
605
619
  var DEFAULT_MAX_TEST_RUNNER_REUSE = 50;
620
+ function defaultConcurrency(parallelism, ci) {
621
+ return ci ? 2 : Math.min(8, Math.max(1, Math.floor(parallelism / 2)));
622
+ }
606
623
  function createStrykerGatesConfig(options = {}) {
607
624
  const { rootDir: rootDirOption, ...overrides } = options;
608
625
  for (const option of WEAKENING_OPTIONS) {
@@ -628,6 +645,7 @@ function createStrykerGatesConfig(options = {}) {
628
645
  incrementalFile: "reports/stryker-incremental.json",
629
646
  tempDirName: ".stryker-tmp",
630
647
  maxTestRunnerReuse: DEFAULT_MAX_TEST_RUNNER_REUSE,
648
+ concurrency: defaultConcurrency(import_node_os.default.availableParallelism(), inCi(process.env)),
631
649
  clearTextReporter: { logTests: false, reportTests: false },
632
650
  ...overrides,
633
651
  mutate: manifest.gates.map((gate) => gate.path),
@@ -639,5 +657,6 @@ function createStrykerGatesConfig(options = {}) {
639
657
  // Annotate the CommonJS export names for ESM import in node:
640
658
  0 && (module.exports = {
641
659
  DEFAULT_MAX_TEST_RUNNER_REUSE,
642
- createStrykerGatesConfig
660
+ createStrykerGatesConfig,
661
+ defaultConcurrency
643
662
  });