@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
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
|
|
6
6
|
// src/core/args.ts
|
|
7
7
|
var COMMANDS = ["check", "mutation", "mutation-result", "selfcheck", "lcov"];
|
|
8
8
|
var UsageError = class extends Error {
|
|
9
9
|
};
|
|
10
|
-
var VALUE_OPTIONS = ["--dir", "--report", "--file"];
|
|
10
|
+
var VALUE_OPTIONS = ["--dir", "--report", "--file", "--gate", "--shard", "--jobs"];
|
|
11
|
+
function parseShard(value) {
|
|
12
|
+
const match = /^([1-9]\d*)\/([1-9]\d*)$/.exec(value);
|
|
13
|
+
const index = Number(match?.[1]);
|
|
14
|
+
const total = Number(match?.[2]);
|
|
15
|
+
if (!Number.isSafeInteger(total) || index > total) {
|
|
16
|
+
throw new UsageError(
|
|
17
|
+
`--shard needs <index>/<total> with 1 <= index <= total (for example 2/4), got ${value}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return { index, total };
|
|
21
|
+
}
|
|
11
22
|
var OPTIONS_BY_COMMAND = {
|
|
12
23
|
check: ["--dir"],
|
|
13
|
-
mutation: ["--dir"],
|
|
24
|
+
mutation: ["--dir", "--gate"],
|
|
14
25
|
"mutation-result": ["--dir", "--report"],
|
|
15
|
-
selfcheck: ["--dir", "--all", "--first"],
|
|
26
|
+
selfcheck: ["--dir", "--all", "--first", "--gate", "--shard", "--quick", "--jobs"],
|
|
16
27
|
lcov: ["--dir", "--file"]
|
|
17
28
|
};
|
|
18
29
|
function consume(parsed, argv, index, allowed) {
|
|
@@ -41,13 +52,31 @@ function consume(parsed, argv, index, allowed) {
|
|
|
41
52
|
if (value === void 0 || value === "") {
|
|
42
53
|
throw new UsageError(`${name} needs a value`);
|
|
43
54
|
}
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
if (name === "--gate") {
|
|
56
|
+
parsed.gates.push(value);
|
|
57
|
+
} else if (name === "--jobs") {
|
|
58
|
+
if (!/^[1-9]\d{0,2}$/.test(value)) {
|
|
59
|
+
throw new UsageError(`--jobs needs a number from 1 to 999, got ${value}`);
|
|
60
|
+
}
|
|
61
|
+
parsed.jobs = Number(value);
|
|
62
|
+
} else if (name === "--shard") {
|
|
63
|
+
if (parsed.shard !== null) {
|
|
64
|
+
throw new UsageError("--shard can be given only once");
|
|
65
|
+
}
|
|
66
|
+
parsed.shard = parseShard(value);
|
|
67
|
+
} else {
|
|
68
|
+
const key = name.slice(2);
|
|
69
|
+
parsed[key] = value;
|
|
70
|
+
}
|
|
46
71
|
return equals !== -1 ? next : next + 1;
|
|
47
72
|
}
|
|
48
73
|
if (equals !== -1) {
|
|
49
74
|
throw new UsageError(`${name} does not take a value`);
|
|
50
75
|
}
|
|
76
|
+
if (name === "--quick") {
|
|
77
|
+
parsed.quick = true;
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
51
80
|
const mode = name === "--all" ? "all" : "first";
|
|
52
81
|
if (parsed.mode !== null && parsed.mode !== mode) {
|
|
53
82
|
throw new UsageError("--all and --first cannot be combined");
|
|
@@ -72,6 +101,10 @@ function parseArgs(argv) {
|
|
|
72
101
|
report: null,
|
|
73
102
|
file: null,
|
|
74
103
|
mode: null,
|
|
104
|
+
gates: [],
|
|
105
|
+
shard: null,
|
|
106
|
+
quick: false,
|
|
107
|
+
jobs: null,
|
|
75
108
|
rest: []
|
|
76
109
|
};
|
|
77
110
|
let index = 0;
|
|
@@ -87,6 +120,23 @@ function parseArgs(argv) {
|
|
|
87
120
|
while (index < argv.length) {
|
|
88
121
|
index = consume(parsed, argv, index, allowed);
|
|
89
122
|
}
|
|
123
|
+
const selections = [
|
|
124
|
+
parsed.mode === null ? null : `--${parsed.mode}`,
|
|
125
|
+
parsed.gates.length > 0 ? "--gate" : null,
|
|
126
|
+
parsed.shard === null ? null : "--shard"
|
|
127
|
+
].filter((option) => option !== null);
|
|
128
|
+
if (selections.length > 1) {
|
|
129
|
+
throw new UsageError(`${selections.join(" and ")} cannot be combined`);
|
|
130
|
+
}
|
|
131
|
+
const misplaced = parsed.rest.find((arg) => /^--(gate|shard|quick|jobs)(=|$)/.test(arg));
|
|
132
|
+
if (parsed.command === "selfcheck" && misplaced !== void 0) {
|
|
133
|
+
throw new UsageError(
|
|
134
|
+
`${misplaced.split("=")[0]} after "--" would go to the gate command. Write it before "--"`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (parsed.rest.some((arg) => arg === "--gate" || arg.startsWith("--gate="))) {
|
|
138
|
+
throw new UsageError('--gate after "--" would go to Stryker. Write it before "--"');
|
|
139
|
+
}
|
|
90
140
|
if (parsed.command === "selfcheck" && parsed.rest.length === 0 && argv.includes("--")) {
|
|
91
141
|
throw new UsageError('"--" must be followed by the gate command');
|
|
92
142
|
}
|
|
@@ -559,19 +609,22 @@ function vitestCoverageViolation(input) {
|
|
|
559
609
|
// src/fs.ts
|
|
560
610
|
import fs from "fs";
|
|
561
611
|
import path from "path";
|
|
562
|
-
function existsExact(rootDir, relativePath, kind = "file") {
|
|
612
|
+
function existsExact(rootDir, relativePath, kind = "file", listing = /* @__PURE__ */ new Map()) {
|
|
563
613
|
let current = rootDir;
|
|
564
614
|
for (const segment of relativePath.split("/")) {
|
|
565
615
|
if (segment === "" || segment === ".") {
|
|
566
616
|
continue;
|
|
567
617
|
}
|
|
568
|
-
let entries;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
618
|
+
let entries = listing.get(current);
|
|
619
|
+
if (entries === void 0) {
|
|
620
|
+
try {
|
|
621
|
+
entries = fs.readdirSync(current);
|
|
622
|
+
} catch {
|
|
623
|
+
entries = null;
|
|
624
|
+
}
|
|
625
|
+
listing.set(current, entries);
|
|
573
626
|
}
|
|
574
|
-
if (!entries.includes(segment)) {
|
|
627
|
+
if (entries === null || !entries.includes(segment)) {
|
|
575
628
|
return false;
|
|
576
629
|
}
|
|
577
630
|
current = path.join(current, segment);
|
|
@@ -927,6 +980,7 @@ function resolveSettings(raw) {
|
|
|
927
980
|
|
|
928
981
|
// src/core/selfcheck.ts
|
|
929
982
|
var EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
|
|
983
|
+
var EXCLUDE_ALL_ENV = "TEST_GATES_EXCLUDE_SPECS";
|
|
930
984
|
var THRESHOLD_MESSAGE = /threshold|coverage data for/i;
|
|
931
985
|
function escapeRegExp2(text) {
|
|
932
986
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -946,6 +1000,194 @@ function judgeSelfcheck(input) {
|
|
|
946
1000
|
const reported = output.split("\n").some((line) => line.includes(gatePath) && THRESHOLD_MESSAGE.test(line));
|
|
947
1001
|
return reported ? null : `the gate failed (exit ${status}) but not on the coverage threshold of ${gatePath}. It may be failing for another reason`;
|
|
948
1002
|
}
|
|
1003
|
+
function selectControls(input) {
|
|
1004
|
+
const { gates, mode, only, shard } = input;
|
|
1005
|
+
if (only.length > 0) {
|
|
1006
|
+
const unknown = only.filter((path11) => !gates.includes(path11));
|
|
1007
|
+
if (unknown.length > 0) {
|
|
1008
|
+
return {
|
|
1009
|
+
error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return { selected: gates.filter((path11) => only.includes(path11)) };
|
|
1013
|
+
}
|
|
1014
|
+
if (shard !== null) {
|
|
1015
|
+
if (shard.total > gates.length) {
|
|
1016
|
+
return {
|
|
1017
|
+
error: `--shard ${shard.index}/${shard.total}: there are only ${gates.length} gate(s). A part without a gate would pass without checking anything`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
selected: gates.filter((_path, position) => position % shard.total === shard.index - 1)
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return { selected: mode === "first" ? gates.slice(0, 1) : gates };
|
|
1025
|
+
}
|
|
1026
|
+
var VERIFIED_RUNS = [
|
|
1027
|
+
{ runner: "jest", versions: ["30.5"], provider: "babel", pools: ["default"] },
|
|
1028
|
+
{ runner: "vitest", versions: ["3.2"], provider: "istanbul", pools: ["forks", "threads"] }
|
|
1029
|
+
];
|
|
1030
|
+
function minorOf(version) {
|
|
1031
|
+
const match = /^(\d+\.\d+)\./.exec(version);
|
|
1032
|
+
return match === null ? version : match[1];
|
|
1033
|
+
}
|
|
1034
|
+
function unverifiedVersion(runner, version, table = VERIFIED_RUNS) {
|
|
1035
|
+
const known = table.find((entry) => entry.runner === runner);
|
|
1036
|
+
if (known === void 0) {
|
|
1037
|
+
return `the shared run is not verified with ${runner}`;
|
|
1038
|
+
}
|
|
1039
|
+
return known.versions.includes(minorOf(version)) ? null : `the shared run is verified with ${runner} ${known.versions.join(", ")}, not with ${version}`;
|
|
1040
|
+
}
|
|
1041
|
+
var SURROUNDINGS = /\bprocess\s*\.\s*env\b|\bimport\.meta\.env\b|['"`](?:node:)?(?:fs|fs\/promises|child_process|worker_threads|net|http|https)['"`]/;
|
|
1042
|
+
function inCi(env) {
|
|
1043
|
+
const ci = (env.CI ?? "").trim().toLowerCase();
|
|
1044
|
+
return ci !== "" && ci !== "false" && ci !== "0";
|
|
1045
|
+
}
|
|
1046
|
+
function quickRefused(env) {
|
|
1047
|
+
return inCi(env) ? "--quick is refused in CI (the variable CI is set): what it reports as holding is not the final check. Run selfcheck without it, with --shard to split it over jobs" : null;
|
|
1048
|
+
}
|
|
1049
|
+
function gateRunRefused(env) {
|
|
1050
|
+
return inCi(env) ? "--gate is refused in CI (the variable CI is set): a mutation run of a part of the gates is not the final check. Run mutation without it" : null;
|
|
1051
|
+
}
|
|
1052
|
+
function gateRunCaveat(judged, all) {
|
|
1053
|
+
return `. Only ${judged} of ${all} gate(s) were run and judged (--gate), and a change in one gate can make a mutant of another survive: run without --gate for the final check`;
|
|
1054
|
+
}
|
|
1055
|
+
function quickCaveat(shared) {
|
|
1056
|
+
return shared === 0 ? "" : `. Of the controls that held, ${shared} came from one shared run (--quick), which a spec that depends on other specs can make differ from a run per gate: run without --quick for the final check`;
|
|
1057
|
+
}
|
|
1058
|
+
function sharedRunPrecheck(input) {
|
|
1059
|
+
const reaching = input.specs.find((spec) => SURROUNDINGS.test(spec.source));
|
|
1060
|
+
if (reaching !== void 0) {
|
|
1061
|
+
return `${reaching.path} reads the environment or files, so what it executes may depend on which other specs ran`;
|
|
1062
|
+
}
|
|
1063
|
+
const runner = ["jest", "vitest"].find((name) => name === input.program);
|
|
1064
|
+
if (runner === void 0) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
if (input.version !== null) {
|
|
1068
|
+
const unverified = unverifiedVersion(runner, input.version);
|
|
1069
|
+
if (unverified !== null) {
|
|
1070
|
+
return unverified;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (input.configSource === null) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
const helper = runner === "jest" ? "createJestGatesConfig" : "createVitestGatesConfig";
|
|
1077
|
+
if (!input.configSource.includes(helper)) {
|
|
1078
|
+
return `the gate config does not use ${helper}`;
|
|
1079
|
+
}
|
|
1080
|
+
if (runner === "vitest" && !input.configSource.includes("istanbul")) {
|
|
1081
|
+
return "Vitest measures with the v8 provider; the shared run needs coverageProvider: 'istanbul'";
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
function unverifiedRun(run) {
|
|
1086
|
+
if (run.problem !== null) {
|
|
1087
|
+
return run.problem;
|
|
1088
|
+
}
|
|
1089
|
+
const version = unverifiedVersion(run.runner, run.version);
|
|
1090
|
+
if (version !== null) {
|
|
1091
|
+
return version;
|
|
1092
|
+
}
|
|
1093
|
+
const known = VERIFIED_RUNS.find(
|
|
1094
|
+
(entry) => entry.runner === run.runner
|
|
1095
|
+
);
|
|
1096
|
+
if (known.provider !== run.provider) {
|
|
1097
|
+
return `the shared run is not verified with the coverage provider ${run.provider}`;
|
|
1098
|
+
}
|
|
1099
|
+
if (!known.pools.includes(run.pool)) {
|
|
1100
|
+
return `the shared run is not verified with the pool ${run.pool}`;
|
|
1101
|
+
}
|
|
1102
|
+
if (!run.isolate) {
|
|
1103
|
+
return "the shared run is not verified with spec files that share their modules (isolate: false)";
|
|
1104
|
+
}
|
|
1105
|
+
if (run.projects !== 1) {
|
|
1106
|
+
return `the shared run is not verified with ${run.projects} projects in one run`;
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
function sorted(counts) {
|
|
1111
|
+
return [...counts].sort((left, right) => left - right);
|
|
1112
|
+
}
|
|
1113
|
+
function same(left, right) {
|
|
1114
|
+
return left.every((count, index) => count === right[index]);
|
|
1115
|
+
}
|
|
1116
|
+
function contained(part, whole) {
|
|
1117
|
+
const left = sorted(whole);
|
|
1118
|
+
return sorted(part).every((count) => {
|
|
1119
|
+
const position = left.indexOf(count);
|
|
1120
|
+
if (position === -1) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
left.splice(position, 1);
|
|
1124
|
+
return true;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
function accountedFor(total, parts) {
|
|
1128
|
+
const first = parts[0];
|
|
1129
|
+
if (total === void 0 || first === void 0) {
|
|
1130
|
+
return "unknown";
|
|
1131
|
+
}
|
|
1132
|
+
const lined = parts.every(
|
|
1133
|
+
(part) => part.s.length === first.s.length && part.f.length === first.f.length && part.b.length === first.b.length
|
|
1134
|
+
);
|
|
1135
|
+
if (!lined || total.f.length !== first.f.length || total.b.length !== first.b.length) {
|
|
1136
|
+
return "unknown";
|
|
1137
|
+
}
|
|
1138
|
+
const sum = (key) => first[key].map(
|
|
1139
|
+
(_count, index) => parts.reduce((added, part) => added + part[key][index], 0)
|
|
1140
|
+
);
|
|
1141
|
+
return same(sorted(total.f), sorted(sum("f"))) && same(sorted(total.b), sorted(sum("b"))) && contained(total.s, sum("s")) ? "yes" : "no";
|
|
1142
|
+
}
|
|
1143
|
+
function planControls(input) {
|
|
1144
|
+
const { all, targets, run, notes, totals } = input;
|
|
1145
|
+
const everyoneAlone = (reason) => ({ together: [], alone: targets, reason });
|
|
1146
|
+
if (input.failurePattern && !input.failurePattern.includes("{gate}")) {
|
|
1147
|
+
return everyoneAlone("settings.selfcheck.failurePattern does not contain {gate}");
|
|
1148
|
+
}
|
|
1149
|
+
if (run === null || notes === null || totals === null) {
|
|
1150
|
+
return everyoneAlone(
|
|
1151
|
+
"the gate run left no notes (the gate config does not use the helpers of this package, or Vitest measures with the v8 provider)"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const unverified = unverifiedRun(run);
|
|
1155
|
+
if (unverified !== null) {
|
|
1156
|
+
return everyoneAlone(unverified);
|
|
1157
|
+
}
|
|
1158
|
+
const silent = all.find((gate) => !notes.some((note) => note.spec === gate.specFile));
|
|
1159
|
+
if (silent !== void 0) {
|
|
1160
|
+
return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
|
|
1161
|
+
}
|
|
1162
|
+
const partsOf = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
|
|
1163
|
+
const short = all.find((gate) => accountedFor(totals[gate.file], partsOf(gate)) === "no");
|
|
1164
|
+
if (short !== void 0) {
|
|
1165
|
+
return everyoneAlone(
|
|
1166
|
+
`the notes of the specs do not add up to what the runner counted for ${short.path}`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
const together = targets.filter((target) => {
|
|
1170
|
+
const gate = all.find((candidate) => candidate.path === target);
|
|
1171
|
+
if (gate === void 0) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
if (accountedFor(totals[gate.file], partsOf(gate)) !== "yes") {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
const touching = notes.filter((note) => {
|
|
1181
|
+
const counters = note.files[gate.file];
|
|
1182
|
+
return counters !== void 0 && [...counters.s, ...counters.f, ...counters.b].some((count) => count !== 0);
|
|
1183
|
+
}).map((note) => note.spec);
|
|
1184
|
+
return touching.length === 1 && touching.includes(gate.specFile);
|
|
1185
|
+
});
|
|
1186
|
+
if (together.length < 2) {
|
|
1187
|
+
return everyoneAlone("fewer than two gates are touched by their own spec only");
|
|
1188
|
+
}
|
|
1189
|
+
return { together, alone: targets.filter((target) => !together.includes(target)), reason: null };
|
|
1190
|
+
}
|
|
949
1191
|
|
|
950
1192
|
// src/load.ts
|
|
951
1193
|
function readManifest(dir) {
|
|
@@ -970,9 +1212,12 @@ function readManifest(dir) {
|
|
|
970
1212
|
}
|
|
971
1213
|
throw error;
|
|
972
1214
|
}
|
|
1215
|
+
const listing = /* @__PURE__ */ new Map();
|
|
973
1216
|
const gates = parsed.gates.map((gate) => {
|
|
974
1217
|
const specCandidates = specCandidatesFor(gate.path, settings);
|
|
975
|
-
const existingSpecs = specCandidates.filter(
|
|
1218
|
+
const existingSpecs = specCandidates.filter(
|
|
1219
|
+
(candidate) => existsExact(absoluteDir, candidate, "file", listing)
|
|
1220
|
+
);
|
|
976
1221
|
return {
|
|
977
1222
|
...gate,
|
|
978
1223
|
specCandidates,
|
|
@@ -1020,6 +1265,7 @@ function isPlainRelativePath(file) {
|
|
|
1020
1265
|
function checkGates(manifest) {
|
|
1021
1266
|
const { dir, settings } = manifest;
|
|
1022
1267
|
const violations = [...manifest.violations];
|
|
1268
|
+
const listing = /* @__PURE__ */ new Map();
|
|
1023
1269
|
for (const gate of manifest.gates) {
|
|
1024
1270
|
if (!isPlainRelativePath(gate.path)) {
|
|
1025
1271
|
violations.push({
|
|
@@ -1047,7 +1293,7 @@ function checkGates(manifest) {
|
|
|
1047
1293
|
});
|
|
1048
1294
|
}
|
|
1049
1295
|
}
|
|
1050
|
-
if (!existsExact(dir, gate.path)) {
|
|
1296
|
+
if (!existsExact(dir, gate.path, "file", listing)) {
|
|
1051
1297
|
violations.push({
|
|
1052
1298
|
file: gate.path,
|
|
1053
1299
|
message: "file not found (the check is case-sensitive)"
|
|
@@ -1076,7 +1322,7 @@ function checkGates(manifest) {
|
|
|
1076
1322
|
violations.push(...scanSpecSource(gate.path, specPath, readText(dir, specPath), settings));
|
|
1077
1323
|
}
|
|
1078
1324
|
for (const candidate of manifest.candidates) {
|
|
1079
|
-
if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any")) {
|
|
1325
|
+
if (!isPlainRelativePath(candidate.path) || !existsExact(dir, candidate.path, "any", listing)) {
|
|
1080
1326
|
violations.push({
|
|
1081
1327
|
file: MANIFEST_FILE,
|
|
1082
1328
|
message: `${candidate.path}: candidate not found. Remove it, or fix the path`
|
|
@@ -1167,9 +1413,9 @@ function parseLcov(text, rootDir) {
|
|
|
1167
1413
|
const line = rawLine.trim();
|
|
1168
1414
|
if (line.startsWith("SF:")) {
|
|
1169
1415
|
closeRecord();
|
|
1170
|
-
const
|
|
1171
|
-
const file = files.get(
|
|
1172
|
-
files.set(
|
|
1416
|
+
const path11 = normalizePath(line.slice(3), rootDir);
|
|
1417
|
+
const file = files.get(path11) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
1418
|
+
files.set(path11, file);
|
|
1173
1419
|
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
1174
1420
|
} else if (line === "end_of_record") {
|
|
1175
1421
|
closeRecord();
|
|
@@ -1264,8 +1510,9 @@ function runLcov(dir, fileOption, io2) {
|
|
|
1264
1510
|
return 1;
|
|
1265
1511
|
}
|
|
1266
1512
|
const violations = [...manifest.violations];
|
|
1513
|
+
const listing = /* @__PURE__ */ new Map();
|
|
1267
1514
|
const existing = manifest.gates.filter((gate) => {
|
|
1268
|
-
const exists = existsExact(manifest.dir, gate.path);
|
|
1515
|
+
const exists = existsExact(manifest.dir, gate.path, "file", listing);
|
|
1269
1516
|
if (!exists) {
|
|
1270
1517
|
violations.push({
|
|
1271
1518
|
file: gate.path,
|
|
@@ -1298,12 +1545,437 @@ function runLcov(dir, fileOption, io2) {
|
|
|
1298
1545
|
}
|
|
1299
1546
|
|
|
1300
1547
|
// src/commands/mutation.ts
|
|
1301
|
-
import { spawnSync } from "child_process";
|
|
1302
|
-
import
|
|
1548
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1549
|
+
import fs7 from "fs";
|
|
1550
|
+
import path7 from "path";
|
|
1551
|
+
import { pathToFileURL } from "url";
|
|
1303
1552
|
|
|
1304
|
-
// src/
|
|
1553
|
+
// src/core/reuse-guard.ts
|
|
1554
|
+
var RECORD_FORMAT = 1;
|
|
1555
|
+
function isStringRecord(value) {
|
|
1556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1557
|
+
}
|
|
1558
|
+
function parseRecord(value) {
|
|
1559
|
+
const { format, incremental, snapshot } = value ?? {};
|
|
1560
|
+
if (format !== RECORD_FORMAT || typeof incremental !== "string") {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
if (typeof snapshot !== "object" || snapshot === null) {
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
const { mode, environment, args, files } = snapshot;
|
|
1567
|
+
if (mode !== "git" && mode !== "walk") {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
if (!isStringRecord(environment) || !isStringRecord(files)) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
return { format, incremental, snapshot: { mode, environment, args, files } };
|
|
1577
|
+
}
|
|
1578
|
+
function listed(names) {
|
|
1579
|
+
const shown = names.slice(0, 3).join(", ");
|
|
1580
|
+
return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
|
|
1581
|
+
}
|
|
1582
|
+
function differingKeys(before, after) {
|
|
1583
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).sort();
|
|
1584
|
+
}
|
|
1585
|
+
function refuseReuse(input) {
|
|
1586
|
+
if (input.record === void 0) {
|
|
1587
|
+
return "there is no record of the run that wrote the incremental file";
|
|
1588
|
+
}
|
|
1589
|
+
const record = parseRecord(input.record);
|
|
1590
|
+
if (record === null) {
|
|
1591
|
+
return "the record of the last run is not one this version can read";
|
|
1592
|
+
}
|
|
1593
|
+
if (record.incremental !== input.incremental) {
|
|
1594
|
+
return "the incremental file is not the one the last recorded run wrote";
|
|
1595
|
+
}
|
|
1596
|
+
const before = record.snapshot;
|
|
1597
|
+
const now = input.snapshot;
|
|
1598
|
+
if (before.mode !== now.mode) {
|
|
1599
|
+
return `the files are listed in another way than in the last run (${before.mode}, now ${now.mode})`;
|
|
1600
|
+
}
|
|
1601
|
+
const environment = differingKeys(before.environment, now.environment);
|
|
1602
|
+
if (environment.length > 0) {
|
|
1603
|
+
const changes = environment.map(
|
|
1604
|
+
(key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
|
|
1605
|
+
);
|
|
1606
|
+
return `the tools changed since the last run: ${listed(changes)}`;
|
|
1607
|
+
}
|
|
1608
|
+
if (JSON.stringify(before.args) !== JSON.stringify(now.args)) {
|
|
1609
|
+
return "the arguments for Stryker are not those of the last run";
|
|
1610
|
+
}
|
|
1611
|
+
const files = differingKeys(before.files, now.files);
|
|
1612
|
+
if (files.length > 0) {
|
|
1613
|
+
return `changed since the last run: ${listed(files)}`;
|
|
1614
|
+
}
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
function incrementalOptions(config, args) {
|
|
1618
|
+
const options = typeof config === "object" && config !== null ? config : {};
|
|
1619
|
+
const valueOf = (name) => {
|
|
1620
|
+
let value;
|
|
1621
|
+
args.forEach((arg, position) => {
|
|
1622
|
+
if (arg === name) {
|
|
1623
|
+
value = args[position + 1];
|
|
1624
|
+
} else if (arg.startsWith(`${name}=`)) {
|
|
1625
|
+
value = arg.slice(name.length + 1);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
return value;
|
|
1629
|
+
};
|
|
1630
|
+
const fromConfig = (name, fallback) => typeof options[name] === "string" ? options[name] : fallback;
|
|
1631
|
+
return {
|
|
1632
|
+
incremental: options.incremental === true || args.includes("--incremental"),
|
|
1633
|
+
force: options.force === true || args.includes("--force"),
|
|
1634
|
+
incrementalFile: valueOf("--incrementalFile") ?? fromConfig("incrementalFile", "reports/stryker-incremental.json"),
|
|
1635
|
+
tempDirName: valueOf("--tempDirName") ?? fromConfig("tempDirName", ".stryker-tmp")
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function comparableArgs(args) {
|
|
1639
|
+
return args.filter((arg) => arg !== "--force");
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/snapshot.ts
|
|
1643
|
+
import { spawnSync } from "child_process";
|
|
1644
|
+
import { createHash } from "crypto";
|
|
1305
1645
|
import fs5 from "fs";
|
|
1646
|
+
import { createRequire } from "module";
|
|
1647
|
+
import os from "os";
|
|
1306
1648
|
import path5 from "path";
|
|
1649
|
+
var SnapshotError = class extends Error {
|
|
1650
|
+
};
|
|
1651
|
+
var TOOLS = [
|
|
1652
|
+
"@stryker-mutator/core",
|
|
1653
|
+
"@stryker-mutator/jest-runner",
|
|
1654
|
+
"@stryker-mutator/vitest-runner",
|
|
1655
|
+
"@stryker-mutator/typescript-checker",
|
|
1656
|
+
"jest",
|
|
1657
|
+
"babel-jest",
|
|
1658
|
+
"ts-jest",
|
|
1659
|
+
"@swc/jest",
|
|
1660
|
+
"vitest",
|
|
1661
|
+
"vite",
|
|
1662
|
+
"@vitest/coverage-istanbul",
|
|
1663
|
+
"@vitest/coverage-v8",
|
|
1664
|
+
"typescript"
|
|
1665
|
+
];
|
|
1666
|
+
var INSTALL_MARKERS = [
|
|
1667
|
+
".package-lock.json",
|
|
1668
|
+
".yarn-integrity",
|
|
1669
|
+
".yarn-state.yml",
|
|
1670
|
+
".modules.yaml"
|
|
1671
|
+
];
|
|
1672
|
+
var LOCK_FILES = [
|
|
1673
|
+
"package-lock.json",
|
|
1674
|
+
"npm-shrinkwrap.json",
|
|
1675
|
+
"yarn.lock",
|
|
1676
|
+
"pnpm-lock.yaml",
|
|
1677
|
+
"bun.lock",
|
|
1678
|
+
"bun.lockb"
|
|
1679
|
+
];
|
|
1680
|
+
var NEVER = ["node_modules", ".git"];
|
|
1681
|
+
var SCAN_LIMIT = 8 * 1024 * 1024;
|
|
1682
|
+
var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
|
|
1683
|
+
function hashOf(content) {
|
|
1684
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1685
|
+
}
|
|
1686
|
+
function hashFile(file) {
|
|
1687
|
+
try {
|
|
1688
|
+
return hashOf(fs5.readFileSync(file));
|
|
1689
|
+
} catch {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
function isInside(parent, child) {
|
|
1694
|
+
const relative = path5.relative(parent, child);
|
|
1695
|
+
return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
|
|
1696
|
+
}
|
|
1697
|
+
function toPosix(file) {
|
|
1698
|
+
return file.split(path5.sep).join("/");
|
|
1699
|
+
}
|
|
1700
|
+
function git(cwd, args) {
|
|
1701
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 1024 * 1024 * 1024 });
|
|
1702
|
+
return result.error || result.status !== 0 ? null : result.stdout;
|
|
1703
|
+
}
|
|
1704
|
+
function gitLister(root) {
|
|
1705
|
+
return (directory, recursive) => {
|
|
1706
|
+
const relative = toPosix(path5.relative(root, directory));
|
|
1707
|
+
const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
|
|
1708
|
+
const listed2 = git(root, [
|
|
1709
|
+
"ls-files",
|
|
1710
|
+
"-z",
|
|
1711
|
+
"--cached",
|
|
1712
|
+
"--others",
|
|
1713
|
+
"--exclude-standard",
|
|
1714
|
+
"--",
|
|
1715
|
+
`:(glob)${pattern}`
|
|
1716
|
+
]);
|
|
1717
|
+
if (listed2 === null) {
|
|
1718
|
+
throw new SnapshotError("git could not list the files");
|
|
1719
|
+
}
|
|
1720
|
+
return listed2.split("\0").filter((file) => file !== "");
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
function walkLister(root) {
|
|
1724
|
+
const walk = (directory, recursive, found) => {
|
|
1725
|
+
for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
|
|
1726
|
+
const file = path5.join(directory, entry.name);
|
|
1727
|
+
if (NEVER.includes(entry.name)) {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (entry.isDirectory()) {
|
|
1731
|
+
if (recursive) {
|
|
1732
|
+
walk(file, true, found);
|
|
1733
|
+
}
|
|
1734
|
+
} else {
|
|
1735
|
+
found.push(toPosix(path5.relative(root, file)));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return found;
|
|
1739
|
+
};
|
|
1740
|
+
return (directory, recursive) => walk(directory, recursive, []);
|
|
1741
|
+
}
|
|
1742
|
+
function upTo(from, to) {
|
|
1743
|
+
const directories = [from];
|
|
1744
|
+
while (directories[directories.length - 1] !== to) {
|
|
1745
|
+
directories.push(path5.dirname(directories[directories.length - 1]));
|
|
1746
|
+
}
|
|
1747
|
+
return directories;
|
|
1748
|
+
}
|
|
1749
|
+
function declaresWorkspaces(directory) {
|
|
1750
|
+
if (fs5.existsSync(path5.join(directory, "pnpm-workspace.yaml"))) {
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const manifest = JSON.parse(fs5.readFileSync(path5.join(directory, "package.json"), "utf8"));
|
|
1755
|
+
return manifest.workspaces !== void 0;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function walkRoot(dir) {
|
|
1761
|
+
const candidates = withinBounds(dir);
|
|
1762
|
+
const repository = candidates.find((directory) => fs5.existsSync(path5.join(directory, ".git")));
|
|
1763
|
+
if (repository !== void 0) {
|
|
1764
|
+
return repository;
|
|
1765
|
+
}
|
|
1766
|
+
return candidates.filter(declaresWorkspaces).pop() ?? dir;
|
|
1767
|
+
}
|
|
1768
|
+
function withinBounds(dir) {
|
|
1769
|
+
let home = null;
|
|
1770
|
+
try {
|
|
1771
|
+
home = fs5.realpathSync(os.homedir());
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
1774
|
+
return upTo(dir, path5.parse(dir).root).filter(
|
|
1775
|
+
(directory) => directory !== path5.parse(dir).root && (home === null || !isInside(directory, home))
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
function linkedPackages(nodeModules, root) {
|
|
1779
|
+
const found = [];
|
|
1780
|
+
const visit = (directory, depth) => {
|
|
1781
|
+
let entries;
|
|
1782
|
+
try {
|
|
1783
|
+
entries = fs5.readdirSync(directory, { withFileTypes: true });
|
|
1784
|
+
} catch {
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
for (const entry of entries) {
|
|
1788
|
+
const file = path5.join(directory, entry.name);
|
|
1789
|
+
if (entry.isSymbolicLink()) {
|
|
1790
|
+
let target;
|
|
1791
|
+
try {
|
|
1792
|
+
target = fs5.realpathSync(file);
|
|
1793
|
+
} catch {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (target.split(path5.sep).includes("node_modules") || !fs5.statSync(target).isDirectory()) {
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (!isInside(root, target)) {
|
|
1800
|
+
throw new SnapshotError(
|
|
1801
|
+
`${file} is a link to a directory outside of what can be listed (${target})`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
found.push(target);
|
|
1805
|
+
} else if (depth === 0 && entry.isDirectory() && entry.name.startsWith("@")) {
|
|
1806
|
+
visit(file, 1);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
visit(nodeModules, 0);
|
|
1811
|
+
return found;
|
|
1812
|
+
}
|
|
1813
|
+
function installedVersion(start, name) {
|
|
1814
|
+
let current = start;
|
|
1815
|
+
for (; ; ) {
|
|
1816
|
+
try {
|
|
1817
|
+
const manifest = JSON.parse(
|
|
1818
|
+
fs5.readFileSync(path5.join(current, "node_modules", name, "package.json"), "utf8")
|
|
1819
|
+
);
|
|
1820
|
+
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
1821
|
+
} catch {
|
|
1822
|
+
const parent = path5.dirname(current);
|
|
1823
|
+
if (parent === current) {
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
current = parent;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function ownVersion() {
|
|
1831
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
1832
|
+
return version;
|
|
1833
|
+
}
|
|
1834
|
+
function takeSnapshot(input) {
|
|
1835
|
+
const dir = fs5.realpathSync(input.dir);
|
|
1836
|
+
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1837
|
+
let root = top === null ? dir : fs5.realpathSync(top.trim());
|
|
1838
|
+
let lister = gitLister(root);
|
|
1839
|
+
let mode = "git";
|
|
1840
|
+
const manifest = toPosix(path5.relative(root, path5.join(dir, "test-gates.json")));
|
|
1841
|
+
if (top === null || !lister(dir, false).includes(manifest)) {
|
|
1842
|
+
mode = "walk";
|
|
1843
|
+
root = walkRoot(dir);
|
|
1844
|
+
lister = walkLister(root);
|
|
1845
|
+
}
|
|
1846
|
+
const listRoot = root;
|
|
1847
|
+
const list = lister;
|
|
1848
|
+
const walk = walkLister(listRoot);
|
|
1849
|
+
const written = input.written.map((file) => {
|
|
1850
|
+
const target = path5.resolve(dir, file);
|
|
1851
|
+
try {
|
|
1852
|
+
return fs5.realpathSync(target);
|
|
1853
|
+
} catch {
|
|
1854
|
+
return target;
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
const swallowing = written.find((target) => isInside(target, path5.join(dir, "test-gates.json")));
|
|
1858
|
+
if (swallowing !== void 0) {
|
|
1859
|
+
throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
|
|
1860
|
+
}
|
|
1861
|
+
const files = /* @__PURE__ */ Object.create(null);
|
|
1862
|
+
const regions = [dir];
|
|
1863
|
+
const queued = new Set(regions);
|
|
1864
|
+
const enqueue = (target, from) => {
|
|
1865
|
+
const directory = isInside(target, dir) && isInside(listRoot, target) ? listRoot : target;
|
|
1866
|
+
if (!isInside(listRoot, directory)) {
|
|
1867
|
+
throw new SnapshotError(
|
|
1868
|
+
`${toPosix(path5.relative(dir, from))} points outside of what can be listed (${directory})`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
if (![...queued].some((region) => isInside(region, directory))) {
|
|
1872
|
+
queued.add(directory);
|
|
1873
|
+
regions.push(directory);
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
const follow = (literal, file) => {
|
|
1877
|
+
const fixed = literal.includes("${") ? literal.slice(0, literal.lastIndexOf("/", literal.indexOf("${")) + 1) : literal;
|
|
1878
|
+
if (!fixed.startsWith("../")) {
|
|
1879
|
+
enqueue(listRoot, file);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const target = path5.resolve(path5.dirname(file), fixed);
|
|
1883
|
+
let isDirectory = false;
|
|
1884
|
+
try {
|
|
1885
|
+
isDirectory = fs5.statSync(target).isDirectory();
|
|
1886
|
+
} catch {
|
|
1887
|
+
}
|
|
1888
|
+
const directory = isDirectory ? target : path5.dirname(target);
|
|
1889
|
+
if (fs5.existsSync(directory)) {
|
|
1890
|
+
enqueue(directory, file);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
const add = (relative) => {
|
|
1894
|
+
const file = path5.join(listRoot, relative);
|
|
1895
|
+
if (relative in files || written.some((target) => isInside(target, file))) {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
let stat;
|
|
1899
|
+
try {
|
|
1900
|
+
stat = fs5.lstatSync(file);
|
|
1901
|
+
} catch {
|
|
1902
|
+
files[relative] = "missing";
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (stat.isSymbolicLink()) {
|
|
1906
|
+
files[relative] = `link ${fs5.readlinkSync(file)}`;
|
|
1907
|
+
let target;
|
|
1908
|
+
try {
|
|
1909
|
+
target = fs5.realpathSync(file);
|
|
1910
|
+
} catch {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
enqueue(fs5.statSync(target).isDirectory() ? target : path5.dirname(target), file);
|
|
1914
|
+
} else if (stat.isDirectory()) {
|
|
1915
|
+
files[relative] = "directory";
|
|
1916
|
+
walk(file, true).forEach(add);
|
|
1917
|
+
} else if (!stat.isFile()) {
|
|
1918
|
+
files[relative] = "special";
|
|
1919
|
+
} else {
|
|
1920
|
+
const content = fs5.readFileSync(file);
|
|
1921
|
+
files[relative] = hashOf(content);
|
|
1922
|
+
if (content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
|
|
1923
|
+
const text = content.toString("utf8").replace(/\\[\\/]/g, "/");
|
|
1924
|
+
for (const match of text.matchAll(UPWARD)) {
|
|
1925
|
+
follow(match[2], file);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
const ancestors = upTo(dir, listRoot).slice(1);
|
|
1931
|
+
for (const directory of ancestors) {
|
|
1932
|
+
list(directory, false).forEach(add);
|
|
1933
|
+
}
|
|
1934
|
+
for (const directory of [dir, ...ancestors]) {
|
|
1935
|
+
for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
|
|
1936
|
+
if (!isInside(linked, dir)) {
|
|
1937
|
+
enqueue(linked, path5.join(directory, "node_modules"));
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const above = withinBounds(listRoot).slice(1);
|
|
1942
|
+
for (const directory of [dir, ...ancestors, ...above]) {
|
|
1943
|
+
const candidates = [
|
|
1944
|
+
...INSTALL_MARKERS.map((marker) => path5.join(directory, "node_modules", marker)),
|
|
1945
|
+
...above.includes(directory) ? LOCK_FILES.map((name) => path5.join(directory, name)) : []
|
|
1946
|
+
];
|
|
1947
|
+
for (const file of candidates) {
|
|
1948
|
+
const hash = hashFile(file);
|
|
1949
|
+
if (hash !== null) {
|
|
1950
|
+
files[toPosix(path5.relative(listRoot, file))] = hash;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
for (let position = 0; position < regions.length; position += 1) {
|
|
1955
|
+
list(regions[position], true).forEach(add);
|
|
1956
|
+
}
|
|
1957
|
+
const environment = {
|
|
1958
|
+
"Node.js": process.version,
|
|
1959
|
+
platform: `${process.platform} ${process.arch}`,
|
|
1960
|
+
"test-gates": ownVersion()
|
|
1961
|
+
};
|
|
1962
|
+
for (const name of TOOLS) {
|
|
1963
|
+
const version = installedVersion(dir, name);
|
|
1964
|
+
if (version !== null) {
|
|
1965
|
+
environment[name] = version;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
mode,
|
|
1970
|
+
environment,
|
|
1971
|
+
args: input.args,
|
|
1972
|
+
files
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/commands/mutation-result.ts
|
|
1977
|
+
import fs6 from "fs";
|
|
1978
|
+
import path6 from "path";
|
|
1307
1979
|
|
|
1308
1980
|
// src/core/mutation-result.ts
|
|
1309
1981
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -1413,11 +2085,11 @@ function judgeMutationReport(input) {
|
|
|
1413
2085
|
}
|
|
1414
2086
|
const located = [];
|
|
1415
2087
|
const byKey = /* @__PURE__ */ new Map();
|
|
1416
|
-
const
|
|
2088
|
+
const sorted2 = [...result.mutants].sort(
|
|
1417
2089
|
(a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
|
|
1418
2090
|
);
|
|
1419
2091
|
let locationsValid = true;
|
|
1420
|
-
for (const mutant of
|
|
2092
|
+
for (const mutant of sorted2) {
|
|
1421
2093
|
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
1422
2094
|
if (original === null) {
|
|
1423
2095
|
violations.push({
|
|
@@ -1553,15 +2225,17 @@ function formatSummary(summary) {
|
|
|
1553
2225
|
|
|
1554
2226
|
// src/commands/mutation-result.ts
|
|
1555
2227
|
function reportPathOf(manifest, override) {
|
|
1556
|
-
return
|
|
2228
|
+
return path6.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
|
|
1557
2229
|
}
|
|
1558
|
-
function runMutationResult(dir, reportOverride, io2) {
|
|
2230
|
+
function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
1559
2231
|
const manifest = readManifest(dir);
|
|
2232
|
+
const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
|
|
2233
|
+
const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
|
|
1560
2234
|
const reportPath = reportPathOf(manifest, reportOverride);
|
|
1561
|
-
const shown =
|
|
2235
|
+
const shown = path6.relative(manifest.dir, reportPath) || reportPath;
|
|
1562
2236
|
let report;
|
|
1563
2237
|
try {
|
|
1564
|
-
report = JSON.parse(
|
|
2238
|
+
report = JSON.parse(fs6.readFileSync(reportPath, "utf8"));
|
|
1565
2239
|
} catch (error) {
|
|
1566
2240
|
io2.err(
|
|
1567
2241
|
`test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
|
|
@@ -1573,12 +2247,12 @@ function runMutationResult(dir, reportOverride, io2) {
|
|
|
1573
2247
|
return 1;
|
|
1574
2248
|
}
|
|
1575
2249
|
const verdict = judgeMutationReport({
|
|
1576
|
-
gates
|
|
2250
|
+
gates,
|
|
1577
2251
|
report,
|
|
1578
2252
|
maxTimeouts: manifest.settings.maxTimeouts,
|
|
1579
2253
|
readSource: (gatePath) => {
|
|
1580
2254
|
try {
|
|
1581
|
-
return
|
|
2255
|
+
return fs6.readFileSync(path6.join(manifest.dir, gatePath), "utf8");
|
|
1582
2256
|
} catch {
|
|
1583
2257
|
return void 0;
|
|
1584
2258
|
}
|
|
@@ -1586,7 +2260,7 @@ function runMutationResult(dir, reportOverride, io2) {
|
|
|
1586
2260
|
});
|
|
1587
2261
|
const summary = formatSummary(verdict.summary);
|
|
1588
2262
|
if (verdict.violations.length === 0 && verdict.survivors.length === 0) {
|
|
1589
|
-
io2.out(`test-gates mutation: OK (${summary})`);
|
|
2263
|
+
io2.out(`test-gates mutation: OK (${summary})${caveat}`);
|
|
1590
2264
|
return 0;
|
|
1591
2265
|
}
|
|
1592
2266
|
if (verdict.survivors.length > 0) {
|
|
@@ -1638,13 +2312,133 @@ function runMutationResult(dir, reportOverride, io2) {
|
|
|
1638
2312
|
if (verdict.violations.length > 0) {
|
|
1639
2313
|
printViolations(io2, "mutation", verdict.violations);
|
|
1640
2314
|
}
|
|
1641
|
-
io2.err(`test-gates mutation: FAILED (${summary})`);
|
|
2315
|
+
io2.err(`test-gates mutation: FAILED (${summary})${caveat}`);
|
|
1642
2316
|
return 1;
|
|
1643
2317
|
}
|
|
1644
2318
|
|
|
1645
2319
|
// src/commands/mutation.ts
|
|
1646
|
-
|
|
2320
|
+
var CONFIG_MARK = "\0test-gates-stryker-config\0";
|
|
2321
|
+
function loadStrykerConfig(manifest, config) {
|
|
2322
|
+
const file = path7.resolve(manifest.dir, config);
|
|
2323
|
+
if (file.endsWith(".json")) {
|
|
2324
|
+
try {
|
|
2325
|
+
return JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
2326
|
+
} catch {
|
|
2327
|
+
return void 0;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
2331
|
+
const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
|
|
2332
|
+
cwd: manifest.dir,
|
|
2333
|
+
encoding: "utf8",
|
|
2334
|
+
timeout: 6e4
|
|
2335
|
+
});
|
|
2336
|
+
const printed = result.stdout?.split(CONFIG_MARK)[1];
|
|
2337
|
+
if (result.status !== 0 || printed === void 0) {
|
|
2338
|
+
return void 0;
|
|
2339
|
+
}
|
|
2340
|
+
return JSON.parse(printed);
|
|
2341
|
+
}
|
|
2342
|
+
function modifiedAt(file) {
|
|
2343
|
+
try {
|
|
2344
|
+
const stat = fs7.statSync(file, { bigint: true });
|
|
2345
|
+
return `${stat.mtimeNs} ${stat.ino}`;
|
|
2346
|
+
} catch {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
function guardRecordPath(incrementalFile) {
|
|
2351
|
+
return `${incrementalFile}.test-gates.json`;
|
|
2352
|
+
}
|
|
2353
|
+
function guardIncremental(manifest, config, strykerArgs, part) {
|
|
2354
|
+
const nothing = { force: null, record: () => void 0 };
|
|
2355
|
+
const loaded = loadStrykerConfig(manifest, config);
|
|
2356
|
+
const options = incrementalOptions(loaded, strykerArgs);
|
|
2357
|
+
if (loaded !== void 0 && !options.incremental) {
|
|
2358
|
+
return nothing;
|
|
2359
|
+
}
|
|
2360
|
+
const base = fs7.realpathSync(manifest.dir);
|
|
2361
|
+
const incrementalFile = path7.resolve(base, options.incrementalFile);
|
|
2362
|
+
const recordFile = guardRecordPath(incrementalFile);
|
|
2363
|
+
let snapshot = null;
|
|
2364
|
+
let failure = "the Stryker config could not be loaded to see where the incremental file is";
|
|
2365
|
+
if (loaded !== void 0) {
|
|
2366
|
+
try {
|
|
2367
|
+
snapshot = takeSnapshot({
|
|
2368
|
+
dir: manifest.dir,
|
|
2369
|
+
written: [
|
|
2370
|
+
incrementalFile,
|
|
2371
|
+
recordFile,
|
|
2372
|
+
path7.resolve(base, options.tempDirName),
|
|
2373
|
+
path7.resolve(base, manifest.settings.strykerReportFile)
|
|
2374
|
+
],
|
|
2375
|
+
args: comparableArgs(strykerArgs)
|
|
2376
|
+
});
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
failure = `the files the run depends on could not be listed (${error.message})`;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
let force = null;
|
|
2382
|
+
const incremental = hashFile(incrementalFile);
|
|
2383
|
+
if (part) {
|
|
2384
|
+
fs7.rmSync(recordFile, { force: true });
|
|
2385
|
+
return {
|
|
2386
|
+
force: incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null,
|
|
2387
|
+
record: () => void 0
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
if (incremental !== null && !options.force) {
|
|
2391
|
+
if (snapshot === null) {
|
|
2392
|
+
force = failure;
|
|
2393
|
+
} else {
|
|
2394
|
+
let record;
|
|
2395
|
+
try {
|
|
2396
|
+
record = JSON.parse(fs7.readFileSync(recordFile, "utf8"));
|
|
2397
|
+
} catch {
|
|
2398
|
+
record = void 0;
|
|
2399
|
+
}
|
|
2400
|
+
force = refuseReuse({ record, incremental, snapshot });
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
fs7.rmSync(recordFile, { force: true });
|
|
2404
|
+
const before = modifiedAt(incrementalFile);
|
|
2405
|
+
return {
|
|
2406
|
+
force,
|
|
2407
|
+
record: () => {
|
|
2408
|
+
const written = hashFile(incrementalFile);
|
|
2409
|
+
if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const record = { format: RECORD_FORMAT, incremental: written, snapshot };
|
|
2413
|
+
try {
|
|
2414
|
+
fs7.writeFileSync(recordFile, `${JSON.stringify(record)}
|
|
2415
|
+
`);
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function runMutation(dir, strykerArgs, io2, only = []) {
|
|
2422
|
+
const part = only.length > 0;
|
|
2423
|
+
if (part) {
|
|
2424
|
+
const refused = gateRunRefused(process.env);
|
|
2425
|
+
if (refused !== null) {
|
|
2426
|
+
throw new UsageError(refused);
|
|
2427
|
+
}
|
|
2428
|
+
if (strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
2429
|
+
throw new UsageError("--gate and --mutate cannot be combined: --gate sets --mutate itself");
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
1647
2432
|
const manifest = readManifest(dir);
|
|
2433
|
+
const chosen = selectControls({
|
|
2434
|
+
gates: manifest.gates.map((gate) => gate.path),
|
|
2435
|
+
mode: "all",
|
|
2436
|
+
only,
|
|
2437
|
+
shard: null
|
|
2438
|
+
});
|
|
2439
|
+
if ("error" in chosen) {
|
|
2440
|
+
throw new UsageError(chosen.error);
|
|
2441
|
+
}
|
|
1648
2442
|
const config = findStrykerConfig(manifest);
|
|
1649
2443
|
if (config === null) {
|
|
1650
2444
|
throw new ManifestError(
|
|
@@ -1657,8 +2451,17 @@ function runMutation(dir, strykerArgs, io2) {
|
|
|
1657
2451
|
"stryker is not installed in this project (node_modules/.bin/stryker). Add @stryker-mutator/core as a devDependency"
|
|
1658
2452
|
);
|
|
1659
2453
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
2454
|
+
const reportFile = reportPathOf(manifest, null);
|
|
2455
|
+
fs7.rmSync(reportFile, { force: true });
|
|
2456
|
+
const guard = guardIncremental(manifest, config, strykerArgs, part);
|
|
2457
|
+
if (guard.force !== null) {
|
|
2458
|
+
io2.out(
|
|
2459
|
+
`test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
const guardArgs = guard.force === null ? [] : ["--force"];
|
|
2463
|
+
const partArgs = part ? ["--mutate", chosen.selected.join(",")] : [];
|
|
2464
|
+
const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
|
|
1662
2465
|
cwd: manifest.dir,
|
|
1663
2466
|
stdio: "inherit",
|
|
1664
2467
|
shell: process.platform === "win32"
|
|
@@ -1673,13 +2476,392 @@ function runMutation(dir, strykerArgs, io2) {
|
|
|
1673
2476
|
);
|
|
1674
2477
|
return 1;
|
|
1675
2478
|
}
|
|
1676
|
-
|
|
2479
|
+
guard.record();
|
|
2480
|
+
const status = runMutationResult(dir, null, io2, part ? chosen.selected : []);
|
|
2481
|
+
if (part) {
|
|
2482
|
+
fs7.rmSync(reportFile, { force: true });
|
|
2483
|
+
}
|
|
2484
|
+
return status;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/commands/selfcheck.ts
|
|
2488
|
+
import { spawn, spawnSync as spawnSync3 } from "child_process";
|
|
2489
|
+
import fs10 from "fs";
|
|
2490
|
+
import os2 from "os";
|
|
2491
|
+
import path10 from "path";
|
|
2492
|
+
|
|
2493
|
+
// src/core/jobs.ts
|
|
2494
|
+
var SWITCHES = {
|
|
2495
|
+
jest: ["--runInBand", "-i", "--ci"],
|
|
2496
|
+
vitest: ["--no-file-parallelism", "--fileParallelism=false", "--pool=forks", "--pool=threads"]
|
|
2497
|
+
};
|
|
2498
|
+
var COUNTS = {
|
|
2499
|
+
jest: ["--maxWorkers", "-w"],
|
|
2500
|
+
vitest: ["--maxWorkers", "--minWorkers"]
|
|
2501
|
+
};
|
|
2502
|
+
var COUNT = /^\d+%?$/;
|
|
2503
|
+
function knownCommand(program, args) {
|
|
2504
|
+
const runner = ["jest", "vitest"].find((name) => name === program);
|
|
2505
|
+
if (runner === void 0) {
|
|
2506
|
+
return { stray: program };
|
|
2507
|
+
}
|
|
2508
|
+
if (runner === "vitest" && args[0] !== "run") {
|
|
2509
|
+
return { stray: args[0] ?? 'vitest without "run"' };
|
|
2510
|
+
}
|
|
2511
|
+
const rest = runner === "vitest" ? args.slice(1) : [...args];
|
|
2512
|
+
let configs = 0;
|
|
2513
|
+
for (let arg = rest.shift(); arg !== void 0; arg = rest.shift()) {
|
|
2514
|
+
if (SWITCHES[runner].includes(arg)) {
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
const equals = arg.indexOf("=");
|
|
2518
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
2519
|
+
const value = equals === -1 ? rest.shift() : arg.slice(equals + 1);
|
|
2520
|
+
if (value !== void 0 && (name === "--config" || name === "-c")) {
|
|
2521
|
+
configs += 1;
|
|
2522
|
+
} else if (!COUNTS[runner].includes(name) || !COUNT.test(String(value))) {
|
|
2523
|
+
return { stray: arg };
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return configs === 1 ? { runner } : { stray: configs === 0 ? "no --config" : "two --config" };
|
|
2527
|
+
}
|
|
2528
|
+
function jobFlags(runner, dir) {
|
|
2529
|
+
return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
|
|
2530
|
+
}
|
|
2531
|
+
var REACHES = [
|
|
2532
|
+
"process.",
|
|
2533
|
+
"process?.",
|
|
2534
|
+
"process[",
|
|
2535
|
+
"globalThis",
|
|
2536
|
+
"global.",
|
|
2537
|
+
"global[",
|
|
2538
|
+
"import.meta.env",
|
|
2539
|
+
"Date.now",
|
|
2540
|
+
"Date()",
|
|
2541
|
+
"performance.now",
|
|
2542
|
+
"setTimeout",
|
|
2543
|
+
"setInterval",
|
|
2544
|
+
"setImmediate",
|
|
2545
|
+
"waitFor",
|
|
2546
|
+
"waitUntil",
|
|
2547
|
+
"expect.poll",
|
|
2548
|
+
"retryTimes",
|
|
2549
|
+
"retry:",
|
|
2550
|
+
"fetch(",
|
|
2551
|
+
"WebSocket",
|
|
2552
|
+
"XMLHttpRequest",
|
|
2553
|
+
"navigator",
|
|
2554
|
+
"Atomics",
|
|
2555
|
+
"WeakRef",
|
|
2556
|
+
"FinalizationRegistry",
|
|
2557
|
+
"eval(",
|
|
2558
|
+
"Function("
|
|
2559
|
+
];
|
|
2560
|
+
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])/;
|
|
2561
|
+
var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
|
|
2562
|
+
function reachesOutside(file, trusted) {
|
|
2563
|
+
const dense = file.source.replace(/\s/g, "");
|
|
2564
|
+
const mark = REACHES.find((text) => dense.includes(text));
|
|
2565
|
+
if (mark !== void 0) {
|
|
2566
|
+
return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
|
|
2567
|
+
}
|
|
2568
|
+
if (COMPUTED_LOAD.test(dense)) {
|
|
2569
|
+
return "loads a module whose name is computed";
|
|
2570
|
+
}
|
|
2571
|
+
for (const { specifier, builtin } of file.imports) {
|
|
2572
|
+
if (builtin) {
|
|
2573
|
+
return `imports ${specifier}`;
|
|
2574
|
+
} else if (![...RUNNER_PACKAGES, ...trusted].some(
|
|
2575
|
+
(name) => specifier === name || specifier.startsWith(`${name}/`)
|
|
2576
|
+
)) {
|
|
2577
|
+
return `imports the package ${specifier}, which is not looked into`;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
function jobsRefused(input) {
|
|
2583
|
+
if (input.failurePattern) {
|
|
2584
|
+
return "with settings.selfcheck.failurePattern, a run that was disturbed cannot be told from one that was not";
|
|
2585
|
+
}
|
|
2586
|
+
if ("stray" in input.command) {
|
|
2587
|
+
return `the gate command is not known to keep runs at the same time apart: only jest or "vitest run" with --config and options for the number of workers is (found: ${input.command.stray})`;
|
|
2588
|
+
}
|
|
2589
|
+
const helper = input.command.runner === "jest" ? "createJestGatesConfig" : "createVitestGatesConfig";
|
|
2590
|
+
if (input.configSource === null || !input.configSource.includes(helper)) {
|
|
2591
|
+
return `the gate config does not use ${helper}`;
|
|
2592
|
+
}
|
|
2593
|
+
if (input.lost !== null) {
|
|
2594
|
+
return `${input.lost} cannot be followed, so what the gate run loads is not known`;
|
|
2595
|
+
}
|
|
2596
|
+
for (const file of input.files) {
|
|
2597
|
+
const reach = reachesOutside(file, input.trusted);
|
|
2598
|
+
if (reach !== null) {
|
|
2599
|
+
return `${file.path} ${reach}, and may meet what another run is doing`;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
var TROUBLE = [
|
|
2605
|
+
"fail",
|
|
2606
|
+
"fails",
|
|
2607
|
+
"failed",
|
|
2608
|
+
"failing",
|
|
2609
|
+
"failure",
|
|
2610
|
+
"failures",
|
|
2611
|
+
"err",
|
|
2612
|
+
"timeout",
|
|
2613
|
+
"timeouts",
|
|
2614
|
+
"timed",
|
|
2615
|
+
"unhandled",
|
|
2616
|
+
"uncaught",
|
|
2617
|
+
"killed",
|
|
2618
|
+
"terminated",
|
|
2619
|
+
"crashed",
|
|
2620
|
+
"aborted",
|
|
2621
|
+
"skipped",
|
|
2622
|
+
"todo",
|
|
2623
|
+
"retry",
|
|
2624
|
+
"retried",
|
|
2625
|
+
"obsolete",
|
|
2626
|
+
"sigkill",
|
|
2627
|
+
"sigterm",
|
|
2628
|
+
"sigabrt",
|
|
2629
|
+
"sigsegv",
|
|
2630
|
+
"sigint"
|
|
2631
|
+
];
|
|
2632
|
+
var TROUBLE_SIGNS = ["\u2717", "\xD7", "\u2715", "\u2716"];
|
|
2633
|
+
function troubled(line) {
|
|
2634
|
+
const words = line.toLowerCase().split(/[^a-z]/);
|
|
2635
|
+
return (
|
|
2636
|
+
// (`TypeError`, `errors`)
|
|
2637
|
+
words.some((word) => TROUBLE.includes(word) || /errors?$/.test(word)) || TROUBLE_SIGNS.some((sign) => line.includes(sign))
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
function plain(line) {
|
|
2641
|
+
return line.split("\x1B[").map((part, index) => index === 0 ? part : part.slice(part.indexOf("m") + 1)).join("").trim();
|
|
2642
|
+
}
|
|
2643
|
+
var SUMMARY = {
|
|
2644
|
+
jest: {
|
|
2645
|
+
files: "Test Suites:",
|
|
2646
|
+
tests: "Tests:",
|
|
2647
|
+
all: (count) => `${count} passed, ${count} total`
|
|
2648
|
+
},
|
|
2649
|
+
vitest: { files: "Test Files", tests: "Tests", all: (count) => `${count} passed (${count})` }
|
|
2650
|
+
};
|
|
2651
|
+
function cleanHold(input) {
|
|
2652
|
+
const { output, gatePath, others } = input;
|
|
2653
|
+
if (!input.held || input.signal !== null) {
|
|
2654
|
+
return false;
|
|
2655
|
+
}
|
|
2656
|
+
const lines = output.split("\n").map(plain);
|
|
2657
|
+
const summary = SUMMARY[input.runner];
|
|
2658
|
+
const after = (label) => lines.filter((line) => line.startsWith(label)).map((line) => line.slice(label.length).trim());
|
|
2659
|
+
const files = after(summary.files);
|
|
2660
|
+
const tests = after(summary.tests);
|
|
2661
|
+
const count = (tests[0] ?? "").split(" ")[0];
|
|
2662
|
+
if (files.length !== 1 || files[0] !== summary.all(String(input.specs)) || tests.length !== 1 || !/^\d+$/.test(count) || tests[0] !== summary.all(count)) {
|
|
2663
|
+
return false;
|
|
2664
|
+
}
|
|
2665
|
+
return lines.every((line) => {
|
|
2666
|
+
const lower = line.toLowerCase();
|
|
2667
|
+
if (lower.includes("threshold") || lower.includes("coverage data for")) {
|
|
2668
|
+
return line.includes(gatePath) && !others.some((other) => line.includes(other));
|
|
2669
|
+
}
|
|
2670
|
+
return line.split("|").length >= 5 || // What stands around the names of the project, piece by piece.
|
|
2671
|
+
!input.names.reduce((pieces, name) => pieces.flatMap((piece) => piece.split(name)), [line]).some(troubled);
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// src/touch.ts
|
|
2676
|
+
import { createHash as createHash2 } from "crypto";
|
|
2677
|
+
import fs8 from "fs";
|
|
2678
|
+
import path8 from "path";
|
|
2679
|
+
import { fileURLToPath } from "url";
|
|
2680
|
+
var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
|
|
2681
|
+
function isObject(value) {
|
|
2682
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2683
|
+
}
|
|
2684
|
+
function countersOf(coverage) {
|
|
2685
|
+
return {
|
|
2686
|
+
s: Object.values(coverage.s ?? {}),
|
|
2687
|
+
f: Object.values(coverage.f ?? {}),
|
|
2688
|
+
b: Object.values(coverage.b ?? {}).flat()
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
function countersOfAll(coverage) {
|
|
2692
|
+
if (!isObject(coverage)) {
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
const files = {};
|
|
2696
|
+
for (const [file, counters] of Object.entries(coverage)) {
|
|
2697
|
+
if (!isObject(counters)) {
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2700
|
+
files[file] = countersOf(counters);
|
|
2701
|
+
}
|
|
2702
|
+
return files;
|
|
2703
|
+
}
|
|
2704
|
+
function isCounters(value) {
|
|
2705
|
+
return isObject(value) && ["s", "f", "b"].every(
|
|
2706
|
+
(key) => Array.isArray(value[key]) && value[key].every((count) => typeof count === "number")
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
function readNotes(directory) {
|
|
2710
|
+
const read = (file) => {
|
|
2711
|
+
try {
|
|
2712
|
+
return JSON.parse(fs8.readFileSync(path8.join(directory, file), "utf8"));
|
|
2713
|
+
} catch {
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
const run = read("run.json");
|
|
2718
|
+
let notes = [];
|
|
2719
|
+
try {
|
|
2720
|
+
for (const name of fs8.readdirSync(path8.join(directory, "specs"))) {
|
|
2721
|
+
const note = read(path8.join("specs", name));
|
|
2722
|
+
if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
|
|
2723
|
+
throw new Error("not a note");
|
|
2724
|
+
}
|
|
2725
|
+
notes.push(note);
|
|
2726
|
+
}
|
|
2727
|
+
} catch {
|
|
2728
|
+
notes = null;
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
run: isObject(run) && typeof run.runner === "string" && typeof run.version === "string" && typeof run.provider === "string" && typeof run.pool === "string" && typeof run.isolate === "boolean" && typeof run.projects === "number" && (run.problem === null || typeof run.problem === "string") ? run : null,
|
|
2732
|
+
notes,
|
|
2733
|
+
totals: countersOfAll(read(path8.join("coverage", "coverage-final.json")))
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/commands/loaded-files.ts
|
|
2738
|
+
import fs9 from "fs";
|
|
2739
|
+
import { builtinModules } from "module";
|
|
2740
|
+
import path9 from "path";
|
|
2741
|
+
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
2742
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2743
|
+
var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
2744
|
+
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
2745
|
+
function isFile(file) {
|
|
2746
|
+
try {
|
|
2747
|
+
return fs9.statSync(file).isFile();
|
|
2748
|
+
} catch {
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function resolveFile(base, directory = true) {
|
|
2753
|
+
const candidates = [
|
|
2754
|
+
base,
|
|
2755
|
+
...EXTENSIONS.map((extension) => base + extension),
|
|
2756
|
+
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
2757
|
+
...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
2758
|
+
...directory ? EXTENSIONS.map((extension) => path9.join(base, `index${extension}`)) : []
|
|
2759
|
+
];
|
|
2760
|
+
return candidates.find(isFile) ?? null;
|
|
2761
|
+
}
|
|
2762
|
+
function installed(specifier, from) {
|
|
2763
|
+
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2764
|
+
for (let current = path9.dirname(from); ; current = path9.dirname(current)) {
|
|
2765
|
+
if (fs9.existsSync(path9.join(current, "node_modules", name))) {
|
|
2766
|
+
return true;
|
|
2767
|
+
}
|
|
2768
|
+
if (current === path9.dirname(current)) {
|
|
2769
|
+
return false;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function loadedFiles(manifest, configFile) {
|
|
2774
|
+
const aliases = manifest.settings.importAliases;
|
|
2775
|
+
const relative = (file) => path9.relative(manifest.dir, file).split(path9.sep).join("/");
|
|
2776
|
+
const target = (specifier, from) => {
|
|
2777
|
+
if (specifier.startsWith(".")) {
|
|
2778
|
+
return resolveFile(path9.resolve(path9.dirname(from), specifier));
|
|
2779
|
+
}
|
|
2780
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2781
|
+
return alias === void 0 ? null : resolveFile(
|
|
2782
|
+
path9.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
2783
|
+
);
|
|
2784
|
+
};
|
|
2785
|
+
const read = (file) => {
|
|
2786
|
+
const source = fs9.readFileSync(file, "utf8");
|
|
2787
|
+
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
2788
|
+
const specifiers = [
|
|
2789
|
+
.../* @__PURE__ */ new Set([
|
|
2790
|
+
...imports.map((ref) => ref.module),
|
|
2791
|
+
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
2792
|
+
])
|
|
2793
|
+
];
|
|
2794
|
+
const strings = [...source.matchAll(QUOTED)].map((match) => match[2]).filter((text) => !specifiers.includes(text));
|
|
2795
|
+
return { source, specifiers, strings };
|
|
2796
|
+
};
|
|
2797
|
+
const named = /* @__PURE__ */ new Set();
|
|
2798
|
+
const configs = configFile === null ? [] : [path9.resolve(manifest.dir, configFile)];
|
|
2799
|
+
const seenConfigs = new Set(configs);
|
|
2800
|
+
const entry = configs[0];
|
|
2801
|
+
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
2802
|
+
const { specifiers, strings } = read(file);
|
|
2803
|
+
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
2804
|
+
for (const text of paths) {
|
|
2805
|
+
const stripped = text.replace(/^<rootDir>\//, "");
|
|
2806
|
+
const found = resolveFile(path9.resolve(path9.dirname(file), stripped), false) ?? resolveFile(path9.resolve(manifest.dir, stripped), false);
|
|
2807
|
+
if (found !== null && SCRIPT.test(found)) {
|
|
2808
|
+
named.add(found);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
|
|
2812
|
+
const found = target(specifier, file);
|
|
2813
|
+
if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
|
|
2814
|
+
seenConfigs.add(found);
|
|
2815
|
+
configs.push(found);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
const queue = [
|
|
2820
|
+
...manifest.gates.flatMap(
|
|
2821
|
+
(gate) => gate.existingSpecs.map((spec) => path9.join(manifest.dir, spec))
|
|
2822
|
+
),
|
|
2823
|
+
...named
|
|
2824
|
+
];
|
|
2825
|
+
const seen = new Set(queue);
|
|
2826
|
+
const follow = (found) => {
|
|
2827
|
+
if (SCRIPT.test(found) && !seen.has(found)) {
|
|
2828
|
+
seen.add(found);
|
|
2829
|
+
queue.push(found);
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
const files = [];
|
|
2833
|
+
let lost = null;
|
|
2834
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2835
|
+
const { source, specifiers, strings } = read(file);
|
|
2836
|
+
const imports = [];
|
|
2837
|
+
for (const specifier of specifiers) {
|
|
2838
|
+
if (isProjectImport(specifier, aliases)) {
|
|
2839
|
+
const found = target(specifier, file);
|
|
2840
|
+
if (found === null) {
|
|
2841
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
2842
|
+
} else {
|
|
2843
|
+
follow(found);
|
|
2844
|
+
}
|
|
2845
|
+
} else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
|
|
2846
|
+
imports.push({ specifier, builtin: true });
|
|
2847
|
+
} else if (installed(specifier, file)) {
|
|
2848
|
+
imports.push({ specifier, builtin: false });
|
|
2849
|
+
} else {
|
|
2850
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
2854
|
+
const found = resolveFile(path9.resolve(path9.dirname(file), text));
|
|
2855
|
+
if (found !== null) {
|
|
2856
|
+
follow(found);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
files.push({ path: relative(file), source, imports });
|
|
2860
|
+
}
|
|
2861
|
+
return { files, lost };
|
|
1677
2862
|
}
|
|
1678
2863
|
|
|
1679
2864
|
// src/commands/selfcheck.ts
|
|
1680
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
1681
|
-
import fs7 from "fs";
|
|
1682
|
-
import path6 from "path";
|
|
1683
2865
|
var JEST_CONFIGS = ["js", "cjs", "mjs", "ts", "cts", "mts"].map(
|
|
1684
2866
|
(extension) => `jest.gates.config.${extension}`
|
|
1685
2867
|
);
|
|
@@ -1690,7 +2872,7 @@ function resolveGateCommand(manifest) {
|
|
|
1690
2872
|
if (manifest.settings.gateCommand !== null) {
|
|
1691
2873
|
return manifest.settings.gateCommand;
|
|
1692
2874
|
}
|
|
1693
|
-
const exists = (name) =>
|
|
2875
|
+
const exists = (name) => fs10.existsSync(path10.join(manifest.dir, name));
|
|
1694
2876
|
const jest = JEST_CONFIGS.find(exists);
|
|
1695
2877
|
const vitest = VITEST_CONFIGS.find(exists);
|
|
1696
2878
|
if (jest && vitest) {
|
|
@@ -1708,16 +2890,259 @@ function resolveGateCommand(manifest) {
|
|
|
1708
2890
|
"no gate config found (jest.gates.config.* / vitest.gates.config.*). Set settings.gateCommand"
|
|
1709
2891
|
);
|
|
1710
2892
|
}
|
|
1711
|
-
|
|
2893
|
+
var TOUCH_PREFIX = ".test-gates-touch-";
|
|
2894
|
+
function realPath(file) {
|
|
2895
|
+
try {
|
|
2896
|
+
return fs10.realpathSync(file);
|
|
2897
|
+
} catch {
|
|
2898
|
+
return file;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function shareOneRun(manifest, targets, runGate) {
|
|
2902
|
+
let touchDir;
|
|
2903
|
+
try {
|
|
2904
|
+
for (const name of fs10.readdirSync(manifest.dir)) {
|
|
2905
|
+
const left = path10.join(manifest.dir, name);
|
|
2906
|
+
if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs10.statSync(left).mtimeMs > 36e5) {
|
|
2907
|
+
fs10.rmSync(left, { recursive: true, force: true });
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
touchDir = fs10.mkdtempSync(path10.join(manifest.dir, TOUCH_PREFIX));
|
|
2911
|
+
} catch {
|
|
2912
|
+
return { reason: "the subproject cannot be written to" };
|
|
2913
|
+
}
|
|
2914
|
+
let plan;
|
|
2915
|
+
try {
|
|
2916
|
+
const recorded = runGate({ touchDir });
|
|
2917
|
+
if (recorded.status !== 0) {
|
|
2918
|
+
return { reason: "the gate run with every spec does not pass" };
|
|
2919
|
+
}
|
|
2920
|
+
const base = realPath(manifest.dir);
|
|
2921
|
+
const absolute = (file) => realPath(path10.resolve(base, file));
|
|
2922
|
+
const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
|
|
2923
|
+
const left = readNotes(touchDir);
|
|
2924
|
+
plan = planControls({
|
|
2925
|
+
all: manifest.gates.map((gate) => ({
|
|
2926
|
+
path: gate.path,
|
|
2927
|
+
file: path10.join(base, gate.path),
|
|
2928
|
+
specFile: path10.join(base, gate.spec ?? "")
|
|
2929
|
+
})),
|
|
2930
|
+
targets: targets.map((gate) => gate.path),
|
|
2931
|
+
run: left.run,
|
|
2932
|
+
notes: left.notes?.map((note) => ({ spec: absolute(note.spec), files: rekeyed(note.files) })) ?? null,
|
|
2933
|
+
totals: left.totals === null ? null : rekeyed(left.totals),
|
|
2934
|
+
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
2935
|
+
});
|
|
2936
|
+
} finally {
|
|
2937
|
+
fs10.rmSync(touchDir, { recursive: true, force: true });
|
|
2938
|
+
}
|
|
2939
|
+
if (plan.reason !== null) {
|
|
2940
|
+
return { reason: plan.reason };
|
|
2941
|
+
}
|
|
2942
|
+
const together = plan.together;
|
|
2943
|
+
const gates = targets.filter((gate) => together.includes(gate.path));
|
|
2944
|
+
return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
|
|
2945
|
+
}
|
|
2946
|
+
function installedVersion2(start, name) {
|
|
2947
|
+
let current = path10.resolve(start);
|
|
2948
|
+
for (; ; ) {
|
|
2949
|
+
try {
|
|
2950
|
+
const manifest = JSON.parse(
|
|
2951
|
+
fs10.readFileSync(path10.join(current, "node_modules", name, "package.json"), "utf8")
|
|
2952
|
+
);
|
|
2953
|
+
return typeof manifest.version === "string" ? manifest.version : null;
|
|
2954
|
+
} catch {
|
|
2955
|
+
const parent = path10.dirname(current);
|
|
2956
|
+
if (parent === current) {
|
|
2957
|
+
return null;
|
|
2958
|
+
}
|
|
2959
|
+
current = parent;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
function gateConfigFile(args) {
|
|
2964
|
+
const joined = args.find((arg) => arg.startsWith("--config="));
|
|
2965
|
+
const position = args.findIndex((arg) => arg === "--config" || arg === "-c");
|
|
2966
|
+
return joined?.slice("--config=".length) ?? (position === -1 ? null : args[position + 1] ?? null);
|
|
2967
|
+
}
|
|
2968
|
+
function gateConfigSource(manifest, args) {
|
|
2969
|
+
const file = gateConfigFile(args);
|
|
2970
|
+
if (file === null) {
|
|
2971
|
+
return null;
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
return readText(manifest.dir, file);
|
|
2975
|
+
} catch {
|
|
2976
|
+
return null;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
var JOBS_PREFIX = "test-gates-jobs-";
|
|
2980
|
+
async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
2981
|
+
const results = /* @__PURE__ */ new Map();
|
|
2982
|
+
let root;
|
|
2983
|
+
try {
|
|
2984
|
+
for (const name of fs10.readdirSync(os2.tmpdir())) {
|
|
2985
|
+
const left = path10.join(os2.tmpdir(), name);
|
|
2986
|
+
if (name.startsWith(JOBS_PREFIX) && Date.now() - fs10.statSync(left).mtimeMs > 36e5) {
|
|
2987
|
+
fs10.rmSync(left, { recursive: true, force: true });
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
root = fs10.realpathSync(fs10.mkdtempSync(path10.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
2991
|
+
} catch {
|
|
2992
|
+
return results;
|
|
2993
|
+
}
|
|
2994
|
+
const queue = [...gates];
|
|
2995
|
+
const line = async (dir) => {
|
|
2996
|
+
for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
|
|
2997
|
+
const spec = gate.spec;
|
|
2998
|
+
results.set(
|
|
2999
|
+
gate,
|
|
3000
|
+
await new Promise((resolve) => {
|
|
3001
|
+
const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
|
|
3002
|
+
cwd: manifest.dir,
|
|
3003
|
+
env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
|
|
3004
|
+
shell: process.platform === "win32"
|
|
3005
|
+
});
|
|
3006
|
+
let stdout = "";
|
|
3007
|
+
let stderr = "";
|
|
3008
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
3009
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
3010
|
+
child.stdin.on("error", () => void 0);
|
|
3011
|
+
child.stdin.end();
|
|
3012
|
+
child.on("error", () => resolve({ status: null, signal: null, stdout, stderr }));
|
|
3013
|
+
child.on("close", (status, signal) => resolve({ status, signal, stdout, stderr }));
|
|
3014
|
+
})
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
try {
|
|
3019
|
+
await Promise.all(
|
|
3020
|
+
Array.from(
|
|
3021
|
+
{ length: Math.min(jobs, gates.length) },
|
|
3022
|
+
(_unused, index) => line(path10.join(root, `line-${index}`))
|
|
3023
|
+
)
|
|
3024
|
+
);
|
|
3025
|
+
} finally {
|
|
3026
|
+
fs10.rmSync(root, { recursive: true, force: true });
|
|
3027
|
+
}
|
|
3028
|
+
return results;
|
|
3029
|
+
}
|
|
3030
|
+
async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
3031
|
+
const refused = selection.quick === true ? quickRefused(process.env) : null;
|
|
3032
|
+
if (refused !== null) {
|
|
3033
|
+
throw new UsageError(refused);
|
|
3034
|
+
}
|
|
1712
3035
|
const manifest = readManifest(dir);
|
|
1713
3036
|
if (manifest.violations.length > 0 || manifest.gates.length === 0) {
|
|
1714
3037
|
io2.err('test-gates selfcheck: test-gates.json has problems. Run "test-gates check" first');
|
|
1715
3038
|
return 1;
|
|
1716
3039
|
}
|
|
3040
|
+
const chosen = selectControls({
|
|
3041
|
+
gates: manifest.gates.map((gate) => gate.path),
|
|
3042
|
+
mode: selection.mode ?? manifest.settings.selfcheckMode,
|
|
3043
|
+
only: selection.gates,
|
|
3044
|
+
shard: selection.shard
|
|
3045
|
+
});
|
|
3046
|
+
if ("error" in chosen) {
|
|
3047
|
+
throw new UsageError(chosen.error);
|
|
3048
|
+
}
|
|
3049
|
+
const targets = manifest.gates.filter((gate) => chosen.selected.includes(gate.path));
|
|
3050
|
+
const skipped = manifest.gates.length - targets.length;
|
|
3051
|
+
const notSelected = selection.shard !== null ? `, ${skipped} gate(s) not selected by --shard ${selection.shard.index}/${selection.shard.total}` : selection.gates.length > 0 ? `, ${skipped} gate(s) not selected by --gate` : "";
|
|
1717
3052
|
const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
|
|
1718
3053
|
const [program, ...args] = command;
|
|
1719
3054
|
const executable = findLocalBin(manifest.dir, program) ?? program;
|
|
1720
|
-
const
|
|
3055
|
+
const runGate = (env) => spawnSync3(executable, args, {
|
|
3056
|
+
cwd: manifest.dir,
|
|
3057
|
+
env: {
|
|
3058
|
+
...process.env,
|
|
3059
|
+
[EXCLUDE_ENV]: env.exclude ?? "",
|
|
3060
|
+
[EXCLUDE_ALL_ENV]: env.excludeAll === void 0 ? "" : JSON.stringify(env.excludeAll),
|
|
3061
|
+
[TOUCH_ENV]: env.touchDir ?? ""
|
|
3062
|
+
},
|
|
3063
|
+
encoding: "utf8",
|
|
3064
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
3065
|
+
shell: process.platform === "win32"
|
|
3066
|
+
});
|
|
3067
|
+
const judge = (result, gatePath) => result.error ? `cannot start "${command.join(" ")}" (${result.error.message})` : judgeSelfcheck({
|
|
3068
|
+
status: result.status,
|
|
3069
|
+
output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
|
|
3070
|
+
gatePath,
|
|
3071
|
+
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
3072
|
+
});
|
|
3073
|
+
const heldTogether = /* @__PURE__ */ new Map();
|
|
3074
|
+
const single = targets.filter((gate) => gate.spec !== null && gate.existingSpecs.length === 1);
|
|
3075
|
+
if (selection.quick === true && single.length >= 3) {
|
|
3076
|
+
const configSource = gateConfigSource(manifest, args);
|
|
3077
|
+
const shared = sharedRunPrecheck({
|
|
3078
|
+
program,
|
|
3079
|
+
version: installedVersion2(manifest.dir, program),
|
|
3080
|
+
configSource,
|
|
3081
|
+
specs: manifest.gates.flatMap(
|
|
3082
|
+
(gate) => gate.existingSpecs.map((spec) => ({ path: spec, source: readText(manifest.dir, spec) }))
|
|
3083
|
+
)
|
|
3084
|
+
}) ?? shareOneRun(manifest, single, runGate);
|
|
3085
|
+
if (typeof shared === "string" || "reason" in shared) {
|
|
3086
|
+
const reason = typeof shared === "string" ? shared : shared.reason;
|
|
3087
|
+
io2.out(`test-gates selfcheck: a run of the gate command for every gate (${reason})`);
|
|
3088
|
+
} else {
|
|
3089
|
+
for (const gate of shared.gates) {
|
|
3090
|
+
if (judge(shared.result, gate.path) === null) {
|
|
3091
|
+
heldTogether.set(gate.path, shared.result.status);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const heldAtOnce = /* @__PURE__ */ new Map();
|
|
3097
|
+
const waiting = single.filter((gate) => !heldTogether.has(gate.path));
|
|
3098
|
+
const jobs = selection.jobs ?? 1;
|
|
3099
|
+
if (jobs > 1 && waiting.length > 1) {
|
|
3100
|
+
const known = knownCommand(program, args);
|
|
3101
|
+
const configFile = gateConfigFile(args);
|
|
3102
|
+
const ask = (loaded) => jobsRefused({
|
|
3103
|
+
failurePattern: manifest.settings.selfcheckFailurePattern,
|
|
3104
|
+
command: known,
|
|
3105
|
+
configSource: gateConfigSource(manifest, args),
|
|
3106
|
+
...loaded,
|
|
3107
|
+
trusted: manifest.settings.importMode === "allowlist" ? manifest.settings.importAllow.flatMap(
|
|
3108
|
+
(rule) => rule.module === void 0 ? [] : [rule.module]
|
|
3109
|
+
) : []
|
|
3110
|
+
});
|
|
3111
|
+
const refusal = ask({ files: [], lost: null }) ?? ask(loadedFiles(manifest, configFile));
|
|
3112
|
+
if (refusal !== null || !("runner" in known)) {
|
|
3113
|
+
io2.out(`test-gates selfcheck: one negative control at a time (${refusal})`);
|
|
3114
|
+
} else {
|
|
3115
|
+
const specs = new Set(manifest.gates.map((gate) => gate.spec)).size - 1;
|
|
3116
|
+
const names = [
|
|
3117
|
+
manifest.dir,
|
|
3118
|
+
realPath(manifest.dir),
|
|
3119
|
+
...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
|
|
3120
|
+
].sort((left, right) => right.length - left.length);
|
|
3121
|
+
const results = await runAtOnce(manifest, waiting, jobs, known.runner, executable, args);
|
|
3122
|
+
for (const [gate, result] of results) {
|
|
3123
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
3124
|
+
const clean = cleanHold({
|
|
3125
|
+
held: judgeSelfcheck({ status: result.status, output, gatePath: gate.path }) === null,
|
|
3126
|
+
signal: result.signal,
|
|
3127
|
+
output,
|
|
3128
|
+
runner: known.runner,
|
|
3129
|
+
specs,
|
|
3130
|
+
gatePath: gate.path,
|
|
3131
|
+
others: manifest.gates.map((other) => other.path).filter((other) => other !== gate.path),
|
|
3132
|
+
names
|
|
3133
|
+
});
|
|
3134
|
+
if (clean) {
|
|
3135
|
+
heldAtOnce.set(gate.path, result.status);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
const again = waiting.length - heldAtOnce.size;
|
|
3139
|
+
if (again > 0) {
|
|
3140
|
+
io2.out(
|
|
3141
|
+
`test-gates selfcheck: ${again} of ${waiting.length} negative control(s) did not hold cleanly next to others and run again on their own`
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
1721
3146
|
let failed = 0;
|
|
1722
3147
|
for (const gate of targets) {
|
|
1723
3148
|
if (gate.spec === null || gate.existingSpecs.length !== 1) {
|
|
@@ -1725,20 +3150,19 @@ function runSelfcheck(dir, mode, commandOverride, io2) {
|
|
|
1725
3150
|
failed += 1;
|
|
1726
3151
|
continue;
|
|
1727
3152
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
3153
|
+
if (heldTogether.has(gate.path)) {
|
|
3154
|
+
io2.out(
|
|
3155
|
+
` \u2713 without ${gate.spec} the gate fails (exit ${heldTogether.get(gate.path)}, in the shared run)`
|
|
3156
|
+
);
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
if (heldAtOnce.has(gate.path)) {
|
|
3160
|
+
io2.out(` \u2713 without ${gate.spec} the gate fails (exit ${heldAtOnce.get(gate.path)})`);
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
const result = runGate({ exclude: gate.spec });
|
|
1735
3164
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
1736
|
-
const reason = result
|
|
1737
|
-
status: result.status,
|
|
1738
|
-
output,
|
|
1739
|
-
gatePath: gate.path,
|
|
1740
|
-
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
1741
|
-
});
|
|
3165
|
+
const reason = judge(result, gate.path);
|
|
1742
3166
|
if (reason === null) {
|
|
1743
3167
|
io2.out(` \u2713 without ${gate.spec} the gate fails (exit ${result.status})`);
|
|
1744
3168
|
} else {
|
|
@@ -1763,10 +3187,14 @@ function runSelfcheck(dir, mode, commandOverride, io2) {
|
|
|
1763
3187
|
}
|
|
1764
3188
|
}
|
|
1765
3189
|
if (failed > 0) {
|
|
1766
|
-
io2.err(
|
|
3190
|
+
io2.err(
|
|
3191
|
+
`test-gates selfcheck: FAILED (${failed} of ${targets.length} negative control(s)${notSelected})${quickCaveat(heldTogether.size)}`
|
|
3192
|
+
);
|
|
1767
3193
|
return 1;
|
|
1768
3194
|
}
|
|
1769
|
-
io2.out(
|
|
3195
|
+
io2.out(
|
|
3196
|
+
`test-gates selfcheck: OK (${targets.length} negative control(s) failed as they should${notSelected})${quickCaveat(heldTogether.size)}`
|
|
3197
|
+
);
|
|
1770
3198
|
return 0;
|
|
1771
3199
|
}
|
|
1772
3200
|
|
|
@@ -1781,12 +3209,21 @@ Commands:
|
|
|
1781
3209
|
Unknown arguments and everything after "--" go to Stryker
|
|
1782
3210
|
mutation-result Judge an existing Stryker JSON report
|
|
1783
3211
|
selfcheck [-- <command>] Negative control: leave one spec out, the gate must fail
|
|
1784
|
-
lcov --file <lcov.info>
|
|
3212
|
+
lcov [--file <lcov.info>] Per-file 100% line coverage from an lcov tracefile
|
|
1785
3213
|
|
|
1786
3214
|
Options:
|
|
1787
3215
|
--dir <subproject> Directory that holds test-gates.json (default: .)
|
|
1788
3216
|
--report <path> mutation-result: report to judge (default: settings.stryker.reportFile)
|
|
1789
3217
|
--all | --first selfcheck: every gate in turn (default) or only the first
|
|
3218
|
+
--gate <path> selfcheck: only this gate (may be given more than once).
|
|
3219
|
+
mutation: only the mutants of this gate, while working on it.
|
|
3220
|
+
Not the final check, and refused when CI is set
|
|
3221
|
+
--shard <index>/<total> selfcheck: one of <total> parts of the gates, for parallel CI jobs
|
|
3222
|
+
--jobs <n> selfcheck: run <n> negative controls at the same time. One that does
|
|
3223
|
+
not hold cleanly there is run again on its own
|
|
3224
|
+
--quick selfcheck: one run for the gates only their own spec touches.
|
|
3225
|
+
Not the final check: it assumes that no spec depends on which
|
|
3226
|
+
other specs run. Refused when CI is set
|
|
1790
3227
|
--file <path> lcov: tracefile (default: settings.lcov.file)
|
|
1791
3228
|
-h, --help Show this help
|
|
1792
3229
|
-v, --version Show the version
|
|
@@ -1802,7 +3239,7 @@ var io = {
|
|
|
1802
3239
|
function main(argv) {
|
|
1803
3240
|
const args = parseArgs(argv);
|
|
1804
3241
|
if (args.version) {
|
|
1805
|
-
const { version } =
|
|
3242
|
+
const { version } = createRequire2(import.meta.url)("../package.json");
|
|
1806
3243
|
io.out(version);
|
|
1807
3244
|
return 0;
|
|
1808
3245
|
}
|
|
@@ -1814,11 +3251,22 @@ function main(argv) {
|
|
|
1814
3251
|
case "check":
|
|
1815
3252
|
return runCheck(args.dir, io);
|
|
1816
3253
|
case "mutation":
|
|
1817
|
-
return runMutation(args.dir, args.rest, io);
|
|
3254
|
+
return runMutation(args.dir, args.rest, io, args.gates);
|
|
1818
3255
|
case "mutation-result":
|
|
1819
3256
|
return runMutationResult(args.dir, args.report, io);
|
|
1820
3257
|
case "selfcheck":
|
|
1821
|
-
return runSelfcheck(
|
|
3258
|
+
return runSelfcheck(
|
|
3259
|
+
args.dir,
|
|
3260
|
+
{
|
|
3261
|
+
mode: args.mode,
|
|
3262
|
+
gates: args.gates,
|
|
3263
|
+
shard: args.shard,
|
|
3264
|
+
quick: args.quick,
|
|
3265
|
+
jobs: args.jobs
|
|
3266
|
+
},
|
|
3267
|
+
args.rest,
|
|
3268
|
+
io
|
|
3269
|
+
);
|
|
1822
3270
|
case "lcov":
|
|
1823
3271
|
return runLcov(args.dir, args.file, io);
|
|
1824
3272
|
case null:
|
|
@@ -1826,7 +3274,7 @@ function main(argv) {
|
|
|
1826
3274
|
}
|
|
1827
3275
|
}
|
|
1828
3276
|
try {
|
|
1829
|
-
process.exitCode = main(process.argv.slice(2));
|
|
3277
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
1830
3278
|
} catch (error) {
|
|
1831
3279
|
if (error instanceof UsageError) {
|
|
1832
3280
|
io.err(`test-gates: ${error.message}
|