@t09tanaka/test-gates 0.3.2 → 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.
@@ -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
+ };
@@ -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, "\\$&");
@@ -503,19 +521,22 @@ function looksLikeSpec(gatePath, settings) {
503
521
  // src/fs.ts
504
522
  import fs from "fs";
505
523
  import path from "path";
506
- function existsExact(rootDir, relativePath, kind = "file") {
524
+ function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
507
525
  let current = rootDir;
508
526
  for (const segment of relativePath.split("/")) {
509
527
  if (segment === "" || segment === ".") {
510
528
  continue;
511
529
  }
512
- let entries;
513
- try {
514
- entries = fs.readdirSync(current);
515
- } catch {
516
- return false;
530
+ let entries = listing.get(current);
531
+ if (entries === void 0) {
532
+ try {
533
+ entries = fs.readdirSync(current);
534
+ } catch {
535
+ entries = null;
536
+ }
537
+ listing.set(current, entries);
517
538
  }
518
- if (!entries.includes(segment)) {
539
+ if (entries === null || !entries.includes(segment)) {
519
540
  return false;
520
541
  }
521
542
  current = path.join(current, segment);
@@ -553,9 +574,12 @@ function readManifest(dir) {
553
574
  }
554
575
  throw error;
555
576
  }
577
+ const listing = /* @__PURE__ */ new Map();
556
578
  const gates = parsed.gates.map((gate) => {
557
579
  const specCandidates = specCandidatesFor(gate.path, settings);
558
- const existingSpecs = specCandidates.filter((candidate) => existsExact(absoluteDir, candidate));
580
+ const existingSpecs = specCandidates.filter(
581
+ (candidate) => existsExact(absoluteDir, candidate, "file", listing)
582
+ );
559
583
  return {
560
584
  ...gate,
561
585
  specCandidates,
@@ -582,8 +606,8 @@ function loadGates(dir = process.cwd()) {
582
606
  return manifest;
583
607
  }
584
608
  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);
609
+ const excluded = excludedSpecs(env);
610
+ return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && !excluded.includes(spec));
587
611
  }
588
612
  var NO_SPEC_PATTERN = "__test_gates_no_spec_left__";
589
613