@t09tanaka/test-gates 0.3.3 → 0.3.4

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/vitest.cjs CHANGED
@@ -33,6 +33,7 @@ __export(vitest_exports, {
33
33
  createVitestGatesConfig: () => createVitestGatesConfig
34
34
  });
35
35
  module.exports = __toCommonJS(vitest_exports);
36
+ var import_node_path5 = __toESM(require("path"), 1);
36
37
 
37
38
  // src/load.ts
38
39
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -489,6 +490,24 @@ function resolveSettings(raw) {
489
490
 
490
491
  // src/core/selfcheck.ts
491
492
  var EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
493
+ var EXCLUDE_ALL_ENV = "TEST_GATES_EXCLUDE_SPECS";
494
+ function excludedSpecs(env) {
495
+ const one = env[EXCLUDE_ENV];
496
+ const several = env[EXCLUDE_ALL_ENV];
497
+ const excluded = one === void 0 || one === "" ? [] : [one];
498
+ if (several === void 0 || several === "") {
499
+ return excluded;
500
+ }
501
+ let listed = null;
502
+ try {
503
+ listed = JSON.parse(several);
504
+ } catch {
505
+ }
506
+ if (!Array.isArray(listed) || !listed.every((spec) => typeof spec === "string")) {
507
+ throw new Error(`test-gates: ${EXCLUDE_ALL_ENV} must be a JSON list of spec paths`);
508
+ }
509
+ return [...excluded, ...listed];
510
+ }
492
511
 
493
512
  // src/core/spec-path.ts
494
513
  function gateExtensionOf(gatePath, gateExtensions) {
@@ -513,19 +532,22 @@ function specCandidatesFor(gatePath, settings) {
513
532
  // src/fs.ts
514
533
  var import_node_fs = __toESM(require("fs"), 1);
515
534
  var import_node_path = __toESM(require("path"), 1);
516
- function existsExact(rootDir, relativePath, kind = "file") {
535
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
517
536
  let current = rootDir;
518
537
  for (const segment of relativePath.split("/")) {
519
538
  if (segment === "" || segment === ".") {
520
539
  continue;
521
540
  }
522
- let entries;
523
- try {
524
- entries = import_node_fs.default.readdirSync(current);
525
- } catch {
526
- return false;
541
+ let entries = listing.get(current);
542
+ if (entries === void 0) {
543
+ try {
544
+ entries = import_node_fs.default.readdirSync(current);
545
+ } catch {
546
+ entries = null;
547
+ }
548
+ listing.set(current, entries);
527
549
  }
528
- if (!entries.includes(segment)) {
550
+ if (entries === null || !entries.includes(segment)) {
529
551
  return false;
530
552
  }
531
553
  current = import_node_path.default.join(current, segment);
@@ -560,9 +582,12 @@ function readManifest(dir) {
560
582
  }
561
583
  throw error;
562
584
  }
585
+ const listing = /* @__PURE__ */ new Map();
563
586
  const gates = parsed.gates.map((gate) => {
564
587
  const specCandidates = specCandidatesFor(gate.path, settings);
565
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
588
+ const existingSpecs = specCandidates.filter(
589
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
590
+ );
566
591
  return {
567
592
  ...gate,
568
593
  specCandidates,
@@ -589,8 +614,8 @@ function loadGates(dir = process.cwd()) {
589
614
  return manifest;
590
615
  }
591
616
  function gateSpecs(manifest, env = process.env) {
592
- const excluded = env[EXCLUDE_ENV];
593
- return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec !== excluded);
617
+ const excluded = excludedSpecs(env);
618
+ return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && !excluded.includes(spec));
594
619
  }
595
620
  var NO_SPEC_PATTERN = "__test_gates_no_spec_left__";
596
621
 
@@ -608,6 +633,107 @@ function resolveRootDir(rootDir) {
608
633
  return import_node_path3.default.resolve(rootDir);
609
634
  }
610
635
 
636
+ // src/touch.ts
637
+ var import_node_crypto = require("crypto");
638
+ var import_node_fs3 = __toESM(require("fs"), 1);
639
+ var import_node_path4 = __toESM(require("path"), 1);
640
+ var import_node_url2 = require("url");
641
+ var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
642
+ var VITEST_PROVIDER = `import { createHash } from 'node:crypto';
643
+ import fs from 'node:fs';
644
+ import path from 'node:path';
645
+ import istanbul from '@vitest/coverage-istanbul';
646
+
647
+ const directory = "__DIRECTORY__";
648
+ const run = { runner: 'vitest', version: 'unknown', provider: 'istanbul', pool: 'unknown', isolate: false, projects: 0, problem: null };
649
+ const save = () => fs.writeFileSync(path.join(directory, 'run.json'), JSON.stringify(run));
650
+ const problem = (text) => {
651
+ run.problem ??= text;
652
+ save();
653
+ };
654
+
655
+ const counts = (coverage) =>
656
+ Object.fromEntries(
657
+ Object.entries(coverage).map(([file, counters]) => [
658
+ file,
659
+ {
660
+ s: Object.values(counters.s ?? {}),
661
+ f: Object.values(counters.f ?? {}),
662
+ b: Object.values(counters.b ?? {}).flat(),
663
+ },
664
+ ])
665
+ );
666
+ const add = (before, now) =>
667
+ before.length === now.length ? before.map((count, index) => count + now[index]) : [-1];
668
+
669
+ function note(spec, files) {
670
+ const file = path.join(directory, 'specs', createHash('sha256').update(spec).digest('hex') + '.json');
671
+ let before = {};
672
+ try {
673
+ before = JSON.parse(fs.readFileSync(file, 'utf8')).files;
674
+ } catch {
675
+ // the first note of this spec
676
+ }
677
+ const merged = { ...files };
678
+ for (const [name, counters] of Object.entries(before)) {
679
+ const now = files[name];
680
+ merged[name] =
681
+ now === undefined
682
+ ? counters
683
+ : { s: add(counters.s, now.s), f: add(counters.f, now.f), b: add(counters.b, now.b) };
684
+ }
685
+ fs.writeFileSync(file, JSON.stringify({ spec, files: merged }));
686
+ }
687
+
688
+ export default {
689
+ ...istanbul,
690
+ async getProvider() {
691
+ const provider = await istanbul.getProvider();
692
+ fs.mkdirSync(path.join(directory, 'specs'), { recursive: true });
693
+ save();
694
+ if (typeof provider.onAfterSuiteRun !== 'function' || typeof provider.initialize !== 'function') {
695
+ problem('the coverage provider of this Vitest has no onAfterSuiteRun or no initialize');
696
+ return provider;
697
+ }
698
+ const initialize = provider.initialize.bind(provider);
699
+ provider.initialize = (ctx) => {
700
+ // A project can set its own pool and isolation: all of them have to say the same.
701
+ const projects = Array.isArray(ctx?.projects) ? ctx.projects : [];
702
+ const configs = [ctx?.config ?? {}, ...projects.map((project) => project?.config ?? {})];
703
+ const pools = [...new Set(configs.map((config) => (typeof config.pool === 'string' ? config.pool : 'unknown')))];
704
+ run.version = typeof ctx?.version === 'string' ? ctx.version : 'unknown';
705
+ run.pool = pools.length === 1 ? pools[0] : 'mixed';
706
+ run.isolate = configs.every(
707
+ (config) => (config.poolOptions?.[config.pool]?.isolate ?? config.isolate) === true
708
+ );
709
+ run.projects = projects.length;
710
+ save();
711
+ return initialize(ctx);
712
+ };
713
+ const handOver = provider.onAfterSuiteRun.bind(provider);
714
+ provider.onAfterSuiteRun = (meta) => {
715
+ const coverage = meta?.coverage;
716
+ if (coverage !== undefined && coverage !== null) {
717
+ const specs = meta.testFiles;
718
+ if (typeof coverage !== 'object' || !Array.isArray(specs) || specs.length !== 1 || typeof specs[0] !== 'string') {
719
+ problem('Vitest handed over coverage that is not that of exactly one spec file');
720
+ } else {
721
+ note(specs[0], counts(coverage));
722
+ }
723
+ }
724
+ return handOver(meta);
725
+ };
726
+ return provider;
727
+ },
728
+ };
729
+ `;
730
+ function vitestProviderFile(directory) {
731
+ const file = import_node_path4.default.join(directory, "vitest-provider.mjs");
732
+ import_node_fs3.default.mkdirSync(directory, { recursive: true });
733
+ import_node_fs3.default.writeFileSync(file, VITEST_PROVIDER.replace('"__DIRECTORY__"', JSON.stringify(directory)));
734
+ return file;
735
+ }
736
+
611
737
  // src/vitest.ts
612
738
  function createVitestGatesConfig(options = {}) {
613
739
  const { rootDir: rootDirOption, coverageProvider, test = {}, ...overrides } = options;
@@ -615,6 +741,8 @@ function createVitestGatesConfig(options = {}) {
615
741
  const manifest = loadGates(rootDir);
616
742
  const { coverage = {}, ...testOverrides } = test;
617
743
  const specs = gateSpecs(manifest);
744
+ const provider = coverageProvider ?? coverage.provider ?? "v8";
745
+ const touchDir = process.env[TOUCH_ENV];
618
746
  return {
619
747
  ...overrides,
620
748
  root: rootDir,
@@ -626,11 +754,22 @@ function createVitestGatesConfig(options = {}) {
626
754
  // then fail on the coverage threshold, not on "no test files found".
627
755
  passWithNoTests: true,
628
756
  coverage: {
629
- provider: "v8",
630
757
  reporter: ["text"],
631
758
  reportsDirectory: "coverage/gates",
632
759
  ...coverage,
633
- ...coverageProvider === void 0 ? {} : { provider: coverageProvider },
760
+ provider,
761
+ // While `test-gates selfcheck` asks for it: write down what each spec executed. Only
762
+ // the istanbul provider can be wrapped for that (see src/touch.ts).
763
+ ...touchDir && provider === "istanbul" ? {
764
+ provider: "custom",
765
+ customProviderModule: vitestProviderFile(touchDir),
766
+ // The runner's own total, to check the notes against.
767
+ reporter: [
768
+ ...Array.isArray(coverage.reporter) ? coverage.reporter : [coverage.reporter ?? "text"],
769
+ "json"
770
+ ],
771
+ reportsDirectory: import_node_path5.default.join(touchDir, "coverage")
772
+ } : {},
634
773
  enabled: true,
635
774
  include: manifest.gates.map((gate) => gate.path),
636
775
  thresholds: { perFile: true, ...FULL_COVERAGE }
package/dist/vitest.js CHANGED
@@ -1,20 +1,27 @@
1
+ import {
2
+ TOUCH_ENV,
3
+ vitestProviderFile
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-XMZ6NBV7.js";
10
14
 
11
15
  // src/vitest.ts
16
+ import path from "path";
12
17
  function createVitestGatesConfig(options = {}) {
13
18
  const { rootDir: rootDirOption, coverageProvider, test = {}, ...overrides } = options;
14
19
  const rootDir = resolveRootDir(rootDirOption);
15
20
  const manifest = loadGates(rootDir);
16
21
  const { coverage = {}, ...testOverrides } = test;
17
22
  const specs = gateSpecs(manifest);
23
+ const provider = coverageProvider ?? coverage.provider ?? "v8";
24
+ const touchDir = process.env[TOUCH_ENV];
18
25
  return {
19
26
  ...overrides,
20
27
  root: rootDir,
@@ -26,11 +33,22 @@ function createVitestGatesConfig(options = {}) {
26
33
  // then fail on the coverage threshold, not on "no test files found".
27
34
  passWithNoTests: true,
28
35
  coverage: {
29
- provider: "v8",
30
36
  reporter: ["text"],
31
37
  reportsDirectory: "coverage/gates",
32
38
  ...coverage,
33
- ...coverageProvider === void 0 ? {} : { provider: coverageProvider },
39
+ provider,
40
+ // While `test-gates selfcheck` asks for it: write down what each spec executed. Only
41
+ // the istanbul provider can be wrapped for that (see src/touch.ts).
42
+ ...touchDir && provider === "istanbul" ? {
43
+ provider: "custom",
44
+ customProviderModule: vitestProviderFile(touchDir),
45
+ // The runner's own total, to check the notes against.
46
+ reporter: [
47
+ ...Array.isArray(coverage.reporter) ? coverage.reporter : [coverage.reporter ?? "text"],
48
+ "json"
49
+ ],
50
+ reportsDirectory: path.join(touchDir, "coverage")
51
+ } : {},
34
52
  enabled: true,
35
53
  include: manifest.gates.map((gate) => gate.path),
36
54
  thresholds: { perFile: true, ...FULL_COVERAGE }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t09tanaka/test-gates",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",
@@ -94,12 +94,15 @@
94
94
  "format:check": "prettier --check .",
95
95
  "pretest": "npm run build",
96
96
  "test": "vitest run",
97
+ "pretest:heavy": "npm run build",
98
+ "test:heavy": "vitest run --config vitest.heavy.config.ts",
99
+ "check:quick": "npm run lint && npm run typecheck && npm run format:check && npm test",
97
100
  "test:watch": "vitest",
98
101
  "test:coverage": "vitest run --coverage",
99
102
  "test:gates": "node dist/cli.js check && vitest run --config vitest.gates.config.ts",
100
103
  "test:gates:selfcheck": "node dist/cli.js selfcheck",
101
104
  "test:gates:mutation": "node dist/cli.js mutation",
102
- "prepublishOnly": "npm run lint && npm run format:check && npm run typecheck && npm test && npm run test:gates && npm run test:gates:selfcheck"
105
+ "prepublishOnly": "npm run lint && npm run format:check && npm run typecheck && npm test && npm run test:heavy && npm run test:gates && npm run test:gates:selfcheck"
103
106
  },
104
107
  "peerDependencies": {
105
108
  "@stryker-mutator/core": ">=9",
@@ -124,6 +127,7 @@
124
127
  "devDependencies": {
125
128
  "@eslint/js": "^9.39.5",
126
129
  "@stryker-mutator/core": "^10.0.0",
130
+ "@stryker-mutator/jest-runner": "^10.0.0",
127
131
  "@stryker-mutator/vitest-runner": "^10.0.0",
128
132
  "@types/node": "^22.20.5",
129
133
  "@vitest/coverage-istanbul": "^3.2.7",
@@ -131,6 +135,7 @@
131
135
  "eslint": "^9.39.5",
132
136
  "eslint-config-prettier": "^10.1.8",
133
137
  "eslint-plugin-prettier": "^5.5.6",
138
+ "jest": "^30.5.2",
134
139
  "prettier": "^3.9.9",
135
140
  "tsup": "^8.5.1",
136
141
  "typescript": "^5.9.3",
File without changes