@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.
- package/CHANGELOG.md +28 -4
- package/README.md +262 -40
- package/dist/chunk-TOKHAUFX.js +163 -0
- package/dist/{chunk-F7WBEFIL.js → chunk-XMZ6NBV7.js} +34 -10
- package/dist/cli.js +1511 -63
- package/dist/index.cjs +37 -12
- 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 +14 -8
- package/dist/stryker.d.cts +1 -1
- package/dist/stryker.d.ts +1 -1
- package/dist/stryker.js +2 -2
- 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
|
@@ -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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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(
|
|
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
|
|
586
|
-
return manifest.gates.map((gate) => gate.spec).filter((spec) => spec !== null && spec
|
|
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
|
|