@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.
@@ -452,6 +452,24 @@ function resolveSettings(raw) {
452
452
 
453
453
  // src/core/selfcheck.ts
454
454
  var EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
455
+ var EXCLUDE_ALL_ENV = "TEST_GATES_EXCLUDE_SPECS";
456
+ function excludedSpecs(env) {
457
+ const one = env[EXCLUDE_ENV];
458
+ const several = env[EXCLUDE_ALL_ENV];
459
+ const excluded = one === void 0 || one === "" ? [] : [one];
460
+ if (several === void 0 || several === "") {
461
+ return excluded;
462
+ }
463
+ let listed = null;
464
+ try {
465
+ listed = JSON.parse(several);
466
+ } catch {
467
+ }
468
+ if (!Array.isArray(listed) || !listed.every((spec) => typeof spec === "string")) {
469
+ throw new Error(`test-gates: ${EXCLUDE_ALL_ENV} must be a JSON list of spec paths`);
470
+ }
471
+ return [...excluded, ...listed];
472
+ }
455
473
  var THRESHOLD_MESSAGE = /threshold|coverage data for/i;
456
474
  function escapeRegExp(text) {
457
475
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -471,6 +489,10 @@ function judgeSelfcheck(input) {
471
489
  const reported = output.split("\n").some((line) => line.includes(gatePath) && THRESHOLD_MESSAGE.test(line));
472
490
  return reported ? null : `the gate failed (exit ${status}) but not on the coverage threshold of ${gatePath}. It may be failing for another reason`;
473
491
  }
492
+ function inCi(env) {
493
+ const ci = (env.CI ?? "").trim().toLowerCase();
494
+ return ci !== "" && ci !== "false" && ci !== "0";
495
+ }
474
496
 
475
497
  // src/load.ts
476
498
  import fs2 from "fs";
@@ -503,19 +525,22 @@ function looksLikeSpec(gatePath, settings) {
503
525
  // src/fs.ts
504
526
  import fs from "fs";
505
527
  import path from "path";
506
- function existsExact(rootDir, relativePath, kind = "file") {
528
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
507
529
  let current = rootDir;
508
530
  for (const segment of relativePath.split("/")) {
509
531
  if (segment === "" || segment === ".") {
510
532
  continue;
511
533
  }
512
- let entries;
513
- try {
514
- entries = fs.readdirSync(current);
515
- } catch {
516
- return false;
534
+ let entries = listing.get(current);
535
+ if (entries === void 0) {
536
+ try {
537
+ entries = fs.readdirSync(current);
538
+ } catch {
539
+ entries = null;
540
+ }
541
+ listing.set(current, entries);
517
542
  }
518
- if (!entries.includes(segment)) {
543
+ if (entries === null || !entries.includes(segment)) {
519
544
  return false;
520
545
  }
521
546
  current = path.join(current, segment);
@@ -553,9 +578,12 @@ function readManifest(dir) {
553
578
  }
554
579
  throw error;
555
580
  }
581
+ const listing = /* @__PURE__ */ new Map();
556
582
  const gates = parsed.gates.map((gate) => {
557
583
  const specCandidates = specCandidatesFor(gate.path, settings);
558
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
584
+ const existingSpecs = specCandidates.filter(
585
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
586
+ );
559
587
  return {
560
588
  ...gate,
561
589
  specCandidates,
@@ -582,8 +610,8 @@ function loadGates(dir = process.cwd()) {
582
610
  return manifest;
583
611
  }
584
612
  function gateSpecs(manifest, env = process.env) {
585
- const excluded = env[EXCLUDE_ENV];
586
- return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec !== excluded);
613
+ const excluded = excludedSpecs(env);
614
+ return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && !excluded.includes(spec));
587
615
  }
588
616
  var NO_SPEC_PATTERN = "__test_gates_no_spec_left__";
589
617
 
@@ -602,6 +630,7 @@ export {
602
630
  resolveSettings,
603
631
  EXCLUDE_ENV,
604
632
  judgeSelfcheck,
633
+ inCi,
605
634
  gateExtensionOf,
606
635
  looksLikeSpec,
607
636
  existsExact,
@@ -0,0 +1,163 @@
1
+ // src/touch.ts
2
+ import { createHash } from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
7
+ function isObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function countersOf(coverage) {
11
+ return {
12
+ s: Object.values(coverage.s ?? {}),
13
+ f: Object.values(coverage.f ?? {}),
14
+ b: Object.values(coverage.b ?? {}).flat()
15
+ };
16
+ }
17
+ function countersOfAll(coverage) {
18
+ if (!isObject(coverage)) {
19
+ return null;
20
+ }
21
+ const files = {};
22
+ for (const [file, counters] of Object.entries(coverage)) {
23
+ if (!isObject(counters)) {
24
+ return null;
25
+ }
26
+ files[file] = countersOf(counters);
27
+ }
28
+ return files;
29
+ }
30
+ function add(before, now) {
31
+ return before.length === now.length ? before.map((count, index) => count + (now[index] ?? 0)) : [-1];
32
+ }
33
+ function writeSpecNote(directory, spec, coverage) {
34
+ const files = countersOfAll(coverage) ?? {};
35
+ const specs = path.join(directory, "specs");
36
+ fs.mkdirSync(specs, { recursive: true });
37
+ const file = path.join(specs, `${createHash("sha256").update(spec).digest("hex")}.json`);
38
+ let before = {};
39
+ try {
40
+ before = JSON.parse(fs.readFileSync(file, "utf8")).files;
41
+ } catch {
42
+ }
43
+ for (const [name, counters] of Object.entries(before)) {
44
+ const now = files[name];
45
+ files[name] = now === void 0 ? counters : { s: add(counters.s, now.s), f: add(counters.f, now.f), b: add(counters.b, now.b) };
46
+ }
47
+ const note = { spec, files };
48
+ fs.writeFileSync(file, JSON.stringify(note));
49
+ }
50
+ function writeRunInfo(directory, run) {
51
+ fs.mkdirSync(directory, { recursive: true });
52
+ fs.writeFileSync(path.join(directory, "run.json"), JSON.stringify(run));
53
+ }
54
+ function jestReporterFile(moduleUrl) {
55
+ const directory = path.dirname(fileURLToPath(moduleUrl));
56
+ const found = [".cjs", ".ts"].map((extension) => path.join(directory, `jest-touch-reporter${extension}`)).find((file) => fs.existsSync(file));
57
+ if (found === void 0) {
58
+ throw new Error(`test-gates: jest-touch-reporter is missing in ${directory}`);
59
+ }
60
+ return found;
61
+ }
62
+ var VITEST_PROVIDER = `import { createHash } from 'node:crypto';
63
+ import fs from 'node:fs';
64
+ import path from 'node:path';
65
+ import istanbul from '@vitest/coverage-istanbul';
66
+
67
+ const directory = "__DIRECTORY__";
68
+ const run = { runner: 'vitest', version: 'unknown', provider: 'istanbul', pool: 'unknown', isolate: false, projects: 0, problem: null };
69
+ const save = () => fs.writeFileSync(path.join(directory, 'run.json'), JSON.stringify(run));
70
+ const problem = (text) => {
71
+ run.problem ??= text;
72
+ save();
73
+ };
74
+
75
+ const counts = (coverage) =>
76
+ Object.fromEntries(
77
+ Object.entries(coverage).map(([file, counters]) => [
78
+ file,
79
+ {
80
+ s: Object.values(counters.s ?? {}),
81
+ f: Object.values(counters.f ?? {}),
82
+ b: Object.values(counters.b ?? {}).flat(),
83
+ },
84
+ ])
85
+ );
86
+ const add = (before, now) =>
87
+ before.length === now.length ? before.map((count, index) => count + now[index]) : [-1];
88
+
89
+ function note(spec, files) {
90
+ const file = path.join(directory, 'specs', createHash('sha256').update(spec).digest('hex') + '.json');
91
+ let before = {};
92
+ try {
93
+ before = JSON.parse(fs.readFileSync(file, 'utf8')).files;
94
+ } catch {
95
+ // the first note of this spec
96
+ }
97
+ const merged = { ...files };
98
+ for (const [name, counters] of Object.entries(before)) {
99
+ const now = files[name];
100
+ merged[name] =
101
+ now === undefined
102
+ ? counters
103
+ : { s: add(counters.s, now.s), f: add(counters.f, now.f), b: add(counters.b, now.b) };
104
+ }
105
+ fs.writeFileSync(file, JSON.stringify({ spec, files: merged }));
106
+ }
107
+
108
+ export default {
109
+ ...istanbul,
110
+ async getProvider() {
111
+ const provider = await istanbul.getProvider();
112
+ fs.mkdirSync(path.join(directory, 'specs'), { recursive: true });
113
+ save();
114
+ if (typeof provider.onAfterSuiteRun !== 'function' || typeof provider.initialize !== 'function') {
115
+ problem('the coverage provider of this Vitest has no onAfterSuiteRun or no initialize');
116
+ return provider;
117
+ }
118
+ const initialize = provider.initialize.bind(provider);
119
+ provider.initialize = (ctx) => {
120
+ // A project can set its own pool and isolation: all of them have to say the same.
121
+ const projects = Array.isArray(ctx?.projects) ? ctx.projects : [];
122
+ const configs = [ctx?.config ?? {}, ...projects.map((project) => project?.config ?? {})];
123
+ const pools = [...new Set(configs.map((config) => (typeof config.pool === 'string' ? config.pool : 'unknown')))];
124
+ run.version = typeof ctx?.version === 'string' ? ctx.version : 'unknown';
125
+ run.pool = pools.length === 1 ? pools[0] : 'mixed';
126
+ run.isolate = configs.every(
127
+ (config) => (config.poolOptions?.[config.pool]?.isolate ?? config.isolate) === true
128
+ );
129
+ run.projects = projects.length;
130
+ save();
131
+ return initialize(ctx);
132
+ };
133
+ const handOver = provider.onAfterSuiteRun.bind(provider);
134
+ provider.onAfterSuiteRun = (meta) => {
135
+ const coverage = meta?.coverage;
136
+ if (coverage !== undefined && coverage !== null) {
137
+ const specs = meta.testFiles;
138
+ if (typeof coverage !== 'object' || !Array.isArray(specs) || specs.length !== 1 || typeof specs[0] !== 'string') {
139
+ problem('Vitest handed over coverage that is not that of exactly one spec file');
140
+ } else {
141
+ note(specs[0], counts(coverage));
142
+ }
143
+ }
144
+ return handOver(meta);
145
+ };
146
+ return provider;
147
+ },
148
+ };
149
+ `;
150
+ function vitestProviderFile(directory) {
151
+ const file = path.join(directory, "vitest-provider.mjs");
152
+ fs.mkdirSync(directory, { recursive: true });
153
+ fs.writeFileSync(file, VITEST_PROVIDER.replace('"__DIRECTORY__"', JSON.stringify(directory)));
154
+ return file;
155
+ }
156
+
157
+ export {
158
+ TOUCH_ENV,
159
+ writeSpecNote,
160
+ writeRunInfo,
161
+ jestReporterFile,
162
+ vitestProviderFile
163
+ };