@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/CHANGELOG.md +44 -0
- package/README.md +290 -17
- package/dist/{chunk-F7WBEFIL.js → chunk-AK2NTFQO.js} +39 -10
- package/dist/chunk-TOKHAUFX.js +163 -0
- package/dist/cli.js +3129 -501
- package/dist/index.cjs +37 -12
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +4 -3
- package/dist/jest-touch-reporter.cjs +129 -0
- package/dist/jest-touch-reporter.d.cts +19 -0
- package/dist/jest-touch-reporter.d.ts +19 -0
- package/dist/jest-touch-reporter.js +48 -0
- package/dist/jest.cjs +70 -12
- package/dist/jest.js +19 -2
- package/dist/stryker.cjs +29 -10
- package/dist/stryker.d.cts +15 -1
- package/dist/stryker.d.ts +15 -1
- package/dist/stryker.js +10 -3
- package/dist/vitest.cjs +151 -12
- package/dist/vitest.js +22 -4
- package/package.json +7 -2
- /package/dist/{chunk-4HAJIKDZ.js → chunk-C4R3RDHF.js} +0 -0
|
@@ -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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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(
|
|
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
|
|
586
|
-
return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec
|
|
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
|
+
};
|