@t09tanaka/test-gates 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +85 -10
- package/dist/cli.js +1161 -363
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ function parseShard(value) {
|
|
|
28
28
|
}
|
|
29
29
|
var OPTIONS_BY_COMMAND = {
|
|
30
30
|
check: ["--dir"],
|
|
31
|
-
mutation: ["--dir", "--gate", "--affected"],
|
|
31
|
+
mutation: ["--dir", "--gate", "--shard", "--affected"],
|
|
32
32
|
"mutation-result": ["--dir", "--report"],
|
|
33
33
|
selfcheck: ["--dir", "--all", "--first", "--gate", "--shard", "--quick", "--jobs"],
|
|
34
34
|
lcov: ["--dir", "--file"],
|
|
@@ -146,13 +146,18 @@ function parseArgs(argv) {
|
|
|
146
146
|
"--affected and --gate cannot be combined: --gate runs the gates it names, --affected decides which gates run"
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
|
+
if (parsed.affected && parsed.shard !== null) {
|
|
150
|
+
throw new UsageError(
|
|
151
|
+
"--affected and --shard cannot be combined: --shard runs every mutant of its gates, --affected decides which gates run"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
149
154
|
const misplaced = parsed.rest.find((arg) => /^--(gate|shard|quick|jobs)(=|$)/.test(arg));
|
|
150
155
|
if (parsed.command === "selfcheck" && misplaced !== void 0) {
|
|
151
156
|
throw new UsageError(
|
|
152
157
|
`${misplaced.split("=")[0]} after "--" would go to the gate command. Write it before "--"`
|
|
153
158
|
);
|
|
154
159
|
}
|
|
155
|
-
const forStryker = parsed.rest.find((arg) => /^--(gate|affected)(=|$)/.test(arg));
|
|
160
|
+
const forStryker = parsed.rest.find((arg) => /^--(gate|shard|affected)(=|$)/.test(arg));
|
|
156
161
|
if (forStryker !== void 0) {
|
|
157
162
|
throw new UsageError(
|
|
158
163
|
`${forStryker.split("=")[0]} after "--" would go to Stryker. Write it before "--"`
|
|
@@ -1034,13 +1039,13 @@ function judgeSelfcheck(input) {
|
|
|
1034
1039
|
function selectControls(input) {
|
|
1035
1040
|
const { gates, mode, only, shard } = input;
|
|
1036
1041
|
if (only.length > 0) {
|
|
1037
|
-
const unknown = only.filter((
|
|
1042
|
+
const unknown = only.filter((path15) => !gates.includes(path15));
|
|
1038
1043
|
if (unknown.length > 0) {
|
|
1039
1044
|
return {
|
|
1040
1045
|
error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
|
|
1041
1046
|
};
|
|
1042
1047
|
}
|
|
1043
|
-
return { selected: gates.filter((
|
|
1048
|
+
return { selected: gates.filter((path15) => only.includes(path15)) };
|
|
1044
1049
|
}
|
|
1045
1050
|
if (shard !== null) {
|
|
1046
1051
|
if (shard.total > gates.length) {
|
|
@@ -1083,6 +1088,12 @@ function gateRunRefused(env) {
|
|
|
1083
1088
|
function affectedRefused(env) {
|
|
1084
1089
|
return inCi(env) ? "CI is set, and there every mutant is run (the final check)" : null;
|
|
1085
1090
|
}
|
|
1091
|
+
function shardRunCaveat(shard, judged, all) {
|
|
1092
|
+
if (shard.total === 1) {
|
|
1093
|
+
return `. All ${all} gate(s) were run and judged (--shard 1/1 is the only part)`;
|
|
1094
|
+
}
|
|
1095
|
+
return `. Only part ${shard.index}/${shard.total} (${judged} of ${all} gate(s)) was run and judged (--shard): every part has to pass for the final check`;
|
|
1096
|
+
}
|
|
1086
1097
|
function gateRunCaveat(judged, all) {
|
|
1087
1098
|
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`;
|
|
1088
1099
|
}
|
|
@@ -1193,8 +1204,8 @@ function planControls(input) {
|
|
|
1193
1204
|
if (silent !== void 0) {
|
|
1194
1205
|
return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
|
|
1195
1206
|
}
|
|
1196
|
-
const
|
|
1197
|
-
const short = all.find((gate) => accountedFor(totals[gate.file],
|
|
1207
|
+
const partsOf3 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
|
|
1208
|
+
const short = all.find((gate) => accountedFor(totals[gate.file], partsOf3(gate)) === "no");
|
|
1198
1209
|
if (short !== void 0) {
|
|
1199
1210
|
return everyoneAlone(
|
|
1200
1211
|
`the notes of the specs do not add up to what the runner counted for ${short.path}`
|
|
@@ -1208,7 +1219,7 @@ function planControls(input) {
|
|
|
1208
1219
|
if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
|
|
1209
1220
|
return false;
|
|
1210
1221
|
}
|
|
1211
|
-
if (accountedFor(totals[gate.file],
|
|
1222
|
+
if (accountedFor(totals[gate.file], partsOf3(gate)) !== "yes") {
|
|
1212
1223
|
return false;
|
|
1213
1224
|
}
|
|
1214
1225
|
const touching = notes.filter((note) => {
|
|
@@ -1419,9 +1430,9 @@ function runCheck(dir, io2) {
|
|
|
1419
1430
|
}
|
|
1420
1431
|
|
|
1421
1432
|
// src/commands/doctor.ts
|
|
1422
|
-
import { spawnSync as
|
|
1423
|
-
import
|
|
1424
|
-
import
|
|
1433
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
1434
|
+
import fs11 from "fs";
|
|
1435
|
+
import path11 from "path";
|
|
1425
1436
|
|
|
1426
1437
|
// src/core/mutation-result.ts
|
|
1427
1438
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -1688,6 +1699,80 @@ function judgeMutationReport(input) {
|
|
|
1688
1699
|
}
|
|
1689
1700
|
return { violations, survivors, unexpectedTimeouts, summary };
|
|
1690
1701
|
}
|
|
1702
|
+
var UNKNOWN_TEST = "not-in-the-first-run:";
|
|
1703
|
+
function sameMutant(a, b) {
|
|
1704
|
+
return a.mutatorName === b.mutatorName && (a.replacement ?? "") === (b.replacement ?? "") && a.location.start.line === b.location.start.line && a.location.start.column === b.location.start.column && a.location.end.line === b.location.end.line && a.location.end.column === b.location.end.column;
|
|
1705
|
+
}
|
|
1706
|
+
function mergeRetry(first, retry, gates) {
|
|
1707
|
+
const outcome = { retried: 0, statuses: {}, missing: 0 };
|
|
1708
|
+
const firstId = translator(first, retry);
|
|
1709
|
+
const files = { ...first.files };
|
|
1710
|
+
const again = retry.files ?? {};
|
|
1711
|
+
for (const named of gates) {
|
|
1712
|
+
const gate = named.path.split("/").filter((part) => part !== ".").join("/");
|
|
1713
|
+
const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
|
|
1714
|
+
if (result?.mutants === void 0) {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const expected = expectedTimeoutMutants(named, result.source, result.mutants);
|
|
1718
|
+
const rerun = Object.hasOwn(again, gate) ? again[gate]?.mutants : void 0;
|
|
1719
|
+
files[gate] = {
|
|
1720
|
+
...result,
|
|
1721
|
+
mutants: result.mutants.map((mutant) => {
|
|
1722
|
+
if (mutant.status !== "Timeout" || expected.has(mutant)) {
|
|
1723
|
+
return mutant;
|
|
1724
|
+
}
|
|
1725
|
+
outcome.retried += 1;
|
|
1726
|
+
const found = rerun?.find((other) => sameMutant(other, mutant));
|
|
1727
|
+
if (found === void 0) {
|
|
1728
|
+
outcome.missing += 1;
|
|
1729
|
+
return mutant;
|
|
1730
|
+
}
|
|
1731
|
+
outcome.statuses[found.status] = (outcome.statuses[found.status] ?? 0) + 1;
|
|
1732
|
+
return {
|
|
1733
|
+
...found,
|
|
1734
|
+
...found.coveredBy === void 0 ? {} : { coveredBy: found.coveredBy.map(firstId) },
|
|
1735
|
+
...found.killedBy === void 0 ? {} : { killedBy: found.killedBy.map(firstId) }
|
|
1736
|
+
};
|
|
1737
|
+
})
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
return { report: { ...first, files }, outcome };
|
|
1741
|
+
}
|
|
1742
|
+
function testKey(file, test) {
|
|
1743
|
+
const start = test.location?.start;
|
|
1744
|
+
return JSON.stringify([file, test.name ?? "", start?.line, start?.column]);
|
|
1745
|
+
}
|
|
1746
|
+
function* keyedTests(report) {
|
|
1747
|
+
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
1748
|
+
if (entry.tests !== void 0) {
|
|
1749
|
+
for (const test of entry.tests) {
|
|
1750
|
+
yield { id: test.id, key: testKey(file, test) };
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function translator(first, retry) {
|
|
1756
|
+
const firstIds = /* @__PURE__ */ new Map();
|
|
1757
|
+
for (const { id, key } of keyedTests(first)) {
|
|
1758
|
+
firstIds.set(key, firstIds.has(key) ? null : id);
|
|
1759
|
+
}
|
|
1760
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1761
|
+
for (const { id, key } of keyedTests(retry)) {
|
|
1762
|
+
ids.set(id, firstIds.get(key));
|
|
1763
|
+
}
|
|
1764
|
+
return (id) => {
|
|
1765
|
+
const found = ids.get(id);
|
|
1766
|
+
return typeof found === "string" ? found : `${UNKNOWN_TEST}${id}`;
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function formatRetry(outcome) {
|
|
1770
|
+
const statuses = Object.entries(outcome.statuses).sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `${count} ${status}`);
|
|
1771
|
+
if (outcome.missing > 0) {
|
|
1772
|
+
statuses.push(`${outcome.missing} not in the report (still Timeout)`);
|
|
1773
|
+
}
|
|
1774
|
+
return `test-gates mutation: ${outcome.retried} mutant(s) that timed out were run again one at a time (--concurrency 1): ${statuses.join(", ")}`;
|
|
1775
|
+
}
|
|
1691
1776
|
function formatSurvivor(survivor) {
|
|
1692
1777
|
const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
|
|
1693
1778
|
return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
|
|
@@ -1697,7 +1782,7 @@ function formatSummary(summary) {
|
|
|
1697
1782
|
}
|
|
1698
1783
|
|
|
1699
1784
|
// src/core/reuse-guard.ts
|
|
1700
|
-
var RECORD_FORMAT =
|
|
1785
|
+
var RECORD_FORMAT = 3;
|
|
1701
1786
|
function isStringRecord(value) {
|
|
1702
1787
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1703
1788
|
}
|
|
@@ -1709,11 +1794,14 @@ function parseAffected(value) {
|
|
|
1709
1794
|
}
|
|
1710
1795
|
const parsed = /* @__PURE__ */ Object.create(null);
|
|
1711
1796
|
for (const [gate, entry] of Object.entries(gates)) {
|
|
1712
|
-
const { specs, files } = entry ?? {};
|
|
1797
|
+
const { specs, files, blind } = entry ?? {};
|
|
1713
1798
|
if (!isStringList(specs) || !isStringList(files)) {
|
|
1714
1799
|
return null;
|
|
1715
1800
|
}
|
|
1716
|
-
|
|
1801
|
+
if (blind !== void 0 && typeof blind !== "string") {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
parsed[gate] = { specs, files, ...blind === void 0 ? {} : { blind } };
|
|
1717
1805
|
}
|
|
1718
1806
|
return { tests, gates: parsed };
|
|
1719
1807
|
}
|
|
@@ -1876,7 +1964,8 @@ var CONFIG_NAMES = [
|
|
|
1876
1964
|
/\.d\.[cm]?ts$/
|
|
1877
1965
|
];
|
|
1878
1966
|
var ROOT_MOCKS = "__mocks__";
|
|
1879
|
-
var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
|
|
1967
|
+
var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc|html?)$/i;
|
|
1968
|
+
var ENTRY_PAGE = /^index\.html?$/i;
|
|
1880
1969
|
var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
|
|
1881
1970
|
function kindOf(key, subproject) {
|
|
1882
1971
|
const segments = localSegments(key, subproject);
|
|
@@ -1887,7 +1976,7 @@ function kindOf(key, subproject) {
|
|
|
1887
1976
|
if (isNestedMock(key, subproject)) {
|
|
1888
1977
|
return "other";
|
|
1889
1978
|
}
|
|
1890
|
-
if (DOCUMENT.test(name)) {
|
|
1979
|
+
if (DOCUMENT.test(name) && !ENTRY_PAGE.test(name)) {
|
|
1891
1980
|
return "document";
|
|
1892
1981
|
}
|
|
1893
1982
|
const inside = subproject === "" || key.startsWith(`${subproject}/`);
|
|
@@ -1904,6 +1993,7 @@ function isNestedMock(key, subproject) {
|
|
|
1904
1993
|
}
|
|
1905
1994
|
function gateDependencies(input) {
|
|
1906
1995
|
const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
|
|
1996
|
+
const blind = input.blind ?? {};
|
|
1907
1997
|
const result = /* @__PURE__ */ Object.create(null);
|
|
1908
1998
|
for (const gate of input.gates) {
|
|
1909
1999
|
const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
|
|
@@ -1917,7 +2007,13 @@ function gateDependencies(input) {
|
|
|
1917
2007
|
for (const spec of specs) {
|
|
1918
2008
|
closureOf(spec).forEach((file) => files.add(file));
|
|
1919
2009
|
}
|
|
1920
|
-
|
|
2010
|
+
const sorted2 = [...files].sort();
|
|
2011
|
+
const reason = sorted2.find((file) => Object.hasOwn(blind, file));
|
|
2012
|
+
result[gate.key] = {
|
|
2013
|
+
specs: [...specs].sort(),
|
|
2014
|
+
files: sorted2,
|
|
2015
|
+
...reason === void 0 ? {} : { blind: blind[reason] }
|
|
2016
|
+
};
|
|
1921
2017
|
}
|
|
1922
2018
|
return result;
|
|
1923
2019
|
}
|
|
@@ -1948,6 +2044,15 @@ function planAffected(input) {
|
|
|
1948
2044
|
rerun.push({ gate: gate.path, changed: [] });
|
|
1949
2045
|
continue;
|
|
1950
2046
|
}
|
|
2047
|
+
const blind = gate.blind ?? last.blind;
|
|
2048
|
+
if (blind !== void 0) {
|
|
2049
|
+
if (changed.length > 0 || input.seeded === true) {
|
|
2050
|
+
rerun.push({ gate: gate.path, changed, blind });
|
|
2051
|
+
} else {
|
|
2052
|
+
reused.push(gate.path);
|
|
2053
|
+
}
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
1951
2056
|
const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
|
|
1952
2057
|
const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
|
|
1953
2058
|
if (differing.length > 0) {
|
|
@@ -1999,13 +2104,23 @@ function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
|
|
|
1999
2104
|
const lines = [
|
|
2000
2105
|
`test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
|
|
2001
2106
|
];
|
|
2002
|
-
|
|
2107
|
+
const told = plan.rerun.filter((entry) => entry.blind === void 0);
|
|
2108
|
+
for (const { gate, changed } of told.slice(0, shown)) {
|
|
2003
2109
|
lines.push(
|
|
2004
2110
|
changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
|
|
2005
2111
|
);
|
|
2006
2112
|
}
|
|
2007
|
-
if (
|
|
2008
|
-
lines.push(` and ${
|
|
2113
|
+
if (told.length > shown) {
|
|
2114
|
+
lines.push(` and ${told.length - shown} more`);
|
|
2115
|
+
}
|
|
2116
|
+
const reasons = [
|
|
2117
|
+
...new Set(plan.rerun.flatMap((entry) => entry.blind === void 0 ? [] : [entry.blind]))
|
|
2118
|
+
];
|
|
2119
|
+
if (reasons.length > 0) {
|
|
2120
|
+
const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
|
|
2121
|
+
lines.push(
|
|
2122
|
+
` ${plan.rerun.length - told.length} gate(s) run again for any change, as what they depend on cannot be told: ${reasons[0]}${more}`
|
|
2123
|
+
);
|
|
2009
2124
|
}
|
|
2010
2125
|
if (timeouts > 0) {
|
|
2011
2126
|
lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
|
|
@@ -2052,12 +2167,22 @@ function diagnose(facts) {
|
|
|
2052
2167
|
fix: "write such a path from the directory of its file (path.resolve(__dirname, \u2026)) so that it can be followed, or keep it out of the files the run loads"
|
|
2053
2168
|
});
|
|
2054
2169
|
}
|
|
2170
|
+
const everyBlind = facts.blindGates.length >= facts.gates;
|
|
2055
2171
|
if (facts.blind !== null) {
|
|
2056
2172
|
findings.push({
|
|
2057
2173
|
level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
2058
2174
|
text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
|
|
2059
2175
|
fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
|
|
2060
2176
|
});
|
|
2177
|
+
} else if (facts.blindGates.length > 0) {
|
|
2178
|
+
const gates = facts.blindGates.map((one) => one.gate);
|
|
2179
|
+
const reasons = [...new Set(facts.blindGates.map((one) => one.reason))];
|
|
2180
|
+
const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
|
|
2181
|
+
findings.push({
|
|
2182
|
+
level: everyBlind && facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
2183
|
+
text: `with --affected, a change of any file runs ${gates.length} of ${facts.gates} gate(s) again (${listed(gates)}): what they depend on cannot be told (${reasons[0]}${more})`,
|
|
2184
|
+
fix: "keep what reads files by a path no import names out of what those gates and their specs import"
|
|
2185
|
+
});
|
|
2061
2186
|
}
|
|
2062
2187
|
if (facts.scope !== null && "every" in facts.scope) {
|
|
2063
2188
|
findings.push({
|
|
@@ -2066,6 +2191,21 @@ function diagnose(facts) {
|
|
|
2066
2191
|
fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
|
|
2067
2192
|
});
|
|
2068
2193
|
}
|
|
2194
|
+
if (facts.sandboxes !== null) {
|
|
2195
|
+
const { tempDir, count, kept, running } = facts.sandboxes;
|
|
2196
|
+
findings.push(
|
|
2197
|
+
running === null ? {
|
|
2198
|
+
level: "note",
|
|
2199
|
+
text: `${count} sandbox(es) an interrupted Stryker run left in ${tempDir}: a tool that walks the subproject (a test runner, a script) finds a copy of every file in them`,
|
|
2200
|
+
fix: kept === null ? "the next test-gates mutation removes them" : `remove them by hand; the next test-gates mutation keeps them, because ${kept}`
|
|
2201
|
+
} : {
|
|
2202
|
+
// The sandboxes may be those of that run: removing them would break it.
|
|
2203
|
+
level: "note",
|
|
2204
|
+
text: `another test-gates mutation is running in this subproject (pid ${running}): the ${count} sandbox(es) in ${tempDir} are in use`,
|
|
2205
|
+
fix: "leave them; the first test-gates mutation after that run ends removes what is left"
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2069
2209
|
if (facts.hooks.length === 0) {
|
|
2070
2210
|
findings.push({
|
|
2071
2211
|
level: "note",
|
|
@@ -2073,7 +2213,8 @@ function diagnose(facts) {
|
|
|
2073
2213
|
fix: null
|
|
2074
2214
|
});
|
|
2075
2215
|
}
|
|
2076
|
-
|
|
2216
|
+
const toldApart = facts.blind === null && !everyBlind;
|
|
2217
|
+
for (const hook of facts.hooks.filter((one) => !one.affected && toldApart)) {
|
|
2077
2218
|
findings.push({
|
|
2078
2219
|
level: "problem",
|
|
2079
2220
|
text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
|
|
@@ -2099,39 +2240,322 @@ function formatFindings(findings) {
|
|
|
2099
2240
|
var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
|
|
2100
2241
|
var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
|
|
2101
2242
|
var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
|
|
2243
|
+
var NONE = [];
|
|
2244
|
+
var COMMAND = String.raw`(?<=(?:^|[\n;&|(){]|\b(?:then|else|do))\s*)`;
|
|
2245
|
+
var TOKEN = new RegExp(
|
|
2246
|
+
String.raw`\n|;;&?|;&|${COMMAND}(?:case\s+(\S+)\s+in|for\s+(\w+)(?:\s+in\s+([^;\n]*))?|(esac|do|done))(?![\w-])`,
|
|
2247
|
+
"g"
|
|
2248
|
+
);
|
|
2249
|
+
var PATTERN = /\s*(?:esac(?![\w-])|\(?([^()\n;]*)\))/y;
|
|
2250
|
+
var VARIABLE = /^("?)\$(?:(\w+)|\{(\w+)\})\1$/;
|
|
2251
|
+
var REFERENCE = /\$(?:(\w+)|\{(\w+)\})/g;
|
|
2252
|
+
function namesOf(words) {
|
|
2253
|
+
const names = words.map((word) => word.trim().replace(/^(["'])(.*)\1$/, "$2"));
|
|
2254
|
+
return names.some((name) => name === "" || /[$*?[{}~\\`"']/.test(name)) ? null : names;
|
|
2255
|
+
}
|
|
2256
|
+
function partsOf(text) {
|
|
2257
|
+
const parts = [];
|
|
2258
|
+
let scopes = NONE;
|
|
2259
|
+
let start = 0;
|
|
2260
|
+
let loop = null;
|
|
2261
|
+
const enter = (scope) => {
|
|
2262
|
+
scopes = [...scopes, scope];
|
|
2263
|
+
};
|
|
2264
|
+
const leave = (kind) => {
|
|
2265
|
+
const left = scopes.at(-1)?.kind === kind;
|
|
2266
|
+
scopes = scopes.slice(0, -1);
|
|
2267
|
+
return left;
|
|
2268
|
+
};
|
|
2269
|
+
TOKEN.lastIndex = 0;
|
|
2270
|
+
for (; ; ) {
|
|
2271
|
+
const token = TOKEN.exec(text);
|
|
2272
|
+
parts.push({ text: text.slice(start, token?.index), scopes });
|
|
2273
|
+
if (token === null) {
|
|
2274
|
+
return scopes.length === 0 ? parts : null;
|
|
2275
|
+
}
|
|
2276
|
+
const [word, subject, variable, list, keyword] = token;
|
|
2277
|
+
let awaiting = false;
|
|
2278
|
+
if (subject !== void 0) {
|
|
2279
|
+
const named = VARIABLE.exec(subject);
|
|
2280
|
+
enter({
|
|
2281
|
+
kind: "case",
|
|
2282
|
+
id: /* @__PURE__ */ Symbol(),
|
|
2283
|
+
variable: named?.[2] ?? named?.[3] ?? null,
|
|
2284
|
+
names: null
|
|
2285
|
+
});
|
|
2286
|
+
awaiting = true;
|
|
2287
|
+
} else if (variable !== void 0) {
|
|
2288
|
+
loop = {
|
|
2289
|
+
kind: "loop",
|
|
2290
|
+
id: /* @__PURE__ */ Symbol(),
|
|
2291
|
+
variable,
|
|
2292
|
+
names: list === void 0 ? null : namesOf(list.trim().split(/\s+/))
|
|
2293
|
+
};
|
|
2294
|
+
} else if (keyword === "do") {
|
|
2295
|
+
enter(loop ?? { kind: "loop", id: /* @__PURE__ */ Symbol(), variable: null, names: null });
|
|
2296
|
+
loop = null;
|
|
2297
|
+
} else if (keyword === "done" || keyword === "esac") {
|
|
2298
|
+
if (!leave(keyword === "done" ? "loop" : "case")) {
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
} else if (word === ";&") {
|
|
2302
|
+
return null;
|
|
2303
|
+
} else if (word !== "\n") {
|
|
2304
|
+
if (scopes.at(-1)?.kind !== "case") {
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
awaiting = true;
|
|
2308
|
+
}
|
|
2309
|
+
start = TOKEN.lastIndex;
|
|
2310
|
+
if (awaiting) {
|
|
2311
|
+
PATTERN.lastIndex = start;
|
|
2312
|
+
const pattern = PATTERN.exec(text);
|
|
2313
|
+
if (pattern === null) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
start = PATTERN.lastIndex;
|
|
2317
|
+
TOKEN.lastIndex = start;
|
|
2318
|
+
const top = scopes.at(-1);
|
|
2319
|
+
scopes = scopes.slice(0, -1);
|
|
2320
|
+
if (pattern[1] !== void 0) {
|
|
2321
|
+
enter({ ...top, names: namesOf(pattern[1].split("|")) });
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
function sibling(of, scopes) {
|
|
2327
|
+
const apart = of.findIndex((scope, at) => scope !== scopes[at]);
|
|
2328
|
+
return apart !== -1 && of[apart].id === scopes[apart]?.id;
|
|
2329
|
+
}
|
|
2330
|
+
function expansions(target, scopes) {
|
|
2331
|
+
return [...target.matchAll(REFERENCE)].reduce(
|
|
2332
|
+
(words, [reference, plain2, braced]) => {
|
|
2333
|
+
const names = [...scopes].reverse().find((scope) => scope.variable === (plain2 ?? braced))?.names ?? [reference];
|
|
2334
|
+
return words.flatMap((word) => names.map((name) => word.replace(reference, name)));
|
|
2335
|
+
},
|
|
2336
|
+
[target]
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2102
2339
|
function hookUses(input) {
|
|
2103
2340
|
const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
|
|
2341
|
+
const parts = partsOf(lines.join("\n")) ?? lines.map((text) => ({ text, scopes: NONE }));
|
|
2104
2342
|
const uses = [];
|
|
2105
|
-
|
|
2106
|
-
|
|
2343
|
+
const cds = parts.map((part) => ({ cd: CD.exec(part.text)?.[2], scopes: part.scopes }));
|
|
2344
|
+
parts.forEach((part, index) => {
|
|
2345
|
+
if (!RUNS_MUTATION.test(part.text)) {
|
|
2107
2346
|
return;
|
|
2108
2347
|
}
|
|
2109
|
-
const named = TARGET.exec(
|
|
2110
|
-
const
|
|
2111
|
-
const
|
|
2112
|
-
const
|
|
2348
|
+
const named = TARGET.exec(part.text) ?? CD.exec(part.text);
|
|
2349
|
+
const before = cds.slice(0, index).reverse().find((one) => one.cd !== void 0 && !sibling(one.scopes, part.scopes));
|
|
2350
|
+
const target = named?.[2] ?? before?.cd;
|
|
2351
|
+
const scopes = named !== null || before?.scopes === part.scopes ? part.scopes : NONE;
|
|
2352
|
+
const ours = target === void 0 || expansions(target, scopes).some((word) => {
|
|
2353
|
+
const normalized = word.split("/").filter((one) => one !== "." && one !== "").join("/");
|
|
2354
|
+
return normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
|
|
2355
|
+
});
|
|
2113
2356
|
if (ours) {
|
|
2114
2357
|
uses.push({
|
|
2115
2358
|
file: input.file,
|
|
2116
|
-
affected: /--affected\b/.test(
|
|
2359
|
+
affected: /--affected\b/.test(part.text) || /test:gates:mutation\b/.test(part.text) && input.script.includes("--affected")
|
|
2117
2360
|
});
|
|
2118
2361
|
}
|
|
2119
2362
|
});
|
|
2120
2363
|
return uses;
|
|
2121
2364
|
}
|
|
2122
2365
|
|
|
2123
|
-
// src/
|
|
2366
|
+
// src/sandboxes.ts
|
|
2124
2367
|
import { spawnSync } from "child_process";
|
|
2368
|
+
import fs4 from "fs";
|
|
2369
|
+
import path4 from "path";
|
|
2370
|
+
|
|
2371
|
+
// src/core/sandboxes.ts
|
|
2372
|
+
var SANDBOX = /^sandbox-[A-Za-z0-9]{6}$/;
|
|
2373
|
+
var MARKER = /^test-gates-run-([1-9][0-9]*)$/;
|
|
2374
|
+
function markerName(pid) {
|
|
2375
|
+
return `test-gates-run-${pid}`;
|
|
2376
|
+
}
|
|
2377
|
+
function markerText(pid, started) {
|
|
2378
|
+
return `${pid}
|
|
2379
|
+
${started ?? ""}
|
|
2380
|
+
`;
|
|
2381
|
+
}
|
|
2382
|
+
function markerAlive(input) {
|
|
2383
|
+
const recorded = input.text?.split("\n")[1]?.trim() || null;
|
|
2384
|
+
return input.there && (recorded === null || input.started === null || sameStart(recorded, input.started));
|
|
2385
|
+
}
|
|
2386
|
+
var START_SLACK_SECONDS = 5;
|
|
2387
|
+
function sameStart(a, b) {
|
|
2388
|
+
const difference = Math.abs(Date.parse(a) - Date.parse(b));
|
|
2389
|
+
return Number.isNaN(difference) ? a === b : difference <= START_SLACK_SECONDS * 1e3;
|
|
2390
|
+
}
|
|
2391
|
+
function insideSubproject(tempDir) {
|
|
2392
|
+
const parts = tempDir.split("/");
|
|
2393
|
+
return !/^[A-Za-z]:/.test(tempDir) && parts.every((part) => part !== "" && part !== "." && part !== "..");
|
|
2394
|
+
}
|
|
2395
|
+
function planSandboxes(facts) {
|
|
2396
|
+
const sandboxes = facts.entries.filter((entry) => entry.directory && SANDBOX.test(entry.name)).map((entry) => entry.name).sort();
|
|
2397
|
+
if (facts.tempDir === null) {
|
|
2398
|
+
return {
|
|
2399
|
+
kept: sandboxes,
|
|
2400
|
+
why: "the Stryker config could not be loaded to see where they are",
|
|
2401
|
+
running: null
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
if (!insideSubproject(facts.tempDir)) {
|
|
2405
|
+
return {
|
|
2406
|
+
kept: sandboxes,
|
|
2407
|
+
why: `tempDirName (${facts.tempDir}) is not a directory inside the subproject`,
|
|
2408
|
+
running: null
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
if (facts.keeps) {
|
|
2412
|
+
return { kept: sandboxes, why: "the Stryker config sets cleanTempDir: false", running: null };
|
|
2413
|
+
}
|
|
2414
|
+
const markers = [];
|
|
2415
|
+
for (const entry of facts.entries) {
|
|
2416
|
+
const pid = MARKER.exec(entry.name)?.[1];
|
|
2417
|
+
if (pid === void 0 || entry.directory) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
if (facts.alive(Number(pid), entry.name)) {
|
|
2421
|
+
return {
|
|
2422
|
+
kept: sandboxes,
|
|
2423
|
+
why: `another test-gates mutation is running in this subproject (pid ${pid})`,
|
|
2424
|
+
running: Number(pid)
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
markers.push(entry.name);
|
|
2428
|
+
}
|
|
2429
|
+
return { remove: sandboxes, markers };
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// src/sandboxes.ts
|
|
2433
|
+
function there(pid) {
|
|
2434
|
+
try {
|
|
2435
|
+
process.kill(pid, 0);
|
|
2436
|
+
return true;
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
return error.code === "EPERM";
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
function startedAt(pid) {
|
|
2442
|
+
const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
2443
|
+
encoding: "utf8",
|
|
2444
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
2445
|
+
timeout: 5e3
|
|
2446
|
+
});
|
|
2447
|
+
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
2448
|
+
return started === "" ? null : started;
|
|
2449
|
+
}
|
|
2450
|
+
function alive(dir) {
|
|
2451
|
+
return (pid, marker) => {
|
|
2452
|
+
if (!there(pid)) {
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
2455
|
+
let text = null;
|
|
2456
|
+
try {
|
|
2457
|
+
text = fs4.readFileSync(path4.join(dir, marker), "utf8");
|
|
2458
|
+
} catch {
|
|
2459
|
+
}
|
|
2460
|
+
return markerAlive({ there: true, text, started: startedAt(pid) });
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function relativeTempDir(subproject, dir) {
|
|
2464
|
+
let real = dir;
|
|
2465
|
+
try {
|
|
2466
|
+
real = fs4.realpathSync(dir);
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
return path4.relative(fs4.realpathSync(subproject), real).split(path4.sep).join("/");
|
|
2470
|
+
}
|
|
2471
|
+
function sandboxPlan(subproject, setting) {
|
|
2472
|
+
const tempDir = setting.dir === null ? null : relativeTempDir(subproject, setting.dir);
|
|
2473
|
+
let entries = [];
|
|
2474
|
+
if (setting.dir !== null) {
|
|
2475
|
+
try {
|
|
2476
|
+
entries = fs4.readdirSync(setting.dir, { withFileTypes: true });
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const plan = planSandboxes({
|
|
2481
|
+
tempDir,
|
|
2482
|
+
entries: entries.map((entry) => ({ name: entry.name, directory: entry.isDirectory() })),
|
|
2483
|
+
keeps: setting.keeps,
|
|
2484
|
+
alive: alive(setting.dir ?? subproject)
|
|
2485
|
+
});
|
|
2486
|
+
return { plan, shown: tempDir ?? ".stryker-tmp" };
|
|
2487
|
+
}
|
|
2488
|
+
function removeStaleSandboxes(subproject, setting) {
|
|
2489
|
+
const { plan, shown } = sandboxPlan(subproject, setting);
|
|
2490
|
+
if ("kept" in plan) {
|
|
2491
|
+
if (plan.kept.length === 0) {
|
|
2492
|
+
return [];
|
|
2493
|
+
}
|
|
2494
|
+
return plan.running === null ? [
|
|
2495
|
+
`test-gates mutation: ${plan.kept.length} sandbox(es) left in ${shown} by an earlier Stryker run are kept: ${plan.why}`
|
|
2496
|
+
] : [
|
|
2497
|
+
`test-gates mutation: ${plan.why}, so the ${plan.kept.length} sandbox(es) in ${shown} are kept: they are in use`
|
|
2498
|
+
];
|
|
2499
|
+
}
|
|
2500
|
+
const dir = setting.dir;
|
|
2501
|
+
for (const marker of plan.markers) {
|
|
2502
|
+
try {
|
|
2503
|
+
fs4.rmSync(path4.join(dir, marker), { force: true });
|
|
2504
|
+
} catch {
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const failed = [];
|
|
2508
|
+
for (const sandbox of plan.remove) {
|
|
2509
|
+
try {
|
|
2510
|
+
fs4.rmSync(path4.join(dir, sandbox), { recursive: true, force: true });
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
failed.push(`${sandbox} (${error.message})`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
const removed = plan.remove.length - failed.length;
|
|
2516
|
+
return [
|
|
2517
|
+
...removed > 0 ? [
|
|
2518
|
+
`test-gates mutation: removed ${removed} sandbox(es) an earlier Stryker run left in ${shown}`
|
|
2519
|
+
] : [],
|
|
2520
|
+
...failed.length > 0 ? [`test-gates mutation: could not remove from ${shown}: ${failed.join(", ")}`] : []
|
|
2521
|
+
];
|
|
2522
|
+
}
|
|
2523
|
+
function markRun(subproject, setting) {
|
|
2524
|
+
const dir = setting.dir;
|
|
2525
|
+
if (dir === null || !insideSubproject(relativeTempDir(subproject, dir))) {
|
|
2526
|
+
return () => void 0;
|
|
2527
|
+
}
|
|
2528
|
+
const marker = path4.join(dir, markerName(process.pid));
|
|
2529
|
+
try {
|
|
2530
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2531
|
+
if (!insideSubproject(relativeTempDir(subproject, dir))) {
|
|
2532
|
+
return () => void 0;
|
|
2533
|
+
}
|
|
2534
|
+
fs4.writeFileSync(marker, markerText(process.pid, startedAt(process.pid)));
|
|
2535
|
+
} catch {
|
|
2536
|
+
return () => void 0;
|
|
2537
|
+
}
|
|
2538
|
+
return () => {
|
|
2539
|
+
try {
|
|
2540
|
+
fs4.rmSync(marker, { force: true });
|
|
2541
|
+
fs4.rmdirSync(dir);
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/snapshot.ts
|
|
2548
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2125
2549
|
import { createHash } from "crypto";
|
|
2126
|
-
import
|
|
2550
|
+
import fs6 from "fs";
|
|
2127
2551
|
import { createRequire } from "module";
|
|
2128
2552
|
import os from "os";
|
|
2129
|
-
import
|
|
2553
|
+
import path6 from "path";
|
|
2130
2554
|
|
|
2131
2555
|
// src/commands/loaded-files.ts
|
|
2132
|
-
import
|
|
2556
|
+
import fs5 from "fs";
|
|
2133
2557
|
import { builtinModules } from "module";
|
|
2134
|
-
import
|
|
2558
|
+
import path5 from "path";
|
|
2135
2559
|
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
2136
2560
|
var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
|
|
2137
2561
|
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
@@ -2139,7 +2563,7 @@ var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
|
2139
2563
|
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
2140
2564
|
function isFile(file) {
|
|
2141
2565
|
try {
|
|
2142
|
-
return
|
|
2566
|
+
return fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
|
|
2143
2567
|
} catch {
|
|
2144
2568
|
return false;
|
|
2145
2569
|
}
|
|
@@ -2172,9 +2596,9 @@ function filesOf(base, directory, declarations) {
|
|
|
2172
2596
|
...extensions.map((extension) => base + extension),
|
|
2173
2597
|
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
2174
2598
|
...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
2175
|
-
...directory ? extensions.map((extension) =>
|
|
2599
|
+
...directory ? extensions.map((extension) => path5.join(base, `index${extension}`)) : []
|
|
2176
2600
|
];
|
|
2177
|
-
const data = [`${base}.json`, ...directory ? [
|
|
2601
|
+
const data = [`${base}.json`, ...directory ? [path5.join(base, "index.json")] : []];
|
|
2178
2602
|
const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
|
|
2179
2603
|
return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
|
|
2180
2604
|
}
|
|
@@ -2183,11 +2607,11 @@ function resolveFile(base, directory = true, declarations = true) {
|
|
|
2183
2607
|
}
|
|
2184
2608
|
function installed(specifier, from) {
|
|
2185
2609
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2186
|
-
for (let current =
|
|
2187
|
-
if (
|
|
2610
|
+
for (let current = path5.dirname(from); ; current = path5.dirname(current)) {
|
|
2611
|
+
if (fs5.existsSync(path5.join(current, "node_modules", name))) {
|
|
2188
2612
|
return true;
|
|
2189
2613
|
}
|
|
2190
|
-
if (current ===
|
|
2614
|
+
if (current === path5.dirname(current)) {
|
|
2191
2615
|
return false;
|
|
2192
2616
|
}
|
|
2193
2617
|
}
|
|
@@ -2197,18 +2621,18 @@ function loadedFiles(manifest, configFile) {
|
|
|
2197
2621
|
}
|
|
2198
2622
|
function loadedFilesNow(manifest, configFile) {
|
|
2199
2623
|
const aliases = manifest.settings.importAliases;
|
|
2200
|
-
const relative = (file) =>
|
|
2624
|
+
const relative = (file) => path5.relative(manifest.dir, file).split(path5.sep).join("/");
|
|
2201
2625
|
const target = (specifier, from) => {
|
|
2202
2626
|
if (specifier.startsWith(".")) {
|
|
2203
|
-
return resolveFiles(
|
|
2627
|
+
return resolveFiles(path5.resolve(path5.dirname(from), specifier));
|
|
2204
2628
|
}
|
|
2205
2629
|
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2206
2630
|
return alias === void 0 ? [] : resolveFiles(
|
|
2207
|
-
|
|
2631
|
+
path5.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
2208
2632
|
);
|
|
2209
2633
|
};
|
|
2210
2634
|
const read2 = (file) => {
|
|
2211
|
-
const source =
|
|
2635
|
+
const source = fs5.readFileSync(file, "utf8");
|
|
2212
2636
|
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
2213
2637
|
const specifiers = [
|
|
2214
2638
|
.../* @__PURE__ */ new Set([
|
|
@@ -2220,7 +2644,7 @@ function loadedFilesNow(manifest, configFile) {
|
|
|
2220
2644
|
return { source, specifiers, strings };
|
|
2221
2645
|
};
|
|
2222
2646
|
const named = /* @__PURE__ */ new Set();
|
|
2223
|
-
const configs = configFile === null ? [] : [
|
|
2647
|
+
const configs = configFile === null ? [] : [path5.resolve(manifest.dir, configFile)];
|
|
2224
2648
|
const seenConfigs = new Set(configs);
|
|
2225
2649
|
const entry = configs[0];
|
|
2226
2650
|
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
@@ -2228,8 +2652,8 @@ function loadedFilesNow(manifest, configFile) {
|
|
|
2228
2652
|
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
2229
2653
|
for (const text of paths) {
|
|
2230
2654
|
const stripped = text.replace(/^<rootDir>\//, "");
|
|
2231
|
-
const here = resolveFiles(
|
|
2232
|
-
const found = here.length > 0 ? here : resolveFiles(
|
|
2655
|
+
const here = resolveFiles(path5.resolve(path5.dirname(file), stripped), false);
|
|
2656
|
+
const found = here.length > 0 ? here : resolveFiles(path5.resolve(manifest.dir, stripped), false);
|
|
2233
2657
|
for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
|
|
2234
2658
|
named.add(one);
|
|
2235
2659
|
}
|
|
@@ -2270,7 +2694,7 @@ function loadedFilesNow(manifest, configFile) {
|
|
|
2270
2694
|
}
|
|
2271
2695
|
}
|
|
2272
2696
|
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
2273
|
-
next.push(...resolveFiles(
|
|
2697
|
+
next.push(...resolveFiles(path5.resolve(path5.dirname(file), text)));
|
|
2274
2698
|
}
|
|
2275
2699
|
const visited = {
|
|
2276
2700
|
file: { path: relative(file), source, imports },
|
|
@@ -2300,7 +2724,7 @@ function loadedFilesNow(manifest, configFile) {
|
|
|
2300
2724
|
const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
|
|
2301
2725
|
const roots = [
|
|
2302
2726
|
closure(null, [...named]),
|
|
2303
|
-
...specs.map((spec) => closure(spec, [
|
|
2727
|
+
...specs.map((spec) => closure(spec, [path5.join(manifest.dir, spec)]))
|
|
2304
2728
|
];
|
|
2305
2729
|
return { files: [...known.values()].map((visited) => visited.file), roots };
|
|
2306
2730
|
}
|
|
@@ -2352,17 +2776,17 @@ function hashOf(content) {
|
|
|
2352
2776
|
}
|
|
2353
2777
|
function hashFile(file) {
|
|
2354
2778
|
try {
|
|
2355
|
-
return hashOf(
|
|
2779
|
+
return hashOf(fs6.readFileSync(file));
|
|
2356
2780
|
} catch {
|
|
2357
2781
|
return null;
|
|
2358
2782
|
}
|
|
2359
2783
|
}
|
|
2360
2784
|
function isInside(parent, child) {
|
|
2361
|
-
const relative =
|
|
2362
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2785
|
+
const relative = path6.relative(parent, child);
|
|
2786
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
2363
2787
|
}
|
|
2364
2788
|
function toPosix(file) {
|
|
2365
|
-
return file.split(
|
|
2789
|
+
return file.split(path6.sep).join("/");
|
|
2366
2790
|
}
|
|
2367
2791
|
function gitEnvironment(env) {
|
|
2368
2792
|
if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
|
|
@@ -2373,7 +2797,7 @@ function gitEnvironment(env) {
|
|
|
2373
2797
|
return rest;
|
|
2374
2798
|
}
|
|
2375
2799
|
function git(cwd, args) {
|
|
2376
|
-
const result =
|
|
2800
|
+
const result = spawnSync2("git", args, {
|
|
2377
2801
|
cwd,
|
|
2378
2802
|
env: gitEnvironment(process.env),
|
|
2379
2803
|
encoding: "utf8",
|
|
@@ -2386,7 +2810,7 @@ function globLiteral(relative) {
|
|
|
2386
2810
|
}
|
|
2387
2811
|
function gitLister(root) {
|
|
2388
2812
|
return (directory, recursive) => {
|
|
2389
|
-
const relative = globLiteral(toPosix(
|
|
2813
|
+
const relative = globLiteral(toPosix(path6.relative(root, directory)));
|
|
2390
2814
|
const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
|
|
2391
2815
|
const listed2 = git(root, [
|
|
2392
2816
|
"ls-files",
|
|
@@ -2405,8 +2829,8 @@ function gitLister(root) {
|
|
|
2405
2829
|
}
|
|
2406
2830
|
function walkLister(root) {
|
|
2407
2831
|
const walk = (directory, recursive, found) => {
|
|
2408
|
-
for (const entry of
|
|
2409
|
-
const file =
|
|
2832
|
+
for (const entry of fs6.readdirSync(directory, { withFileTypes: true })) {
|
|
2833
|
+
const file = path6.join(directory, entry.name);
|
|
2410
2834
|
if (NEVER.includes(entry.name)) {
|
|
2411
2835
|
continue;
|
|
2412
2836
|
}
|
|
@@ -2415,7 +2839,7 @@ function walkLister(root) {
|
|
|
2415
2839
|
walk(file, true, found);
|
|
2416
2840
|
}
|
|
2417
2841
|
} else {
|
|
2418
|
-
found.push(toPosix(
|
|
2842
|
+
found.push(toPosix(path6.relative(root, file)));
|
|
2419
2843
|
}
|
|
2420
2844
|
}
|
|
2421
2845
|
return found;
|
|
@@ -2425,16 +2849,16 @@ function walkLister(root) {
|
|
|
2425
2849
|
function upTo(from, to) {
|
|
2426
2850
|
const directories = [from];
|
|
2427
2851
|
while (directories[directories.length - 1] !== to) {
|
|
2428
|
-
directories.push(
|
|
2852
|
+
directories.push(path6.dirname(directories[directories.length - 1]));
|
|
2429
2853
|
}
|
|
2430
2854
|
return directories;
|
|
2431
2855
|
}
|
|
2432
2856
|
function declaresWorkspaces(directory) {
|
|
2433
|
-
if (
|
|
2857
|
+
if (fs6.existsSync(path6.join(directory, "pnpm-workspace.yaml"))) {
|
|
2434
2858
|
return true;
|
|
2435
2859
|
}
|
|
2436
2860
|
try {
|
|
2437
|
-
const manifest = JSON.parse(
|
|
2861
|
+
const manifest = JSON.parse(fs6.readFileSync(path6.join(directory, "package.json"), "utf8"));
|
|
2438
2862
|
return manifest.workspaces !== void 0;
|
|
2439
2863
|
} catch {
|
|
2440
2864
|
return false;
|
|
@@ -2442,7 +2866,7 @@ function declaresWorkspaces(directory) {
|
|
|
2442
2866
|
}
|
|
2443
2867
|
function walkRoot(dir) {
|
|
2444
2868
|
const candidates = withinBounds(dir);
|
|
2445
|
-
const repository = candidates.find((directory) =>
|
|
2869
|
+
const repository = candidates.find((directory) => fs6.existsSync(path6.join(directory, ".git")));
|
|
2446
2870
|
if (repository !== void 0) {
|
|
2447
2871
|
return repository;
|
|
2448
2872
|
}
|
|
@@ -2451,11 +2875,11 @@ function walkRoot(dir) {
|
|
|
2451
2875
|
function withinBounds(dir) {
|
|
2452
2876
|
let home = null;
|
|
2453
2877
|
try {
|
|
2454
|
-
home =
|
|
2878
|
+
home = fs6.realpathSync(os.homedir());
|
|
2455
2879
|
} catch {
|
|
2456
2880
|
}
|
|
2457
|
-
return upTo(dir,
|
|
2458
|
-
(directory) => directory !==
|
|
2881
|
+
return upTo(dir, path6.parse(dir).root).filter(
|
|
2882
|
+
(directory) => directory !== path6.parse(dir).root && (home === null || !isInside(directory, home))
|
|
2459
2883
|
);
|
|
2460
2884
|
}
|
|
2461
2885
|
function linkedPackages(nodeModules, root) {
|
|
@@ -2463,20 +2887,20 @@ function linkedPackages(nodeModules, root) {
|
|
|
2463
2887
|
const visit = (directory, depth) => {
|
|
2464
2888
|
let entries;
|
|
2465
2889
|
try {
|
|
2466
|
-
entries =
|
|
2890
|
+
entries = fs6.readdirSync(directory, { withFileTypes: true });
|
|
2467
2891
|
} catch {
|
|
2468
2892
|
return;
|
|
2469
2893
|
}
|
|
2470
2894
|
for (const entry of entries) {
|
|
2471
|
-
const file =
|
|
2895
|
+
const file = path6.join(directory, entry.name);
|
|
2472
2896
|
if (entry.isSymbolicLink()) {
|
|
2473
2897
|
let target;
|
|
2474
2898
|
try {
|
|
2475
|
-
target =
|
|
2899
|
+
target = fs6.realpathSync(file);
|
|
2476
2900
|
} catch {
|
|
2477
2901
|
continue;
|
|
2478
2902
|
}
|
|
2479
|
-
if (target.split(
|
|
2903
|
+
if (target.split(path6.sep).includes("node_modules") || !fs6.statSync(target).isDirectory()) {
|
|
2480
2904
|
continue;
|
|
2481
2905
|
}
|
|
2482
2906
|
if (!isInside(root, target)) {
|
|
@@ -2498,11 +2922,11 @@ function installedVersion(start, name) {
|
|
|
2498
2922
|
for (; ; ) {
|
|
2499
2923
|
try {
|
|
2500
2924
|
const manifest = JSON.parse(
|
|
2501
|
-
|
|
2925
|
+
fs6.readFileSync(path6.join(current, "node_modules", name, "package.json"), "utf8")
|
|
2502
2926
|
);
|
|
2503
2927
|
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
2504
2928
|
} catch {
|
|
2505
|
-
const parent =
|
|
2929
|
+
const parent = path6.dirname(current);
|
|
2506
2930
|
if (parent === current) {
|
|
2507
2931
|
return null;
|
|
2508
2932
|
}
|
|
@@ -2511,7 +2935,7 @@ function installedVersion(start, name) {
|
|
|
2511
2935
|
}
|
|
2512
2936
|
}
|
|
2513
2937
|
function nodeOnPath(cwd) {
|
|
2514
|
-
const result =
|
|
2938
|
+
const result = spawnSync2("node", ["--version"], {
|
|
2515
2939
|
cwd,
|
|
2516
2940
|
encoding: "utf8",
|
|
2517
2941
|
shell: process.platform === "win32",
|
|
@@ -2528,12 +2952,12 @@ function snapshotOf(input) {
|
|
|
2528
2952
|
return resolving(() => snapshotNow(input));
|
|
2529
2953
|
}
|
|
2530
2954
|
function snapshotNow(input) {
|
|
2531
|
-
const dir =
|
|
2955
|
+
const dir = fs6.realpathSync(input.dir);
|
|
2532
2956
|
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
2533
|
-
let root = top === null ? dir :
|
|
2957
|
+
let root = top === null ? dir : fs6.realpathSync(top.trim());
|
|
2534
2958
|
let lister = gitLister(root);
|
|
2535
2959
|
let mode = "git";
|
|
2536
|
-
const manifest = toPosix(
|
|
2960
|
+
const manifest = toPosix(path6.relative(root, path6.join(dir, "test-gates.json")));
|
|
2537
2961
|
if (top === null || !lister(dir, false).includes(manifest)) {
|
|
2538
2962
|
mode = "walk";
|
|
2539
2963
|
root = walkRoot(dir);
|
|
@@ -2541,17 +2965,17 @@ function snapshotNow(input) {
|
|
|
2541
2965
|
}
|
|
2542
2966
|
const listRoot = root;
|
|
2543
2967
|
const list = lister;
|
|
2544
|
-
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) =>
|
|
2968
|
+
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path6.join(listRoot, file)));
|
|
2545
2969
|
const walk = walkLister(listRoot);
|
|
2546
2970
|
const written = input.written.map((file) => {
|
|
2547
|
-
const target =
|
|
2971
|
+
const target = path6.resolve(dir, file);
|
|
2548
2972
|
try {
|
|
2549
|
-
return
|
|
2973
|
+
return fs6.realpathSync(target);
|
|
2550
2974
|
} catch {
|
|
2551
2975
|
return target;
|
|
2552
2976
|
}
|
|
2553
2977
|
});
|
|
2554
|
-
const swallowing = written.find((target) => isInside(target,
|
|
2978
|
+
const swallowing = written.find((target) => isInside(target, path6.join(dir, "test-gates.json")));
|
|
2555
2979
|
if (swallowing !== void 0) {
|
|
2556
2980
|
throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
|
|
2557
2981
|
}
|
|
@@ -2568,12 +2992,12 @@ function snapshotNow(input) {
|
|
|
2568
2992
|
if (!isInside(listRoot, directory)) {
|
|
2569
2993
|
const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
|
|
2570
2994
|
throw new SnapshotError(
|
|
2571
|
-
`${toPosix(
|
|
2995
|
+
`${toPosix(path6.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
|
|
2572
2996
|
);
|
|
2573
2997
|
}
|
|
2574
2998
|
if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
|
|
2575
2999
|
const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
|
|
2576
|
-
widened.push(`${toPosix(
|
|
3000
|
+
widened.push(`${toPosix(path6.relative(listRoot, from))}${written2}`);
|
|
2577
3001
|
}
|
|
2578
3002
|
if (![...queued].some((region) => isInside(region, directory))) {
|
|
2579
3003
|
queued.add(directory);
|
|
@@ -2582,10 +3006,10 @@ function snapshotNow(input) {
|
|
|
2582
3006
|
};
|
|
2583
3007
|
const follow = (fixed, open, anchored, file, reference) => {
|
|
2584
3008
|
const from = (start) => {
|
|
2585
|
-
const target =
|
|
3009
|
+
const target = path6.resolve(start, fixed);
|
|
2586
3010
|
let isDirectory2 = false;
|
|
2587
3011
|
try {
|
|
2588
|
-
isDirectory2 =
|
|
3012
|
+
isDirectory2 = fs6.statSync(target).isDirectory();
|
|
2589
3013
|
} catch {
|
|
2590
3014
|
}
|
|
2591
3015
|
return {
|
|
@@ -2594,9 +3018,9 @@ function snapshotNow(input) {
|
|
|
2594
3018
|
found: isDirectory2 || resolveFile(target, true, false) !== null
|
|
2595
3019
|
};
|
|
2596
3020
|
};
|
|
2597
|
-
const fromFile = from(
|
|
3021
|
+
const fromFile = from(path6.dirname(file));
|
|
2598
3022
|
const targets = [fromFile];
|
|
2599
|
-
if (!anchored &&
|
|
3023
|
+
if (!anchored && path6.dirname(file) !== dir) {
|
|
2600
3024
|
const fromRun = from(dir);
|
|
2601
3025
|
if (fromRun.found && !fromFile.found) {
|
|
2602
3026
|
targets[0] = fromRun;
|
|
@@ -2615,24 +3039,24 @@ function snapshotNow(input) {
|
|
|
2615
3039
|
continue;
|
|
2616
3040
|
}
|
|
2617
3041
|
if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
|
|
2618
|
-
files[toPosix(
|
|
3042
|
+
files[toPosix(path6.relative(listRoot, target))] = "missing";
|
|
2619
3043
|
continue;
|
|
2620
3044
|
}
|
|
2621
|
-
const directory = isDirectory2 ? target :
|
|
2622
|
-
if (
|
|
3045
|
+
const directory = isDirectory2 ? target : path6.dirname(target);
|
|
3046
|
+
if (fs6.existsSync(directory)) {
|
|
2623
3047
|
enqueue(directory, file, reference, anchored && !open);
|
|
2624
3048
|
}
|
|
2625
3049
|
}
|
|
2626
3050
|
};
|
|
2627
3051
|
const reached = /* @__PURE__ */ new Set();
|
|
2628
3052
|
const reach = (file) => {
|
|
2629
|
-
if (reached.has(file) || DOCUMENTS.includes(
|
|
3053
|
+
if (reached.has(file) || DOCUMENTS.includes(path6.extname(file).toLowerCase())) {
|
|
2630
3054
|
return;
|
|
2631
3055
|
}
|
|
2632
3056
|
reached.add(file);
|
|
2633
3057
|
let text;
|
|
2634
3058
|
try {
|
|
2635
|
-
const content =
|
|
3059
|
+
const content = fs6.readFileSync(file);
|
|
2636
3060
|
if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
|
|
2637
3061
|
return;
|
|
2638
3062
|
}
|
|
@@ -2664,7 +3088,7 @@ function snapshotNow(input) {
|
|
|
2664
3088
|
const end = (match.index ?? 0) + match[0].length;
|
|
2665
3089
|
const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
|
|
2666
3090
|
const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
|
|
2667
|
-
follow(
|
|
3091
|
+
follow(path6.posix.join(...segments), open, true, file, reference);
|
|
2668
3092
|
}
|
|
2669
3093
|
for (const match of text.matchAll(UPWARD)) {
|
|
2670
3094
|
const at = match.index ?? 0;
|
|
@@ -2684,35 +3108,35 @@ function snapshotNow(input) {
|
|
|
2684
3108
|
}
|
|
2685
3109
|
};
|
|
2686
3110
|
const add = (relative) => {
|
|
2687
|
-
const file =
|
|
3111
|
+
const file = path6.join(listRoot, relative);
|
|
2688
3112
|
if (relative in files || written.some((target) => isInside(target, file))) {
|
|
2689
3113
|
return;
|
|
2690
3114
|
}
|
|
2691
3115
|
let stat;
|
|
2692
3116
|
try {
|
|
2693
|
-
stat =
|
|
3117
|
+
stat = fs6.lstatSync(file);
|
|
2694
3118
|
} catch {
|
|
2695
3119
|
files[relative] = "missing";
|
|
2696
3120
|
return;
|
|
2697
3121
|
}
|
|
2698
3122
|
if (stat.isSymbolicLink()) {
|
|
2699
|
-
files[relative] = `link ${
|
|
3123
|
+
files[relative] = `link ${fs6.readlinkSync(file)}`;
|
|
2700
3124
|
let target;
|
|
2701
3125
|
try {
|
|
2702
|
-
target =
|
|
3126
|
+
target = fs6.realpathSync(file);
|
|
2703
3127
|
} catch {
|
|
2704
3128
|
return;
|
|
2705
3129
|
}
|
|
2706
|
-
enqueue(
|
|
3130
|
+
enqueue(fs6.statSync(target).isDirectory() ? target : path6.dirname(target), file);
|
|
2707
3131
|
} else if (stat.isDirectory()) {
|
|
2708
3132
|
files[relative] = "directory";
|
|
2709
3133
|
walk(file, true).forEach(add);
|
|
2710
3134
|
} else if (!stat.isFile()) {
|
|
2711
3135
|
files[relative] = "special";
|
|
2712
3136
|
} else {
|
|
2713
|
-
const content =
|
|
3137
|
+
const content = fs6.readFileSync(file);
|
|
2714
3138
|
files[relative] = hashOf(content);
|
|
2715
|
-
const document = DOCUMENTS.includes(
|
|
3139
|
+
const document = DOCUMENTS.includes(path6.extname(file).toLowerCase());
|
|
2716
3140
|
const loaded = followed === null || !isInside(dir, file) || followed.has(file);
|
|
2717
3141
|
if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
|
|
2718
3142
|
scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
|
|
@@ -2723,32 +3147,32 @@ function snapshotNow(input) {
|
|
|
2723
3147
|
const besideAncestors = /* @__PURE__ */ new Set();
|
|
2724
3148
|
const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
|
|
2725
3149
|
for (const relative of ancestorFiles) {
|
|
2726
|
-
besideAncestors.add(
|
|
3150
|
+
besideAncestors.add(path6.join(listRoot, relative));
|
|
2727
3151
|
}
|
|
2728
3152
|
ancestorFiles.forEach(add);
|
|
2729
3153
|
for (const directory of [dir, ...ancestors]) {
|
|
2730
|
-
const patches =
|
|
2731
|
-
if (
|
|
3154
|
+
const patches = path6.join(directory, "patches");
|
|
3155
|
+
if (fs6.existsSync(patches) && fs6.statSync(patches).isDirectory()) {
|
|
2732
3156
|
enqueue(patches, directory);
|
|
2733
3157
|
}
|
|
2734
3158
|
}
|
|
2735
3159
|
for (const directory of [dir, ...ancestors]) {
|
|
2736
|
-
for (const linked of linkedPackages(
|
|
3160
|
+
for (const linked of linkedPackages(path6.join(directory, "node_modules"), listRoot)) {
|
|
2737
3161
|
if (!isInside(linked, dir)) {
|
|
2738
|
-
enqueue(linked,
|
|
3162
|
+
enqueue(linked, path6.join(directory, "node_modules"));
|
|
2739
3163
|
}
|
|
2740
3164
|
}
|
|
2741
3165
|
}
|
|
2742
3166
|
const above = withinBounds(listRoot).slice(1);
|
|
2743
3167
|
for (const directory of [dir, ...ancestors, ...above]) {
|
|
2744
3168
|
const candidates = [
|
|
2745
|
-
...INSTALL_MARKERS.map((marker) =>
|
|
2746
|
-
...above.includes(directory) ? LOCK_FILES.map((name) =>
|
|
3169
|
+
...INSTALL_MARKERS.map((marker) => path6.join(directory, "node_modules", marker)),
|
|
3170
|
+
...above.includes(directory) ? LOCK_FILES.map((name) => path6.join(directory, name)) : []
|
|
2747
3171
|
];
|
|
2748
3172
|
for (const file of candidates) {
|
|
2749
3173
|
const hash = hashFile(file);
|
|
2750
3174
|
if (hash !== null) {
|
|
2751
|
-
files[toPosix(
|
|
3175
|
+
files[toPosix(path6.relative(listRoot, file))] = hash;
|
|
2752
3176
|
}
|
|
2753
3177
|
}
|
|
2754
3178
|
}
|
|
@@ -2760,9 +3184,9 @@ function snapshotNow(input) {
|
|
|
2760
3184
|
};
|
|
2761
3185
|
drain();
|
|
2762
3186
|
for (const file of followed ?? []) {
|
|
2763
|
-
const parts =
|
|
2764
|
-
if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) &&
|
|
2765
|
-
add(toPosix(
|
|
3187
|
+
const parts = path6.relative(listRoot, file).split(path6.sep);
|
|
3188
|
+
if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs6.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
|
|
3189
|
+
add(toPosix(path6.relative(listRoot, file)));
|
|
2766
3190
|
}
|
|
2767
3191
|
}
|
|
2768
3192
|
drain();
|
|
@@ -2782,14 +3206,18 @@ function snapshotNow(input) {
|
|
|
2782
3206
|
}
|
|
2783
3207
|
|
|
2784
3208
|
// src/affected-files.ts
|
|
2785
|
-
import
|
|
3209
|
+
import fs7 from "fs";
|
|
2786
3210
|
import { builtinModules as builtinModules2 } from "module";
|
|
2787
|
-
import
|
|
3211
|
+
import path7 from "path";
|
|
2788
3212
|
var AffectedError = class extends Error {
|
|
2789
3213
|
};
|
|
2790
|
-
var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:glob|resolve)\b/;
|
|
3214
|
+
var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readFile|readdirSync|fromFile|fromURL)\b|\bimport\.meta\.(?:glob|resolve)\b/;
|
|
3215
|
+
var PATH_FROM_TOP = /^[^\s./?#*:][^\s?#*:]*\.(?:html?|md|json)$/i;
|
|
3216
|
+
var withoutQuery = (specifier) => specifier.replace(/\?[^/]*$/, "");
|
|
3217
|
+
var RUNTIME_LOADS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire)\b|\bimport\.meta\.(?:glob|resolve)\b/;
|
|
2791
3218
|
var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
|
|
2792
3219
|
var LOCATION_READS = /\b(?:readFile|readdir|readlink|existsSync|stat|statSync|lstat|lstatSync|access|accessSync|realpath|realpathSync|open|openSync|opendir|opendirSync|createReadStream|watch|watchFile|glob|globSync|promises)\b|\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
|
|
3220
|
+
var LOCATION_LOADS = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
|
|
2793
3221
|
var LOCATION_SAFE = /* @__PURE__ */ new Set([
|
|
2794
3222
|
"path",
|
|
2795
3223
|
"path/posix",
|
|
@@ -2810,30 +3238,31 @@ var FILE_MODULES = /* @__PURE__ */ new Set([
|
|
|
2810
3238
|
"fast-glob",
|
|
2811
3239
|
"globby"
|
|
2812
3240
|
]);
|
|
3241
|
+
var LOADING_MODULES = /* @__PURE__ */ new Set(["child_process", "module", "worker_threads"]);
|
|
2813
3242
|
var bare = (specifier) => specifier.replace(/^node:/, "");
|
|
2814
3243
|
function linkedPackage(specifier, from) {
|
|
2815
3244
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2816
|
-
for (let current =
|
|
2817
|
-
const candidate =
|
|
2818
|
-
if (
|
|
3245
|
+
for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
|
|
3246
|
+
const candidate = path7.join(current, "node_modules", name);
|
|
3247
|
+
if (fs7.existsSync(candidate)) {
|
|
2819
3248
|
const target = realpath(candidate);
|
|
2820
|
-
return target.split(
|
|
3249
|
+
return target.split(path7.sep).includes("node_modules") ? null : target;
|
|
2821
3250
|
}
|
|
2822
|
-
if (current ===
|
|
3251
|
+
if (current === path7.dirname(current)) {
|
|
2823
3252
|
return null;
|
|
2824
3253
|
}
|
|
2825
3254
|
}
|
|
2826
3255
|
}
|
|
2827
|
-
var toPosix2 = (file) => file.split(
|
|
3256
|
+
var toPosix2 = (file) => file.split(path7.sep).join("/");
|
|
2828
3257
|
function realpath(file) {
|
|
2829
3258
|
try {
|
|
2830
|
-
return
|
|
3259
|
+
return fs7.realpathSync.native(file);
|
|
2831
3260
|
} catch {
|
|
2832
3261
|
return file;
|
|
2833
3262
|
}
|
|
2834
3263
|
}
|
|
2835
3264
|
function runnerConfigs(dir, stryker, options) {
|
|
2836
|
-
const files = [
|
|
3265
|
+
const files = [path7.resolve(dir, stryker)];
|
|
2837
3266
|
const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
|
|
2838
3267
|
if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
|
|
2839
3268
|
throw new AffectedError(
|
|
@@ -2841,20 +3270,20 @@ function runnerConfigs(dir, stryker, options) {
|
|
|
2841
3270
|
);
|
|
2842
3271
|
}
|
|
2843
3272
|
if (typeof named === "string") {
|
|
2844
|
-
files.push(
|
|
3273
|
+
files.push(path7.resolve(dir, named));
|
|
2845
3274
|
} else {
|
|
2846
3275
|
const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
|
|
2847
3276
|
for (const name of names) {
|
|
2848
3277
|
for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
|
|
2849
|
-
const file =
|
|
2850
|
-
if (
|
|
3278
|
+
const file = path7.join(dir, name + extension);
|
|
3279
|
+
if (fs7.existsSync(file)) {
|
|
2851
3280
|
files.push(file);
|
|
2852
3281
|
}
|
|
2853
3282
|
}
|
|
2854
3283
|
}
|
|
2855
3284
|
}
|
|
2856
3285
|
return {
|
|
2857
|
-
files: files.filter((file) =>
|
|
3286
|
+
files: files.filter((file) => fs7.existsSync(file)).map(realpath),
|
|
2858
3287
|
runner: options.testRunner,
|
|
2859
3288
|
checkers: Array.isArray(options.checkers) && options.checkers.length > 0
|
|
2860
3289
|
};
|
|
@@ -2875,24 +3304,24 @@ function typesErased(configs, texts, compiler) {
|
|
|
2875
3304
|
erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
|
|
2876
3305
|
}
|
|
2877
3306
|
}
|
|
2878
|
-
const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(
|
|
3307
|
+
const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path7.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
|
|
2879
3308
|
return { erased, inline };
|
|
2880
3309
|
}
|
|
2881
3310
|
function compilerConfigs(dir, root, keys) {
|
|
2882
3311
|
const above = /* @__PURE__ */ new Set();
|
|
2883
|
-
for (let current =
|
|
2884
|
-
const relative =
|
|
2885
|
-
if (relative.startsWith("..") ||
|
|
3312
|
+
for (let current = path7.dirname(dir); ; current = path7.dirname(current)) {
|
|
3313
|
+
const relative = path7.relative(root, current);
|
|
3314
|
+
if (relative.startsWith("..") || path7.isAbsolute(relative)) {
|
|
2886
3315
|
break;
|
|
2887
3316
|
}
|
|
2888
3317
|
above.add(current);
|
|
2889
|
-
if (current ===
|
|
3318
|
+
if (current === path7.dirname(current)) {
|
|
2890
3319
|
break;
|
|
2891
3320
|
}
|
|
2892
3321
|
}
|
|
2893
|
-
const compared = new Set(keys.map((key) =>
|
|
3322
|
+
const compared = new Set(keys.map((key) => path7.join(root, key)));
|
|
2894
3323
|
const starts = [...compared].filter(
|
|
2895
|
-
(file) => (COMPILER_CONFIG.test(
|
|
3324
|
+
(file) => (COMPILER_CONFIG.test(path7.basename(file)) || path7.basename(file) === "package.json") && (file.startsWith(`${dir}${path7.sep}`) || above.has(path7.dirname(file)))
|
|
2896
3325
|
);
|
|
2897
3326
|
const found = [];
|
|
2898
3327
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2903,9 +3332,9 @@ function compilerConfigs(dir, root, keys) {
|
|
|
2903
3332
|
}
|
|
2904
3333
|
seen.add(file);
|
|
2905
3334
|
let text = null;
|
|
2906
|
-
if (compared.has(file) || file.split(
|
|
3335
|
+
if (compared.has(file) || file.split(path7.sep).includes("node_modules")) {
|
|
2907
3336
|
try {
|
|
2908
|
-
text =
|
|
3337
|
+
text = fs7.readFileSync(file, "utf8");
|
|
2909
3338
|
} catch {
|
|
2910
3339
|
text = null;
|
|
2911
3340
|
}
|
|
@@ -2921,24 +3350,24 @@ function extended(base, from) {
|
|
|
2921
3350
|
const candidates = (start) => [
|
|
2922
3351
|
start,
|
|
2923
3352
|
`${start}.json`,
|
|
2924
|
-
|
|
3353
|
+
path7.join(start, "tsconfig.json")
|
|
2925
3354
|
];
|
|
2926
|
-
if (base.startsWith(".") ||
|
|
2927
|
-
return candidates(
|
|
3355
|
+
if (base.startsWith(".") || path7.isAbsolute(base)) {
|
|
3356
|
+
return candidates(path7.resolve(path7.dirname(from), base)).find(isFile2) ?? null;
|
|
2928
3357
|
}
|
|
2929
|
-
for (let current =
|
|
2930
|
-
const file = candidates(
|
|
3358
|
+
for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
|
|
3359
|
+
const file = candidates(path7.join(current, "node_modules", base)).find(isFile2);
|
|
2931
3360
|
if (file !== void 0) {
|
|
2932
3361
|
return file;
|
|
2933
3362
|
}
|
|
2934
|
-
if (current ===
|
|
3363
|
+
if (current === path7.dirname(current)) {
|
|
2935
3364
|
return null;
|
|
2936
3365
|
}
|
|
2937
3366
|
}
|
|
2938
3367
|
}
|
|
2939
3368
|
function isFile2(file) {
|
|
2940
3369
|
try {
|
|
2941
|
-
return
|
|
3370
|
+
return fs7.statSync(file).isFile();
|
|
2942
3371
|
} catch {
|
|
2943
3372
|
return false;
|
|
2944
3373
|
}
|
|
@@ -2949,29 +3378,33 @@ function fileEdges(file, context) {
|
|
|
2949
3378
|
if (known !== void 0) {
|
|
2950
3379
|
return known;
|
|
2951
3380
|
}
|
|
2952
|
-
const shown = (one) => toPosix2(
|
|
3381
|
+
const shown = (one) => toPosix2(path7.relative(dir, one));
|
|
2953
3382
|
const target = (specifier, from) => {
|
|
2954
3383
|
if (specifier.startsWith(".")) {
|
|
2955
|
-
return resolveFiles(
|
|
3384
|
+
return resolveFiles(path7.resolve(path7.dirname(from), specifier));
|
|
2956
3385
|
}
|
|
2957
3386
|
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2958
|
-
return alias === void 0 ? [] : resolveFiles(
|
|
3387
|
+
return alias === void 0 ? [] : resolveFiles(path7.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
|
|
3388
|
+
};
|
|
3389
|
+
const found = { loads: [], unknown: null, loading: null, computed: false, unnamed: [] };
|
|
3390
|
+
const loading = (reason) => {
|
|
3391
|
+
found.unknown ??= reason;
|
|
3392
|
+
found.loading ??= reason;
|
|
2959
3393
|
};
|
|
2960
|
-
const found = { loads: [], unknown: null, computed: false, unnamed: [] };
|
|
2961
3394
|
cache.set(file, found);
|
|
2962
|
-
const snapshotFile =
|
|
2963
|
-
|
|
3395
|
+
const snapshotFile = path7.join(
|
|
3396
|
+
path7.dirname(file),
|
|
2964
3397
|
"__snapshots__",
|
|
2965
|
-
`${
|
|
3398
|
+
`${path7.basename(file)}.snap`
|
|
2966
3399
|
);
|
|
2967
|
-
if (
|
|
3400
|
+
if (fs7.existsSync(snapshotFile)) {
|
|
2968
3401
|
found.loads.push(realpath(snapshotFile));
|
|
2969
3402
|
}
|
|
2970
|
-
const mocks =
|
|
2971
|
-
const name =
|
|
3403
|
+
const mocks = path7.join(path7.dirname(file), "__mocks__");
|
|
3404
|
+
const name = path7.basename(file);
|
|
2972
3405
|
const candidates = isDirectory(mocks) ? [
|
|
2973
|
-
|
|
2974
|
-
...resolveFiles(
|
|
3406
|
+
path7.join(mocks, name),
|
|
3407
|
+
...resolveFiles(path7.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
|
|
2975
3408
|
] : [];
|
|
2976
3409
|
for (const mock of new Set(candidates)) {
|
|
2977
3410
|
if (isFile2(mock)) {
|
|
@@ -2981,18 +3414,23 @@ function fileEdges(file, context) {
|
|
|
2981
3414
|
if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
|
|
2982
3415
|
return found;
|
|
2983
3416
|
}
|
|
2984
|
-
const source =
|
|
3417
|
+
const source = fs7.readFileSync(file, "utf8");
|
|
2985
3418
|
const code = withoutComments(source);
|
|
2986
3419
|
if (COMPONENT.test(file)) {
|
|
2987
|
-
|
|
3420
|
+
loading(`${shown(file)} is a component, which can use others that it does not import`);
|
|
2988
3421
|
}
|
|
2989
3422
|
const uncheckable = findUncheckableImports(code)[0];
|
|
2990
3423
|
const runtime = RUNTIME_PATHS.exec(code);
|
|
3424
|
+
const runtimeLoads = RUNTIME_LOADS.exec(code);
|
|
2991
3425
|
if (uncheckable !== void 0) {
|
|
2992
3426
|
found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
|
|
3427
|
+
found.loading = found.unknown;
|
|
2993
3428
|
found.computed = true;
|
|
2994
3429
|
} else if (runtime !== null) {
|
|
2995
3430
|
found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
|
|
3431
|
+
if (runtimeLoads !== null) {
|
|
3432
|
+
found.loading ??= `${shown(file)} uses ${runtimeLoads[0]}, which can reach files that no import names`;
|
|
3433
|
+
}
|
|
2996
3434
|
}
|
|
2997
3435
|
const location = LOCATION.exec(code);
|
|
2998
3436
|
const refs = parseImports(code);
|
|
@@ -3007,30 +3445,39 @@ function fileEdges(file, context) {
|
|
|
3007
3445
|
typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
|
|
3008
3446
|
}
|
|
3009
3447
|
for (const specifier of specifiers) {
|
|
3010
|
-
if (
|
|
3448
|
+
if (path7.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
3011
3449
|
const resolved2 = resolveFiles(
|
|
3012
|
-
specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
|
|
3450
|
+
withoutQuery(specifier.startsWith("file:") ? new URL(specifier).pathname : specifier)
|
|
3013
3451
|
);
|
|
3014
3452
|
if (resolved2.length === 0) {
|
|
3015
|
-
|
|
3453
|
+
loading(`${shown(file)} imports ${specifier}, which leads to no file`);
|
|
3016
3454
|
} else {
|
|
3017
3455
|
found.loads.push(...resolved2.map(realpath));
|
|
3018
3456
|
}
|
|
3019
3457
|
} else if (isProjectImport(specifier, aliases)) {
|
|
3020
|
-
const resolved2 = target(specifier, file);
|
|
3458
|
+
const resolved2 = target(withoutQuery(specifier), file);
|
|
3021
3459
|
if (resolved2.length === 0) {
|
|
3022
|
-
|
|
3460
|
+
loading(`${shown(file)} imports ${specifier}, which leads to no file`);
|
|
3023
3461
|
} else {
|
|
3024
3462
|
found.loads.push(...resolved2.map(realpath));
|
|
3025
3463
|
}
|
|
3026
3464
|
} else if (FILE_MODULES.has(bare(specifier))) {
|
|
3027
|
-
|
|
3465
|
+
const reason = `${shown(file)} imports ${specifier}, which can reach files that no import names`;
|
|
3466
|
+
if (LOADING_MODULES.has(bare(specifier))) {
|
|
3467
|
+
loading(reason);
|
|
3468
|
+
} else {
|
|
3469
|
+
found.unknown ??= reason;
|
|
3470
|
+
}
|
|
3028
3471
|
} else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
|
|
3029
3472
|
if (!installed(specifier, file)) {
|
|
3030
|
-
|
|
3473
|
+
loading(
|
|
3474
|
+
`${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`
|
|
3475
|
+
);
|
|
3031
3476
|
found.unnamed.push(specifier);
|
|
3032
3477
|
} else if (linkedPackage(specifier, file) !== null) {
|
|
3033
|
-
|
|
3478
|
+
loading(
|
|
3479
|
+
`${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`
|
|
3480
|
+
);
|
|
3034
3481
|
found.computed = true;
|
|
3035
3482
|
}
|
|
3036
3483
|
}
|
|
@@ -3038,10 +3485,14 @@ function fileEdges(file, context) {
|
|
|
3038
3485
|
if (location !== null) {
|
|
3039
3486
|
const reads = LOCATION_READS.exec(code);
|
|
3040
3487
|
const reader = [...specifiers].find(
|
|
3041
|
-
(specifier) => !specifier.startsWith(".") && !
|
|
3488
|
+
(specifier) => !specifier.startsWith(".") && !path7.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
|
|
3042
3489
|
);
|
|
3043
3490
|
if (reads !== null) {
|
|
3044
3491
|
found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
|
|
3492
|
+
const loads = LOCATION_LOADS.exec(code);
|
|
3493
|
+
if (loads !== null) {
|
|
3494
|
+
found.loading ??= `${shown(file)} uses ${location[0]} and ${loads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
|
|
3495
|
+
}
|
|
3045
3496
|
} else if (reader !== void 0) {
|
|
3046
3497
|
found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
|
|
3047
3498
|
}
|
|
@@ -3060,7 +3511,9 @@ function fileEdges(file, context) {
|
|
|
3060
3511
|
if (left > 0) {
|
|
3061
3512
|
typeLines.set(key, left - 1);
|
|
3062
3513
|
} else if (text.startsWith(".") && !specifiers.has(text)) {
|
|
3063
|
-
found.loads.push(...resolveFiles(
|
|
3514
|
+
found.loads.push(...resolveFiles(path7.resolve(path7.dirname(file), text)).map(realpath));
|
|
3515
|
+
} else if (PATH_FROM_TOP.test(text) && isFile2(path7.resolve(dir, text))) {
|
|
3516
|
+
found.loads.push(realpath(path7.resolve(dir, text)));
|
|
3064
3517
|
}
|
|
3065
3518
|
}
|
|
3066
3519
|
return found;
|
|
@@ -3072,25 +3525,25 @@ function inputsOf(manifest, input) {
|
|
|
3072
3525
|
const dir = realpath(manifest.dir);
|
|
3073
3526
|
const aliases = manifest.settings.importAliases;
|
|
3074
3527
|
const placed = [...input.tests, ...Object.values(input.covering).flat()];
|
|
3075
|
-
const absolute = placed.find((file) =>
|
|
3528
|
+
const absolute = placed.find((file) => path7.isAbsolute(file) || file.startsWith(".."));
|
|
3076
3529
|
if (absolute !== void 0) {
|
|
3077
3530
|
throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
|
|
3078
3531
|
}
|
|
3079
3532
|
const keyOf2 = (file) => {
|
|
3080
|
-
const relative =
|
|
3081
|
-
if (relative.startsWith("..") ||
|
|
3533
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3534
|
+
if (relative.startsWith("..") || path7.isAbsolute(relative)) {
|
|
3082
3535
|
throw new AffectedError(`${file} is outside of the files that are compared`);
|
|
3083
3536
|
}
|
|
3084
3537
|
return toPosix2(relative);
|
|
3085
3538
|
};
|
|
3086
|
-
const shown = (file) => toPosix2(
|
|
3539
|
+
const shown = (file) => toPosix2(path7.relative(dir, file));
|
|
3087
3540
|
const plain2 = /* @__PURE__ */ new Map();
|
|
3088
3541
|
const configTexts = [];
|
|
3089
3542
|
const runnerSources = [];
|
|
3090
3543
|
const reached = new Set(input.configs.files);
|
|
3091
3544
|
const pending = [...input.configs.files];
|
|
3092
3545
|
for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
|
|
3093
|
-
const text =
|
|
3546
|
+
const text = fs7.readFileSync(file, "utf8");
|
|
3094
3547
|
configTexts.push(text);
|
|
3095
3548
|
runnerSources.push({ file, text });
|
|
3096
3549
|
for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
|
|
@@ -3108,7 +3561,7 @@ function inputsOf(manifest, input) {
|
|
|
3108
3561
|
// Not what lies outside of the compared files: this package, imported by path, is installed.
|
|
3109
3562
|
runner: [
|
|
3110
3563
|
...runnerSources.filter(({ file }) => {
|
|
3111
|
-
const relative =
|
|
3564
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3112
3565
|
return !relative.startsWith("..") && toPosix2(relative) in input.files;
|
|
3113
3566
|
}),
|
|
3114
3567
|
...jestInPackage(dir, input.configs)
|
|
@@ -3121,14 +3574,21 @@ function inputsOf(manifest, input) {
|
|
|
3121
3574
|
}
|
|
3122
3575
|
const edges = /* @__PURE__ */ new Map();
|
|
3123
3576
|
const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
|
|
3577
|
+
const blind = /* @__PURE__ */ Object.create(null);
|
|
3124
3578
|
const closure = (start, strict) => {
|
|
3125
3579
|
const seen = /* @__PURE__ */ new Set([start]);
|
|
3126
3580
|
const queue = [start];
|
|
3127
3581
|
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
3128
3582
|
const found = edgesOf(file);
|
|
3129
|
-
if (strict && found.unknown !== null) {
|
|
3583
|
+
if (strict === "every" && found.unknown !== null) {
|
|
3130
3584
|
throw new AffectedError(found.unknown);
|
|
3131
3585
|
}
|
|
3586
|
+
if (strict === "gate" && found.unknown !== null) {
|
|
3587
|
+
if (found.loading !== null) {
|
|
3588
|
+
throw new AffectedError(found.loading);
|
|
3589
|
+
}
|
|
3590
|
+
blind[keyOf2(file)] = found.unknown;
|
|
3591
|
+
}
|
|
3132
3592
|
for (const next of found.loads) {
|
|
3133
3593
|
if (!seen.has(next)) {
|
|
3134
3594
|
seen.add(next);
|
|
@@ -3139,8 +3599,8 @@ function inputsOf(manifest, input) {
|
|
|
3139
3599
|
return [...seen];
|
|
3140
3600
|
};
|
|
3141
3601
|
const isCompared = (file) => {
|
|
3142
|
-
const relative =
|
|
3143
|
-
return !relative.startsWith("..") && !
|
|
3602
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3603
|
+
return !relative.startsWith("..") && !path7.isAbsolute(relative) && toPosix2(relative) in input.files;
|
|
3144
3604
|
};
|
|
3145
3605
|
const keysOf = (files) => files.map((file) => {
|
|
3146
3606
|
const key = keyOf2(file);
|
|
@@ -3159,10 +3619,10 @@ function inputsOf(manifest, input) {
|
|
|
3159
3619
|
if (!SCRIPT.test(loaded)) {
|
|
3160
3620
|
continue;
|
|
3161
3621
|
}
|
|
3162
|
-
for (const match of
|
|
3622
|
+
for (const match of fs7.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
|
|
3163
3623
|
const text = match[2].replace(/^<rootDir>\//, "");
|
|
3164
|
-
const here = resolveFiles(
|
|
3165
|
-
const candidates = here.length > 0 ? here : resolveFiles(
|
|
3624
|
+
const here = resolveFiles(path7.resolve(path7.dirname(loaded), text), false, false);
|
|
3625
|
+
const candidates = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false, false);
|
|
3166
3626
|
for (const found of candidates) {
|
|
3167
3627
|
if (text.includes("*") || !isCompared(found)) {
|
|
3168
3628
|
continue;
|
|
@@ -3177,7 +3637,7 @@ function inputsOf(manifest, input) {
|
|
|
3177
3637
|
}
|
|
3178
3638
|
}
|
|
3179
3639
|
const gatePaths = new Map(
|
|
3180
|
-
manifest.gates.map((gate) => [realpath(
|
|
3640
|
+
manifest.gates.map((gate) => [realpath(path7.join(dir, gate.path)), gate.path])
|
|
3181
3641
|
);
|
|
3182
3642
|
const tests = [
|
|
3183
3643
|
.../* @__PURE__ */ new Set([
|
|
@@ -3188,13 +3648,13 @@ function inputsOf(manifest, input) {
|
|
|
3188
3648
|
];
|
|
3189
3649
|
const runAsSuch = new Set(
|
|
3190
3650
|
[...tests, ...manifest.gates.map((gate) => gate.path)].map(
|
|
3191
|
-
(file) => realpath(
|
|
3651
|
+
(file) => realpath(path7.join(dir, file))
|
|
3192
3652
|
)
|
|
3193
3653
|
);
|
|
3194
|
-
const subprojectKey = toPosix2(
|
|
3654
|
+
const subprojectKey = toPosix2(path7.relative(input.root, dir));
|
|
3195
3655
|
const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
|
|
3196
3656
|
for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
|
|
3197
|
-
for (const loaded of closure(file,
|
|
3657
|
+
for (const loaded of closure(file, "every")) {
|
|
3198
3658
|
const gate = gatePaths.get(loaded);
|
|
3199
3659
|
if (gate !== void 0) {
|
|
3200
3660
|
throw new AffectedError(
|
|
@@ -3204,16 +3664,16 @@ function inputsOf(manifest, input) {
|
|
|
3204
3664
|
config.add(loaded);
|
|
3205
3665
|
}
|
|
3206
3666
|
}
|
|
3207
|
-
const fromSubproject = (file) =>
|
|
3667
|
+
const fromSubproject = (file) => path7.join(dir, file);
|
|
3208
3668
|
const closures = /* @__PURE__ */ Object.create(null);
|
|
3209
3669
|
for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
|
|
3210
3670
|
const absolute2 = fromSubproject(file);
|
|
3211
|
-
if (!
|
|
3671
|
+
if (!fs7.existsSync(absolute2)) {
|
|
3212
3672
|
continue;
|
|
3213
3673
|
}
|
|
3214
|
-
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2),
|
|
3674
|
+
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), "gate"));
|
|
3215
3675
|
}
|
|
3216
|
-
const keyed = (files) => files.map((file) => toPosix2(
|
|
3676
|
+
const keyed = (files) => files.map((file) => toPosix2(path7.relative(input.root, path7.join(dir, file))));
|
|
3217
3677
|
const dependencies = gateDependencies({
|
|
3218
3678
|
gates: manifest.gates.map((gate) => ({
|
|
3219
3679
|
key: keyed([gate.path])[0],
|
|
@@ -3223,9 +3683,10 @@ function inputsOf(manifest, input) {
|
|
|
3223
3683
|
covering: Object.fromEntries(
|
|
3224
3684
|
Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
|
|
3225
3685
|
),
|
|
3226
|
-
closures
|
|
3686
|
+
closures,
|
|
3687
|
+
blind
|
|
3227
3688
|
});
|
|
3228
|
-
const subproject = toPosix2(
|
|
3689
|
+
const subproject = toPosix2(path7.relative(input.root, dir));
|
|
3229
3690
|
const prefix = subproject === "" ? "" : `${subproject}/`;
|
|
3230
3691
|
const gates = /* @__PURE__ */ Object.create(null);
|
|
3231
3692
|
for (const gate of manifest.gates) {
|
|
@@ -3236,12 +3697,13 @@ function inputsOf(manifest, input) {
|
|
|
3236
3697
|
specs: found.specs.map(
|
|
3237
3698
|
(spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
|
|
3238
3699
|
),
|
|
3239
|
-
files: found.files
|
|
3700
|
+
files: found.files,
|
|
3701
|
+
...found.blind === void 0 ? {} : { blind: found.blind }
|
|
3240
3702
|
};
|
|
3241
3703
|
}
|
|
3242
3704
|
const packageMocks = Object.keys(input.files).filter(
|
|
3243
3705
|
(key) => isNestedMock(key, subproject) && mockNames(key).some(
|
|
3244
|
-
(name) => builtinModules2.includes(name) || installed(name,
|
|
3706
|
+
(name) => builtinModules2.includes(name) || installed(name, path7.join(input.root, key))
|
|
3245
3707
|
)
|
|
3246
3708
|
);
|
|
3247
3709
|
return {
|
|
@@ -3297,7 +3759,7 @@ function scopeOf(manifest, configs, listed2) {
|
|
|
3297
3759
|
const cache = /* @__PURE__ */ new Map();
|
|
3298
3760
|
const context = { dir, aliases: manifest.settings.importAliases, cache };
|
|
3299
3761
|
const runner = configs.files.slice(1);
|
|
3300
|
-
const texts = runner.map((file) => withoutComments(
|
|
3762
|
+
const texts = runner.map((file) => withoutComments(fs7.readFileSync(file, "utf8")));
|
|
3301
3763
|
if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
|
|
3302
3764
|
return { every: NOT_THE_HELPER2 };
|
|
3303
3765
|
}
|
|
@@ -3312,21 +3774,21 @@ function scopeOf(manifest, configs, listed2) {
|
|
|
3312
3774
|
fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
|
|
3313
3775
|
}
|
|
3314
3776
|
}
|
|
3315
|
-
if ([...parts].some((file) => PROJECTS.test(withoutComments(
|
|
3777
|
+
if ([...parts].some((file) => PROJECTS.test(withoutComments(fs7.readFileSync(file, "utf8"))))) {
|
|
3316
3778
|
return {
|
|
3317
3779
|
every: "the runner config, or a file it is put together from, names projects or a workspace"
|
|
3318
3780
|
};
|
|
3319
3781
|
}
|
|
3320
|
-
const named =
|
|
3321
|
-
const mocks =
|
|
3782
|
+
const named = fs7.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path7.join(dir, name));
|
|
3783
|
+
const mocks = path7.join(dir, "__mocks__");
|
|
3322
3784
|
const seeds = [
|
|
3323
3785
|
...configs.files,
|
|
3324
3786
|
...named,
|
|
3325
|
-
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) =>
|
|
3326
|
-
...listed2.filter((file) => file.startsWith(`${mocks}${
|
|
3327
|
-
].filter((file) =>
|
|
3787
|
+
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path7.join(dir, file)),
|
|
3788
|
+
...listed2.filter((file) => file.startsWith(`${mocks}${path7.sep}`))
|
|
3789
|
+
].filter((file) => fs7.existsSync(file)).map(realpath);
|
|
3328
3790
|
const naming = new Set(
|
|
3329
|
-
[...configs.files, ...named].filter((file) =>
|
|
3791
|
+
[...configs.files, ...named].filter((file) => fs7.existsSync(file)).map(realpath)
|
|
3330
3792
|
);
|
|
3331
3793
|
const seen = new Set(seeds);
|
|
3332
3794
|
const queue = [...seeds];
|
|
@@ -3338,7 +3800,7 @@ function scopeOf(manifest, configs, listed2) {
|
|
|
3338
3800
|
}
|
|
3339
3801
|
};
|
|
3340
3802
|
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
3341
|
-
if (
|
|
3803
|
+
if (fs7.statSync(file).isDirectory()) {
|
|
3342
3804
|
continue;
|
|
3343
3805
|
}
|
|
3344
3806
|
const edges = fileEdges(file, context);
|
|
@@ -3349,11 +3811,11 @@ function scopeOf(manifest, configs, listed2) {
|
|
|
3349
3811
|
for (const specifier of edges.unnamed) {
|
|
3350
3812
|
endingAs(specifier, listed2).forEach(add);
|
|
3351
3813
|
}
|
|
3352
|
-
if (naming.has(file) || SCRIPT.test(file) &&
|
|
3814
|
+
if (naming.has(file) || SCRIPT.test(file) && path7.dirname(file) === dir) {
|
|
3353
3815
|
for (const named2 of namedIn(file)) {
|
|
3354
3816
|
const text = named2.replace(/^<rootDir>\//, "");
|
|
3355
|
-
const here = resolveFiles(
|
|
3356
|
-
const found = here.length > 0 ? here : resolveFiles(
|
|
3817
|
+
const here = resolveFiles(path7.resolve(path7.dirname(file), text), false);
|
|
3818
|
+
const found = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false);
|
|
3357
3819
|
for (const one of text.includes("*") ? [] : found) {
|
|
3358
3820
|
if (!SCRIPT.test(one)) {
|
|
3359
3821
|
naming.add(realpath(one));
|
|
@@ -3377,7 +3839,7 @@ function endingAs(specifier, files) {
|
|
|
3377
3839
|
});
|
|
3378
3840
|
}
|
|
3379
3841
|
function namedIn(file) {
|
|
3380
|
-
const text =
|
|
3842
|
+
const text = fs7.readFileSync(file, "utf8");
|
|
3381
3843
|
if (SCRIPT.test(file)) {
|
|
3382
3844
|
return [...text.matchAll(QUOTED)].map((match) => match[2]);
|
|
3383
3845
|
}
|
|
@@ -3531,15 +3993,15 @@ function mayStartPattern(before) {
|
|
|
3531
3993
|
return before.last === "" || !")]".includes(before.last);
|
|
3532
3994
|
}
|
|
3533
3995
|
function jestInPackage(dir, configs) {
|
|
3534
|
-
const file =
|
|
3535
|
-
return configs.runner === "jest" && configs.files.length === 1 &&
|
|
3996
|
+
const file = path7.join(dir, "package.json");
|
|
3997
|
+
return configs.runner === "jest" && configs.files.length === 1 && fs7.existsSync(file) ? [{ file, text: fs7.readFileSync(file, "utf8"), mapperOnly: true }] : [];
|
|
3536
3998
|
}
|
|
3537
3999
|
var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
|
|
3538
4000
|
function stringValue(text) {
|
|
3539
4001
|
const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
|
|
3540
4002
|
return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
|
|
3541
4003
|
}
|
|
3542
|
-
function
|
|
4004
|
+
function partsOf2(text, at) {
|
|
3543
4005
|
const parts = [];
|
|
3544
4006
|
let depth = 0;
|
|
3545
4007
|
let start = at + 1;
|
|
@@ -3576,7 +4038,7 @@ function property(part) {
|
|
|
3576
4038
|
}
|
|
3577
4039
|
function aliasPath(expression, file, text, depth = 0) {
|
|
3578
4040
|
const value = expression.trim();
|
|
3579
|
-
const here =
|
|
4041
|
+
const here = path7.dirname(file);
|
|
3580
4042
|
if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
|
|
3581
4043
|
return here;
|
|
3582
4044
|
}
|
|
@@ -3584,18 +4046,18 @@ function aliasPath(expression, file, text, depth = 0) {
|
|
|
3584
4046
|
`^(?:(?:url\\.)?fileURLToPath\\(\\s*new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\)|new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\.pathname)$`
|
|
3585
4047
|
).exec(value);
|
|
3586
4048
|
if (url !== null) {
|
|
3587
|
-
return
|
|
4049
|
+
return path7.resolve(here, stringValue(url[1] ?? url[2]));
|
|
3588
4050
|
}
|
|
3589
4051
|
const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
|
|
3590
4052
|
if (call !== null) {
|
|
3591
|
-
const found =
|
|
4053
|
+
const found = partsOf2(value, call[0].length - 1);
|
|
3592
4054
|
if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
|
|
3593
4055
|
return null;
|
|
3594
4056
|
}
|
|
3595
4057
|
const [first, ...rest] = found.parts;
|
|
3596
4058
|
const base = aliasPath(first, file, text, depth + 1);
|
|
3597
4059
|
const parts = rest.map(stringValue);
|
|
3598
|
-
return base === null || parts.some((part) => part === null) ? null :
|
|
4060
|
+
return base === null || parts.some((part) => part === null) ? null : path7.resolve(base, ...parts);
|
|
3599
4061
|
}
|
|
3600
4062
|
if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
|
|
3601
4063
|
const declarations = text.match(
|
|
@@ -3611,7 +4073,7 @@ function aliasPath(expression, file, text, depth = 0) {
|
|
|
3611
4073
|
var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
|
|
3612
4074
|
function isDirectory(file) {
|
|
3613
4075
|
try {
|
|
3614
|
-
return
|
|
4076
|
+
return fs7.statSync(file).isDirectory();
|
|
3615
4077
|
} catch {
|
|
3616
4078
|
return false;
|
|
3617
4079
|
}
|
|
@@ -3637,8 +4099,8 @@ function unexplainedMaps(input) {
|
|
|
3637
4099
|
const { dir, aliases } = input;
|
|
3638
4100
|
const reasons = [];
|
|
3639
4101
|
const mapped = [];
|
|
3640
|
-
const shown = (file) => toPosix2(
|
|
3641
|
-
const under = (base, rest) => rest === "" ? `${base}/` :
|
|
4102
|
+
const shown = (file) => toPosix2(path7.relative(dir, file));
|
|
4103
|
+
const under = (base, rest) => rest === "" ? `${base}/` : path7.join(base, rest);
|
|
3642
4104
|
const named = (prefix, target) => {
|
|
3643
4105
|
const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
|
|
3644
4106
|
if (alias === void 0) {
|
|
@@ -3646,10 +4108,10 @@ function unexplainedMaps(input) {
|
|
|
3646
4108
|
}
|
|
3647
4109
|
const full = alias.target + prefix.slice(alias.prefix.length);
|
|
3648
4110
|
const slash = full === "" || full.endsWith("/");
|
|
3649
|
-
return
|
|
4111
|
+
return path7.resolve(dir, full) === path7.resolve(target) && slash === target.endsWith("/");
|
|
3650
4112
|
};
|
|
3651
4113
|
const advice = (prefix, target) => {
|
|
3652
|
-
const relative = toPosix2(
|
|
4114
|
+
const relative = toPosix2(path7.relative(dir, target));
|
|
3653
4115
|
const shownTarget = relative === "" ? "./" : `${relative}/`;
|
|
3654
4116
|
const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
|
|
3655
4117
|
const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
|
|
@@ -3662,7 +4124,7 @@ function unexplainedMaps(input) {
|
|
|
3662
4124
|
let prefix = null;
|
|
3663
4125
|
let to = null;
|
|
3664
4126
|
if (list) {
|
|
3665
|
-
const object = /^\s*\{/.test(part) ?
|
|
4127
|
+
const object = /^\s*\{/.test(part) ? partsOf2(part, part.indexOf("{")) : null;
|
|
3666
4128
|
const fields = Object.fromEntries(
|
|
3667
4129
|
(object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
|
|
3668
4130
|
);
|
|
@@ -3714,7 +4176,7 @@ function unexplainedMaps(input) {
|
|
|
3714
4176
|
const word = match[0];
|
|
3715
4177
|
const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
|
|
3716
4178
|
const at = opening === null ? -1 : match.index + word.length + opening[0].length;
|
|
3717
|
-
const found = at === -1 ? null :
|
|
4179
|
+
const found = at === -1 ? null : partsOf2(text, at);
|
|
3718
4180
|
const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
|
|
3719
4181
|
if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
|
|
3720
4182
|
reasons.push(unreadable);
|
|
@@ -3744,7 +4206,7 @@ function unexplainedMaps(input) {
|
|
|
3744
4206
|
}
|
|
3745
4207
|
const prefix = mapperPrefix(entry.key);
|
|
3746
4208
|
const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
|
|
3747
|
-
const to = target === null ? null : under(
|
|
4209
|
+
const to = target === null ? null : under(path7.dirname(file), target[1]);
|
|
3748
4210
|
if (prefix === null || to === null) {
|
|
3749
4211
|
reasons.push(missing(file, entry.key, word));
|
|
3750
4212
|
} else if (!named(prefix, to)) {
|
|
@@ -3755,19 +4217,19 @@ function unexplainedMaps(input) {
|
|
|
3755
4217
|
}
|
|
3756
4218
|
}
|
|
3757
4219
|
}
|
|
3758
|
-
const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(
|
|
4220
|
+
const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path7.basename(file)));
|
|
3759
4221
|
const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
|
|
3760
4222
|
for (const { file, text } of input.compiler) {
|
|
3761
4223
|
if (text === null) {
|
|
3762
4224
|
continue;
|
|
3763
4225
|
}
|
|
3764
4226
|
const plain2 = withoutComments(text);
|
|
3765
|
-
const name =
|
|
4227
|
+
const name = path7.basename(file);
|
|
3766
4228
|
const swc = name === ".swcrc";
|
|
3767
4229
|
if (!/^[jt]sconfig/.test(name) && !swc) {
|
|
3768
4230
|
const babel = /"babel"\s*:\s*/.exec(plain2);
|
|
3769
4231
|
const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
|
|
3770
|
-
const found2 =
|
|
4232
|
+
const found2 = partsOf2(plain2, babel.index + babel[0].length);
|
|
3771
4233
|
return found2 === null ? plain2 : found2.parts.join(",");
|
|
3772
4234
|
})();
|
|
3773
4235
|
const maps = /module-resolver|\balias\b/.exec(own2);
|
|
@@ -3781,7 +4243,7 @@ function unexplainedMaps(input) {
|
|
|
3781
4243
|
continue;
|
|
3782
4244
|
}
|
|
3783
4245
|
const at = paths.index + paths[0].length;
|
|
3784
|
-
const found = plain2[at] === "{" ?
|
|
4246
|
+
const found = plain2[at] === "{" ? partsOf2(plain2, at) : null;
|
|
3785
4247
|
if (found === null) {
|
|
3786
4248
|
reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
|
|
3787
4249
|
continue;
|
|
@@ -3791,7 +4253,7 @@ function unexplainedMaps(input) {
|
|
|
3791
4253
|
reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
|
|
3792
4254
|
continue;
|
|
3793
4255
|
}
|
|
3794
|
-
const base =
|
|
4256
|
+
const base = path7.resolve(path7.dirname(file), own?.[1] ?? ".");
|
|
3795
4257
|
for (const part of found.parts) {
|
|
3796
4258
|
const entry = property(part);
|
|
3797
4259
|
if (entry === null) {
|
|
@@ -3800,10 +4262,10 @@ function unexplainedMaps(input) {
|
|
|
3800
4262
|
);
|
|
3801
4263
|
continue;
|
|
3802
4264
|
}
|
|
3803
|
-
const list = entry.value.startsWith("[") ?
|
|
4265
|
+
const list = entry.value.startsWith("[") ? partsOf2(entry.value, 0) : null;
|
|
3804
4266
|
const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
|
|
3805
4267
|
const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
|
|
3806
|
-
if (prefix === null && target !== null && !target.includes("*") &&
|
|
4268
|
+
if (prefix === null && target !== null && !target.includes("*") && path7.resolve(base, target).split(path7.sep).includes("node_modules")) {
|
|
3807
4269
|
continue;
|
|
3808
4270
|
}
|
|
3809
4271
|
const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
|
|
@@ -3821,26 +4283,69 @@ function unexplainedMaps(input) {
|
|
|
3821
4283
|
}
|
|
3822
4284
|
|
|
3823
4285
|
// src/commands/mutation.ts
|
|
3824
|
-
import { spawnSync as
|
|
4286
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4287
|
+
import fs10 from "fs";
|
|
4288
|
+
import path10 from "path";
|
|
4289
|
+
import { pathToFileURL } from "url";
|
|
4290
|
+
|
|
4291
|
+
// src/main-worktree.ts
|
|
4292
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3825
4293
|
import fs8 from "fs";
|
|
3826
4294
|
import path8 from "path";
|
|
3827
|
-
|
|
4295
|
+
function git2(dir, args) {
|
|
4296
|
+
const result = spawnSync3("git", args, {
|
|
4297
|
+
cwd: dir,
|
|
4298
|
+
env: gitEnvironment(process.env),
|
|
4299
|
+
encoding: "utf8"
|
|
4300
|
+
});
|
|
4301
|
+
return result.error || result.status !== 0 ? null : result.stdout.trim().split("\n");
|
|
4302
|
+
}
|
|
4303
|
+
function linkedWorktreeOf(dir) {
|
|
4304
|
+
const found = git2(dir, [
|
|
4305
|
+
"rev-parse",
|
|
4306
|
+
"--path-format=absolute",
|
|
4307
|
+
"--show-toplevel",
|
|
4308
|
+
"--git-dir",
|
|
4309
|
+
"--git-common-dir"
|
|
4310
|
+
]);
|
|
4311
|
+
if (found === null || found.length !== 3) {
|
|
4312
|
+
return null;
|
|
4313
|
+
}
|
|
4314
|
+
const [top, gitDir, common] = found;
|
|
4315
|
+
if (gitDir === common || path8.basename(common) !== ".git") {
|
|
4316
|
+
return null;
|
|
4317
|
+
}
|
|
4318
|
+
try {
|
|
4319
|
+
const main2 = fs8.realpathSync(path8.dirname(common));
|
|
4320
|
+
const real = fs8.realpathSync(top);
|
|
4321
|
+
return main2 === real ? null : { top: real, main: main2 };
|
|
4322
|
+
} catch {
|
|
4323
|
+
return null;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
function inMainWorktree(linked, file) {
|
|
4327
|
+
const relative = path8.relative(linked.top, file);
|
|
4328
|
+
if (relative === "" || relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
4329
|
+
return null;
|
|
4330
|
+
}
|
|
4331
|
+
return path8.join(linked.main, relative);
|
|
4332
|
+
}
|
|
3828
4333
|
|
|
3829
4334
|
// src/commands/mutation-result.ts
|
|
3830
|
-
import
|
|
3831
|
-
import
|
|
4335
|
+
import fs9 from "fs";
|
|
4336
|
+
import path9 from "path";
|
|
3832
4337
|
function reportPathOf(manifest, override) {
|
|
3833
|
-
return
|
|
4338
|
+
return path9.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
|
|
3834
4339
|
}
|
|
3835
|
-
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
|
|
4340
|
+
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0, retried = null, shard = null) {
|
|
3836
4341
|
const manifest = readManifest(dir);
|
|
3837
4342
|
const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
|
|
3838
|
-
const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
|
|
4343
|
+
const caveat = only.length === 0 ? "" : shard === null ? gateRunCaveat(gates.length, manifest.gates.length) : shardRunCaveat(shard, gates.length, manifest.gates.length);
|
|
3839
4344
|
const reportPath = reportPathOf(manifest, reportOverride);
|
|
3840
|
-
const shown =
|
|
4345
|
+
const shown = path9.relative(manifest.dir, reportPath) || reportPath;
|
|
3841
4346
|
let report;
|
|
3842
4347
|
try {
|
|
3843
|
-
report = JSON.parse(
|
|
4348
|
+
report = JSON.parse(fs9.readFileSync(reportPath, "utf8"));
|
|
3844
4349
|
} catch (error) {
|
|
3845
4350
|
io2.err(
|
|
3846
4351
|
`test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
|
|
@@ -3857,7 +4362,7 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3857
4362
|
maxTimeouts: manifest.settings.maxTimeouts,
|
|
3858
4363
|
readSource: (gatePath) => {
|
|
3859
4364
|
try {
|
|
3860
|
-
return
|
|
4365
|
+
return fs9.readFileSync(path9.join(manifest.dir, gatePath), "utf8");
|
|
3861
4366
|
} catch {
|
|
3862
4367
|
return void 0;
|
|
3863
4368
|
}
|
|
@@ -3905,8 +4410,34 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3905
4410
|
io2.err(` - ${formatSurvivor(mutant)}`);
|
|
3906
4411
|
}
|
|
3907
4412
|
io2.err("");
|
|
3908
|
-
|
|
3909
|
-
|
|
4413
|
+
if (retried === null || retried === "failed") {
|
|
4414
|
+
io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
|
|
4415
|
+
if (retried === null) {
|
|
4416
|
+
io2.err(
|
|
4417
|
+
"can time out instead. `test-gates mutation` runs such timeouts again one at a time"
|
|
4418
|
+
);
|
|
4419
|
+
io2.err("(--concurrency 1) before it fails; these were not run again. Only for a");
|
|
4420
|
+
} else {
|
|
4421
|
+
io2.err(
|
|
4422
|
+
"can time out instead. They were run again one at a time (--concurrency 1), but that"
|
|
4423
|
+
);
|
|
4424
|
+
io2.err("run gave no result (see above), so these are those of the first run. Only for a");
|
|
4425
|
+
}
|
|
4426
|
+
} else if (retried.missing === 0) {
|
|
4427
|
+
io2.err("They timed out again when they were run one at a time (--concurrency 1), so they");
|
|
4428
|
+
io2.err("most likely never finish. Look at each mutant first. Only for a");
|
|
4429
|
+
} else {
|
|
4430
|
+
const again = retried.statuses["Timeout"] ?? 0;
|
|
4431
|
+
if (again > 0) {
|
|
4432
|
+
io2.err(
|
|
4433
|
+
`${again} of them timed out again when they were run one at a time (--concurrency 1).`
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
io2.err(
|
|
4437
|
+
`${retried.missing} of them were not in the report of the run again (--concurrency 1),`
|
|
4438
|
+
);
|
|
4439
|
+
io2.err("so they keep the timeout of the first run. Look at each mutant first. Only for a");
|
|
4440
|
+
}
|
|
3910
4441
|
io2.err("mutant that can never finish (a loop that no longer ends), add it to");
|
|
3911
4442
|
io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
|
|
3912
4443
|
io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
|
|
@@ -3925,16 +4456,16 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3925
4456
|
// src/commands/mutation.ts
|
|
3926
4457
|
var CONFIG_MARK = "\0test-gates-stryker-config\0";
|
|
3927
4458
|
function loadStrykerConfig(manifest, config) {
|
|
3928
|
-
const file =
|
|
4459
|
+
const file = path10.resolve(manifest.dir, config);
|
|
3929
4460
|
if (file.endsWith(".json")) {
|
|
3930
4461
|
try {
|
|
3931
|
-
return JSON.parse(
|
|
4462
|
+
return JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3932
4463
|
} catch {
|
|
3933
4464
|
return void 0;
|
|
3934
4465
|
}
|
|
3935
4466
|
}
|
|
3936
|
-
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,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
3937
|
-
const result =
|
|
4467
|
+
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,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns, cleanTempDir: config.cleanTempDir,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
4468
|
+
const result = spawnSync4(process.execPath, ["--input-type=module", "--eval", script], {
|
|
3938
4469
|
cwd: manifest.dir,
|
|
3939
4470
|
encoding: "utf8",
|
|
3940
4471
|
timeout: 6e4
|
|
@@ -3947,7 +4478,7 @@ function loadStrykerConfig(manifest, config) {
|
|
|
3947
4478
|
}
|
|
3948
4479
|
function modifiedAt(file) {
|
|
3949
4480
|
try {
|
|
3950
|
-
const stat =
|
|
4481
|
+
const stat = fs10.statSync(file, { bigint: true });
|
|
3951
4482
|
return `${stat.mtimeNs} ${stat.ino}`;
|
|
3952
4483
|
} catch {
|
|
3953
4484
|
return null;
|
|
@@ -3956,6 +4487,7 @@ function modifiedAt(file) {
|
|
|
3956
4487
|
function guardRecordPath(incrementalFile) {
|
|
3957
4488
|
return `${incrementalFile}.test-gates.json`;
|
|
3958
4489
|
}
|
|
4490
|
+
var forcedLine = (reason) => `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${reason}`;
|
|
3959
4491
|
function coveringSpecs(report, gates) {
|
|
3960
4492
|
const fileOf = /* @__PURE__ */ new Map();
|
|
3961
4493
|
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
@@ -3982,10 +4514,20 @@ function coveringSpecs(report, gates) {
|
|
|
3982
4514
|
}
|
|
3983
4515
|
return covering;
|
|
3984
4516
|
}
|
|
3985
|
-
function
|
|
4517
|
+
function readSeed(incrementalFile, recordFile) {
|
|
4518
|
+
try {
|
|
4519
|
+
const incremental = fs10.readFileSync(incrementalFile);
|
|
4520
|
+
const record = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
|
|
4521
|
+
const named = record?.incremental;
|
|
4522
|
+
return named === hashOf(incremental) ? { incremental, record } : null;
|
|
4523
|
+
} catch {
|
|
4524
|
+
return null;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
function decideReuse(manifest, config, strykerArgs, part, affected, found = () => void 0) {
|
|
3986
4528
|
const loaded = loadStrykerConfig(manifest, config);
|
|
3987
4529
|
const options = incrementalOptions(loaded, strykerArgs);
|
|
3988
|
-
const base =
|
|
4530
|
+
const base = fs10.realpathSync(manifest.dir);
|
|
3989
4531
|
const checkersArg = strykerArgs.some(
|
|
3990
4532
|
(arg) => arg === "--checkers" || arg.startsWith("--checkers=")
|
|
3991
4533
|
);
|
|
@@ -4009,35 +4551,42 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4009
4551
|
last: void 0,
|
|
4010
4552
|
ci: affected ? affectedRefused(process.env) : null,
|
|
4011
4553
|
unchanged: null,
|
|
4012
|
-
configs
|
|
4554
|
+
configs,
|
|
4555
|
+
temp: {
|
|
4556
|
+
dir: loaded === void 0 ? null : path10.resolve(base, options.tempDirName),
|
|
4557
|
+
keeps: loaded?.cleanTempDir === false
|
|
4558
|
+
},
|
|
4559
|
+
mainWorktree: null,
|
|
4560
|
+
seed: null
|
|
4013
4561
|
};
|
|
4562
|
+
found(decision.temp);
|
|
4014
4563
|
if (loaded !== void 0 && !options.incremental) {
|
|
4015
4564
|
return decision;
|
|
4016
4565
|
}
|
|
4017
4566
|
decision.incremental = true;
|
|
4018
|
-
const incrementalFile =
|
|
4567
|
+
const incrementalFile = path10.resolve(base, options.incrementalFile);
|
|
4019
4568
|
const recordFile = guardRecordPath(incrementalFile);
|
|
4020
4569
|
decision.incrementalFile = incrementalFile;
|
|
4021
4570
|
decision.recordFile = recordFile;
|
|
4022
4571
|
let snapshot = null;
|
|
4023
4572
|
let root = base;
|
|
4024
4573
|
let failure = "the Stryker config could not be loaded to see where the incremental file is";
|
|
4025
|
-
const reports = reportFiles(loaded).map((file) =>
|
|
4574
|
+
const reports = reportFiles(loaded).map((file) => path10.resolve(base, file));
|
|
4026
4575
|
const ignores = strykerIgnores(loaded);
|
|
4027
4576
|
const read2 = (file) => {
|
|
4028
|
-
const relative =
|
|
4029
|
-
const inside = !relative.startsWith("..") && !
|
|
4030
|
-
return !reports.includes(file) && !(inside && ignores(relative.split(
|
|
4577
|
+
const relative = path10.relative(base, file);
|
|
4578
|
+
const inside = !relative.startsWith("..") && !path10.isAbsolute(relative);
|
|
4579
|
+
return !reports.includes(file) && !(inside && ignores(relative.split(path10.sep).join("/")));
|
|
4031
4580
|
};
|
|
4032
4581
|
if (loaded !== void 0) {
|
|
4033
4582
|
const scope = (listed2) => {
|
|
4034
4583
|
try {
|
|
4035
|
-
const
|
|
4036
|
-
if (!("files" in
|
|
4037
|
-
decision.scope =
|
|
4584
|
+
const found2 = loadedScope(manifest, configs(), listed2);
|
|
4585
|
+
if (!("files" in found2)) {
|
|
4586
|
+
decision.scope = found2;
|
|
4038
4587
|
return null;
|
|
4039
4588
|
}
|
|
4040
|
-
const files = new Set([...
|
|
4589
|
+
const files = new Set([...found2.files].filter(read2));
|
|
4041
4590
|
decision.scope = { files: files.size };
|
|
4042
4591
|
return files;
|
|
4043
4592
|
} catch (error) {
|
|
@@ -4053,8 +4602,8 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4053
4602
|
written: [
|
|
4054
4603
|
incrementalFile,
|
|
4055
4604
|
recordFile,
|
|
4056
|
-
|
|
4057
|
-
|
|
4605
|
+
path10.resolve(base, options.tempDirName),
|
|
4606
|
+
path10.resolve(base, manifest.settings.strykerReportFile),
|
|
4058
4607
|
...reports
|
|
4059
4608
|
],
|
|
4060
4609
|
args: comparableArgs(strykerArgs)
|
|
@@ -4067,17 +4616,32 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4067
4616
|
decision.snapshot = snapshot;
|
|
4068
4617
|
decision.failure = snapshot === null ? failure : null;
|
|
4069
4618
|
decision.root = root;
|
|
4070
|
-
const
|
|
4619
|
+
const tempDir = path10.basename(options.tempDirName);
|
|
4620
|
+
if (!part && !options.force && snapshot?.mode === "git" && !path10.relative(root, base).split(path10.sep).includes(tempDir) && !fs10.existsSync(incrementalFile) && !fs10.existsSync(recordFile)) {
|
|
4621
|
+
const linked = linkedWorktreeOf(base);
|
|
4622
|
+
const mainIncremental = linked === null ? null : inMainWorktree(linked, incrementalFile);
|
|
4623
|
+
const mainRecord = linked === null ? null : inMainWorktree(linked, recordFile);
|
|
4624
|
+
if (linked !== null && mainIncremental !== null && mainRecord !== null) {
|
|
4625
|
+
decision.mainWorktree = linked.main;
|
|
4626
|
+
const seed = readSeed(mainIncremental, mainRecord);
|
|
4627
|
+
if (seed !== null) {
|
|
4628
|
+
decision.seed = seed;
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
const incremental = decision.seed === null ? hashFile(incrementalFile) : hashOf(decision.seed.incremental);
|
|
4071
4633
|
decision.incrementalHash = incremental;
|
|
4072
4634
|
if (part) {
|
|
4073
4635
|
decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
|
|
4074
4636
|
return decision;
|
|
4075
4637
|
}
|
|
4076
|
-
let stored;
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4638
|
+
let stored = decision.seed?.record;
|
|
4639
|
+
if (decision.seed === null) {
|
|
4640
|
+
try {
|
|
4641
|
+
stored = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
|
|
4642
|
+
} catch {
|
|
4643
|
+
stored = void 0;
|
|
4644
|
+
}
|
|
4081
4645
|
}
|
|
4082
4646
|
const ci = decision.ci;
|
|
4083
4647
|
let force = null;
|
|
@@ -4091,6 +4655,9 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4091
4655
|
if (force === null) {
|
|
4092
4656
|
const read3 = readRecord({ record: stored, incremental, snapshot });
|
|
4093
4657
|
last = "record" in read3 ? read3.record.affected : void 0;
|
|
4658
|
+
if (decision.seed !== null) {
|
|
4659
|
+
force = blindInMainRecord(manifest, { root, snapshot, configs, last });
|
|
4660
|
+
}
|
|
4094
4661
|
}
|
|
4095
4662
|
} else if (ci !== null) {
|
|
4096
4663
|
force = ci;
|
|
@@ -4117,8 +4684,10 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4117
4684
|
config: inputs.config,
|
|
4118
4685
|
gates: manifest.gates.map((gate) => ({
|
|
4119
4686
|
path: gate.path,
|
|
4120
|
-
files: inputs.gates[gate.path]?.files ?? []
|
|
4121
|
-
|
|
4687
|
+
files: inputs.gates[gate.path]?.files ?? [],
|
|
4688
|
+
blind: inputs.gates[gate.path]?.blind
|
|
4689
|
+
})),
|
|
4690
|
+
seeded: decision.seed !== null
|
|
4122
4691
|
});
|
|
4123
4692
|
} catch (error) {
|
|
4124
4693
|
if (!(error instanceof AffectedError)) {
|
|
@@ -4127,7 +4696,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4127
4696
|
plan = { full: `what a gate depends on cannot be told: ${error.message}` };
|
|
4128
4697
|
}
|
|
4129
4698
|
if ("full" in plan) {
|
|
4130
|
-
if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
4699
|
+
if (decision.seed === null && refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
4131
4700
|
decision.unchanged = plan.full;
|
|
4132
4701
|
plan = null;
|
|
4133
4702
|
} else {
|
|
@@ -4144,9 +4713,45 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
4144
4713
|
decision.last = last;
|
|
4145
4714
|
return decision;
|
|
4146
4715
|
}
|
|
4147
|
-
function
|
|
4148
|
-
const
|
|
4149
|
-
const
|
|
4716
|
+
function blindInMainRecord(manifest, input) {
|
|
4717
|
+
const { last } = input;
|
|
4718
|
+
const unknown = last === void 0 ? "each gate" : manifest.gates.find((gate) => !Object.hasOwn(last.gates, gate.path))?.path;
|
|
4719
|
+
if (last === void 0 || unknown !== void 0) {
|
|
4720
|
+
return `the record of the main worktree does not say what ${unknown} depends on, so it may read files git does not list, which differ between worktrees`;
|
|
4721
|
+
}
|
|
4722
|
+
try {
|
|
4723
|
+
const inputs = affectedInputs(manifest, {
|
|
4724
|
+
root: input.root,
|
|
4725
|
+
files: input.snapshot.files,
|
|
4726
|
+
configs: input.configs(),
|
|
4727
|
+
tests: last.tests,
|
|
4728
|
+
covering: Object.fromEntries(
|
|
4729
|
+
Object.entries(last.gates).map(([gate, entry]) => [gate, entry.specs])
|
|
4730
|
+
)
|
|
4731
|
+
});
|
|
4732
|
+
for (const gate of manifest.gates) {
|
|
4733
|
+
const blind = inputs.gates[gate.path]?.blind ?? last.gates[gate.path]?.blind;
|
|
4734
|
+
if (blind !== void 0) {
|
|
4735
|
+
return `${gate.path} may read files git does not list, which differ between worktrees (${blind})`;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
return null;
|
|
4739
|
+
} catch (error) {
|
|
4740
|
+
if (!(error instanceof AffectedError)) {
|
|
4741
|
+
throw error;
|
|
4742
|
+
}
|
|
4743
|
+
return `what a gate depends on cannot be told, so it may read files git does not list, which differ between worktrees: ${error.message}`;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
function guardIncremental(manifest, config, strykerArgs, part, affected, found) {
|
|
4747
|
+
const decision = decideReuse(manifest, config, strykerArgs, part, affected, found);
|
|
4748
|
+
const nothing = {
|
|
4749
|
+
force: null,
|
|
4750
|
+
mutate: null,
|
|
4751
|
+
said: [],
|
|
4752
|
+
temp: decision.temp,
|
|
4753
|
+
record: () => void 0
|
|
4754
|
+
};
|
|
4150
4755
|
if (!decision.incremental) {
|
|
4151
4756
|
return affected ? {
|
|
4152
4757
|
...nothing,
|
|
@@ -4161,10 +4766,14 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4161
4766
|
const incremental = decision.incrementalHash;
|
|
4162
4767
|
const options = { force: decision.forced };
|
|
4163
4768
|
if (part) {
|
|
4164
|
-
|
|
4769
|
+
fs10.rmSync(recordFile, { force: true });
|
|
4165
4770
|
return { ...nothing, force };
|
|
4166
4771
|
}
|
|
4167
|
-
|
|
4772
|
+
if (decision.seed !== null) {
|
|
4773
|
+
fs10.mkdirSync(path10.dirname(incrementalFile), { recursive: true });
|
|
4774
|
+
fs10.writeFileSync(incrementalFile, decision.seed.incremental);
|
|
4775
|
+
}
|
|
4776
|
+
fs10.rmSync(recordFile, { force: true });
|
|
4168
4777
|
const before = modifiedAt(incrementalFile);
|
|
4169
4778
|
const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
|
|
4170
4779
|
const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
|
|
@@ -4179,7 +4788,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4179
4788
|
rerun: timedOut,
|
|
4180
4789
|
expected
|
|
4181
4790
|
} = timeoutRanges(
|
|
4182
|
-
JSON.parse(
|
|
4791
|
+
JSON.parse(fs10.readFileSync(incrementalFile, "utf8")),
|
|
4183
4792
|
kept
|
|
4184
4793
|
));
|
|
4185
4794
|
} catch {
|
|
@@ -4210,11 +4819,20 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4210
4819
|
said.push(`test-gates mutation: ${describeExpected(expected)}`);
|
|
4211
4820
|
}
|
|
4212
4821
|
}
|
|
4822
|
+
if (decision.seed !== null) {
|
|
4823
|
+
if (said.length === 0 && everyGate !== null) {
|
|
4824
|
+
said.push(forcedLine(everyGate));
|
|
4825
|
+
}
|
|
4826
|
+
said.unshift(
|
|
4827
|
+
`test-gates mutation: this worktree has no record of its own, so it starts from the one of the main worktree (${decision.mainWorktree})`
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4213
4830
|
return {
|
|
4214
4831
|
// Every gate is run again: the same as a run of every mutant.
|
|
4215
4832
|
force: everyGate,
|
|
4216
4833
|
mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
|
|
4217
4834
|
said,
|
|
4835
|
+
temp: decision.temp,
|
|
4218
4836
|
record: (judged) => {
|
|
4219
4837
|
const written = hashFile(incrementalFile);
|
|
4220
4838
|
if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
|
|
@@ -4225,7 +4843,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4225
4843
|
record.affected = affectedRecord(judged, ran, last);
|
|
4226
4844
|
}
|
|
4227
4845
|
try {
|
|
4228
|
-
|
|
4846
|
+
fs10.writeFileSync(recordFile, `${JSON.stringify(record)}
|
|
4229
4847
|
`);
|
|
4230
4848
|
} catch {
|
|
4231
4849
|
}
|
|
@@ -4256,7 +4874,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4256
4874
|
for (const gate of Object.keys(covering)) {
|
|
4257
4875
|
const entry = inputs.gates[gate];
|
|
4258
4876
|
if (entry !== void 0) {
|
|
4259
|
-
recorded[gate] = {
|
|
4877
|
+
recorded[gate] = {
|
|
4878
|
+
specs: entry.specs,
|
|
4879
|
+
files: entry.files,
|
|
4880
|
+
...entry.blind === void 0 ? {} : { blind: entry.blind }
|
|
4881
|
+
};
|
|
4260
4882
|
}
|
|
4261
4883
|
}
|
|
4262
4884
|
return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
|
|
@@ -4268,11 +4890,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
4268
4890
|
}
|
|
4269
4891
|
}
|
|
4270
4892
|
}
|
|
4271
|
-
function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
4893
|
+
function runMutation(dir, strykerArgs, io2, only = [], affected = false, shard = null) {
|
|
4272
4894
|
const part = only.length > 0;
|
|
4273
|
-
if (affected && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
4895
|
+
if ((affected || shard !== null) && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
4274
4896
|
throw new UsageError(
|
|
4275
|
-
"--affected and --mutate cannot be combined: --affected sets --mutate itself
|
|
4897
|
+
`${affected ? "--affected" : "--shard"} and --mutate cannot be combined: ${affected ? "--affected" : "--shard"} sets --mutate itself`
|
|
4276
4898
|
);
|
|
4277
4899
|
}
|
|
4278
4900
|
if (part) {
|
|
@@ -4289,11 +4911,12 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
|
4289
4911
|
gates: manifest.gates.map((gate) => gate.path),
|
|
4290
4912
|
mode: "all",
|
|
4291
4913
|
only,
|
|
4292
|
-
shard
|
|
4914
|
+
shard
|
|
4293
4915
|
});
|
|
4294
4916
|
if ("error" in chosen) {
|
|
4295
4917
|
throw new UsageError(chosen.error);
|
|
4296
4918
|
}
|
|
4919
|
+
const shardGates = shard === null ? null : chosen.selected;
|
|
4297
4920
|
const config = findStrykerConfig(manifest);
|
|
4298
4921
|
if (config === null) {
|
|
4299
4922
|
throw new ManifestError(
|
|
@@ -4307,53 +4930,122 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
|
4307
4930
|
);
|
|
4308
4931
|
}
|
|
4309
4932
|
const reportFile = reportPathOf(manifest, null);
|
|
4310
|
-
|
|
4311
|
-
|
|
4933
|
+
fs10.rmSync(reportFile, { force: true });
|
|
4934
|
+
let removed = [];
|
|
4935
|
+
const guard = guardIncremental(manifest, config, strykerArgs, part, affected, (temp) => {
|
|
4936
|
+
removed = removeStaleSandboxes(manifest.dir, temp);
|
|
4937
|
+
});
|
|
4312
4938
|
if (guard.said.length > 0) {
|
|
4313
4939
|
guard.said.forEach((line) => io2.out(line));
|
|
4314
4940
|
} else if (guard.force !== null) {
|
|
4315
|
-
io2.out(
|
|
4316
|
-
`test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
|
|
4317
|
-
);
|
|
4941
|
+
io2.out(forcedLine(guard.force));
|
|
4318
4942
|
}
|
|
4943
|
+
removed.forEach((line) => io2.out(line));
|
|
4319
4944
|
const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
|
|
4320
|
-
const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
|
|
4321
|
-
const
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4945
|
+
const partArgs = part || shardGates !== null ? ["--mutate", (shardGates ?? chosen.selected).join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
|
|
4946
|
+
const judged = part ? chosen.selected : shardGates ?? [];
|
|
4947
|
+
const unmark = markRun(manifest.dir, guard.temp);
|
|
4948
|
+
let retried;
|
|
4949
|
+
try {
|
|
4950
|
+
const result = spawnSync4(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
|
|
4951
|
+
cwd: manifest.dir,
|
|
4952
|
+
stdio: "inherit",
|
|
4953
|
+
shell: process.platform === "win32"
|
|
4954
|
+
});
|
|
4955
|
+
if (result.error) {
|
|
4956
|
+
io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
|
|
4957
|
+
return 1;
|
|
4958
|
+
}
|
|
4959
|
+
if (result.status !== 0) {
|
|
4960
|
+
io2.err(
|
|
4961
|
+
`test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
|
|
4962
|
+
);
|
|
4963
|
+
return 1;
|
|
4964
|
+
}
|
|
4965
|
+
retried = retryTimeouts(
|
|
4966
|
+
stryker,
|
|
4967
|
+
[config, ...strykerArgs],
|
|
4968
|
+
manifest,
|
|
4969
|
+
reportFile,
|
|
4970
|
+
judged.length > 0 ? manifest.gates.filter((gate) => judged.includes(gate.path)) : manifest.gates,
|
|
4971
|
+
io2
|
|
4333
4972
|
);
|
|
4334
|
-
|
|
4973
|
+
} finally {
|
|
4974
|
+
unmark();
|
|
4335
4975
|
}
|
|
4336
|
-
let
|
|
4337
|
-
const status = runMutationResult(
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4976
|
+
let lastSeen = null;
|
|
4977
|
+
const status = runMutationResult(
|
|
4978
|
+
dir,
|
|
4979
|
+
null,
|
|
4980
|
+
io2,
|
|
4981
|
+
judged,
|
|
4982
|
+
(seen) => {
|
|
4983
|
+
lastSeen = seen;
|
|
4984
|
+
},
|
|
4985
|
+
retried,
|
|
4986
|
+
shard
|
|
4987
|
+
);
|
|
4988
|
+
guard.record(lastSeen);
|
|
4341
4989
|
if (part) {
|
|
4342
|
-
|
|
4990
|
+
fs10.rmSync(reportFile, { force: true });
|
|
4343
4991
|
}
|
|
4344
4992
|
return status;
|
|
4345
4993
|
}
|
|
4994
|
+
function retryTimeouts(stryker, args, manifest, reportFile, gates, io2) {
|
|
4995
|
+
let first;
|
|
4996
|
+
try {
|
|
4997
|
+
first = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
|
|
4998
|
+
} catch {
|
|
4999
|
+
return null;
|
|
5000
|
+
}
|
|
5001
|
+
if (typeof first !== "object" || first === null) {
|
|
5002
|
+
return null;
|
|
5003
|
+
}
|
|
5004
|
+
const { ranges, rerun } = timeoutRanges(first, gates);
|
|
5005
|
+
if (ranges.length === 0 || rerun <= manifest.settings.maxTimeouts) {
|
|
5006
|
+
return null;
|
|
5007
|
+
}
|
|
5008
|
+
io2.out(
|
|
5009
|
+
`test-gates mutation: ${rerun} mutant(s) timed out outside expectedTimeouts, running them again one at a time (--concurrency 1)`
|
|
5010
|
+
);
|
|
5011
|
+
fs10.rmSync(reportFile, { force: true });
|
|
5012
|
+
const result = spawnSync4(
|
|
5013
|
+
stryker,
|
|
5014
|
+
["run", ...args, "--mutate", ranges.join(","), "--concurrency", "1", "--force"],
|
|
5015
|
+
{ cwd: manifest.dir, stdio: "inherit", shell: process.platform === "win32" }
|
|
5016
|
+
);
|
|
5017
|
+
let retry = null;
|
|
5018
|
+
if (!result.error && result.status === 0) {
|
|
5019
|
+
try {
|
|
5020
|
+
retry = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
|
|
5021
|
+
} catch {
|
|
5022
|
+
retry = null;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
if (retry === null || typeof retry !== "object") {
|
|
5026
|
+
const why = result.error ? result.error.message : result.signal ? `signal ${result.signal}` : result.status === 0 ? "no report" : `exit ${result.status}`;
|
|
5027
|
+
io2.err(
|
|
5028
|
+
`test-gates mutation: running the timed-out mutants again failed (${why}). The first run is judged`
|
|
5029
|
+
);
|
|
5030
|
+
fs10.writeFileSync(reportFile, JSON.stringify(first));
|
|
5031
|
+
return "failed";
|
|
5032
|
+
}
|
|
5033
|
+
const merged = mergeRetry(first, retry, gates);
|
|
5034
|
+
fs10.writeFileSync(reportFile, JSON.stringify(merged.report));
|
|
5035
|
+
io2.out(formatRetry(merged.outcome));
|
|
5036
|
+
return merged.outcome;
|
|
5037
|
+
}
|
|
4346
5038
|
|
|
4347
5039
|
// src/commands/doctor.ts
|
|
4348
5040
|
function read(file) {
|
|
4349
5041
|
try {
|
|
4350
|
-
return
|
|
5042
|
+
return fs11.readFileSync(file, "utf8");
|
|
4351
5043
|
} catch {
|
|
4352
5044
|
return null;
|
|
4353
5045
|
}
|
|
4354
5046
|
}
|
|
4355
|
-
function
|
|
4356
|
-
const result =
|
|
5047
|
+
function git3(dir, args) {
|
|
5048
|
+
const result = spawnSync5("git", args, {
|
|
4357
5049
|
cwd: dir,
|
|
4358
5050
|
env: gitEnvironment(process.env),
|
|
4359
5051
|
encoding: "utf8"
|
|
@@ -4361,20 +5053,20 @@ function git2(dir, args) {
|
|
|
4361
5053
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
4362
5054
|
}
|
|
4363
5055
|
function hooksOf(dir) {
|
|
4364
|
-
const root =
|
|
4365
|
-
const hooksDir =
|
|
5056
|
+
const root = git3(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
|
|
5057
|
+
const hooksDir = git3(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
|
|
4366
5058
|
const candidates = [
|
|
4367
|
-
...["pre-push", "pre-commit"].map((name) =>
|
|
4368
|
-
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) =>
|
|
4369
|
-
...["lefthook.yml", ".lefthook.yml"].map((name) =>
|
|
5059
|
+
...["pre-push", "pre-commit"].map((name) => path11.join(root, ".husky", name)),
|
|
5060
|
+
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path11.join(hooksDir, name)),
|
|
5061
|
+
...["lefthook.yml", ".lefthook.yml"].map((name) => path11.join(root, name))
|
|
4370
5062
|
];
|
|
4371
5063
|
let script = "";
|
|
4372
5064
|
try {
|
|
4373
|
-
const manifest = JSON.parse(read(
|
|
5065
|
+
const manifest = JSON.parse(read(path11.join(dir, "package.json")) ?? "{}");
|
|
4374
5066
|
script = manifest.scripts?.["test:gates:mutation"] ?? "";
|
|
4375
5067
|
} catch {
|
|
4376
5068
|
}
|
|
4377
|
-
const subproject =
|
|
5069
|
+
const subproject = path11.relative(fs11.realpathSync(root), dir).split(path11.sep).join("/");
|
|
4378
5070
|
const seen = /* @__PURE__ */ new Set();
|
|
4379
5071
|
const uses = [];
|
|
4380
5072
|
for (const file of candidates) {
|
|
@@ -4382,12 +5074,12 @@ function hooksOf(dir) {
|
|
|
4382
5074
|
if (text === null) {
|
|
4383
5075
|
continue;
|
|
4384
5076
|
}
|
|
4385
|
-
const real =
|
|
5077
|
+
const real = fs11.realpathSync(file);
|
|
4386
5078
|
if (seen.has(real)) {
|
|
4387
5079
|
continue;
|
|
4388
5080
|
}
|
|
4389
5081
|
seen.add(real);
|
|
4390
|
-
const shown =
|
|
5082
|
+
const shown = path11.relative(root, file).split(path11.sep).join("/");
|
|
4391
5083
|
uses.push(...hookUses({ file: shown, text, subproject, script }));
|
|
4392
5084
|
}
|
|
4393
5085
|
return uses;
|
|
@@ -4404,8 +5096,8 @@ function runDoctor(dir, io2) {
|
|
|
4404
5096
|
const affected = decideReuse(manifest, config, [], false, true);
|
|
4405
5097
|
io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
|
|
4406
5098
|
if (plain2.snapshot !== null) {
|
|
4407
|
-
const subproject =
|
|
4408
|
-
const prefix = subproject === "" ? "" : `${subproject.split(
|
|
5099
|
+
const subproject = path11.relative(plain2.root, fs11.realpathSync(manifest.dir));
|
|
5100
|
+
const prefix = subproject === "" ? "" : `${subproject.split(path11.sep).join("/")}/`;
|
|
4409
5101
|
const keys = Object.keys(plain2.snapshot.files);
|
|
4410
5102
|
const outside = keys.filter((key) => !key.startsWith(prefix)).length;
|
|
4411
5103
|
const followed = plain2.scope === null ? "" : "files" in plain2.scope ? `, paths followed in the ${plain2.scope.files} files the run loads` : ", paths followed in every file";
|
|
@@ -4416,6 +5108,15 @@ function runDoctor(dir, io2) {
|
|
|
4416
5108
|
io2.out(
|
|
4417
5109
|
reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
|
|
4418
5110
|
);
|
|
5111
|
+
if (plain2.seed !== null) {
|
|
5112
|
+
io2.out(
|
|
5113
|
+
` this worktree has no record of its own: the next run starts from the one of the main worktree (${plain2.mainWorktree})`
|
|
5114
|
+
);
|
|
5115
|
+
} else if (plain2.mainWorktree !== null) {
|
|
5116
|
+
io2.out(
|
|
5117
|
+
` this worktree has no record of its own, and the main worktree (${plain2.mainWorktree}) has none this worktree can start from`
|
|
5118
|
+
);
|
|
5119
|
+
}
|
|
4419
5120
|
if (affected.ci !== null) {
|
|
4420
5121
|
io2.out(` next run with --affected: every mutant, ${affected.ci}`);
|
|
4421
5122
|
} else if (affected.plan !== null && "rerun" in affected.plan) {
|
|
@@ -4432,15 +5133,20 @@ function runDoctor(dir, io2) {
|
|
|
4432
5133
|
}
|
|
4433
5134
|
}
|
|
4434
5135
|
let blind = null;
|
|
5136
|
+
let blindGates = [];
|
|
4435
5137
|
if (plain2.snapshot !== null) {
|
|
4436
5138
|
try {
|
|
4437
|
-
affectedInputs(manifest, {
|
|
5139
|
+
const inputs = affectedInputs(manifest, {
|
|
4438
5140
|
root: plain2.root,
|
|
4439
5141
|
files: plain2.snapshot.files,
|
|
4440
5142
|
configs: plain2.configs(),
|
|
4441
5143
|
tests: [],
|
|
4442
5144
|
covering: {}
|
|
4443
5145
|
});
|
|
5146
|
+
blindGates = manifest.gates.flatMap((gate) => {
|
|
5147
|
+
const reason = inputs.gates[gate.path]?.blind;
|
|
5148
|
+
return reason === void 0 ? [] : [{ gate: gate.path, reason }];
|
|
5149
|
+
});
|
|
4444
5150
|
} catch (error) {
|
|
4445
5151
|
if (!(error instanceof AffectedError)) {
|
|
4446
5152
|
throw error;
|
|
@@ -4448,6 +5154,8 @@ function runDoctor(dir, io2) {
|
|
|
4448
5154
|
blind = error.message;
|
|
4449
5155
|
}
|
|
4450
5156
|
}
|
|
5157
|
+
const left = sandboxPlan(manifest.dir, plain2.temp);
|
|
5158
|
+
const count = "kept" in left.plan ? left.plan.kept.length : left.plan.remove.length;
|
|
4451
5159
|
const { lines, status } = formatFindings(
|
|
4452
5160
|
diagnose({
|
|
4453
5161
|
incremental: plain2.incremental,
|
|
@@ -4455,8 +5163,16 @@ function runDoctor(dir, io2) {
|
|
|
4455
5163
|
failure: plain2.failure,
|
|
4456
5164
|
widened: plain2.widened,
|
|
4457
5165
|
scope: plain2.scope,
|
|
4458
|
-
hooks: hooksOf(
|
|
4459
|
-
blind
|
|
5166
|
+
hooks: hooksOf(fs11.realpathSync(manifest.dir)),
|
|
5167
|
+
blind,
|
|
5168
|
+
blindGates,
|
|
5169
|
+
gates: manifest.gates.length,
|
|
5170
|
+
sandboxes: count === 0 ? null : {
|
|
5171
|
+
tempDir: left.shown,
|
|
5172
|
+
count,
|
|
5173
|
+
kept: "kept" in left.plan ? left.plan.why : null,
|
|
5174
|
+
running: "kept" in left.plan ? left.plan.running : null
|
|
5175
|
+
}
|
|
4460
5176
|
})
|
|
4461
5177
|
);
|
|
4462
5178
|
lines.forEach((line) => io2.out(line));
|
|
@@ -4464,8 +5180,8 @@ function runDoctor(dir, io2) {
|
|
|
4464
5180
|
}
|
|
4465
5181
|
|
|
4466
5182
|
// src/commands/lcov.ts
|
|
4467
|
-
import
|
|
4468
|
-
import
|
|
5183
|
+
import fs12 from "fs";
|
|
5184
|
+
import path12 from "path";
|
|
4469
5185
|
|
|
4470
5186
|
// src/core/lcov.ts
|
|
4471
5187
|
function normalizePath2(file, rootDir) {
|
|
@@ -4492,9 +5208,9 @@ function parseLcov(text, rootDir) {
|
|
|
4492
5208
|
const line = rawLine.trim();
|
|
4493
5209
|
if (line.startsWith("SF:")) {
|
|
4494
5210
|
closeRecord();
|
|
4495
|
-
const
|
|
4496
|
-
const file = files.get(
|
|
4497
|
-
files.set(
|
|
5211
|
+
const path15 = normalizePath2(line.slice(3), rootDir);
|
|
5212
|
+
const file = files.get(path15) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
5213
|
+
files.set(path15, file);
|
|
4498
5214
|
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
4499
5215
|
} else if (line === "end_of_record") {
|
|
4500
5216
|
closeRecord();
|
|
@@ -4578,10 +5294,10 @@ function runLcov(dir, fileOption, io2) {
|
|
|
4578
5294
|
"no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
|
|
4579
5295
|
);
|
|
4580
5296
|
}
|
|
4581
|
-
const lcovPath =
|
|
5297
|
+
const lcovPath = path12.resolve(manifest.dir, file);
|
|
4582
5298
|
let text;
|
|
4583
5299
|
try {
|
|
4584
|
-
text =
|
|
5300
|
+
text = fs12.readFileSync(lcovPath, "utf8");
|
|
4585
5301
|
} catch (error) {
|
|
4586
5302
|
io2.err(
|
|
4587
5303
|
`test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
|
|
@@ -4624,10 +5340,10 @@ function runLcov(dir, fileOption, io2) {
|
|
|
4624
5340
|
}
|
|
4625
5341
|
|
|
4626
5342
|
// src/commands/selfcheck.ts
|
|
4627
|
-
import { spawn, spawnSync as
|
|
4628
|
-
import
|
|
5343
|
+
import { spawn, spawnSync as spawnSync6 } from "child_process";
|
|
5344
|
+
import fs14 from "fs";
|
|
4629
5345
|
import os2 from "os";
|
|
4630
|
-
import
|
|
5346
|
+
import path14 from "path";
|
|
4631
5347
|
|
|
4632
5348
|
// src/core/jobs.ts
|
|
4633
5349
|
var SWITCHES = {
|
|
@@ -4667,6 +5383,34 @@ function knownCommand(program, args) {
|
|
|
4667
5383
|
function jobFlags(runner, dir) {
|
|
4668
5384
|
return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
|
|
4669
5385
|
}
|
|
5386
|
+
var LIMITS = {
|
|
5387
|
+
jest: ["--runInBand", "-i", "--maxWorkers", "-w"],
|
|
5388
|
+
vitest: ["--no-file-parallelism", "--fileParallelism", "--maxWorkers", "--minWorkers"]
|
|
5389
|
+
};
|
|
5390
|
+
var CONFIG_LIMITS = [
|
|
5391
|
+
"maxWorkers",
|
|
5392
|
+
"minWorkers",
|
|
5393
|
+
"maxThreads",
|
|
5394
|
+
"minThreads",
|
|
5395
|
+
"maxForks",
|
|
5396
|
+
"minForks",
|
|
5397
|
+
"fileParallelism",
|
|
5398
|
+
"singleThread",
|
|
5399
|
+
"singleFork"
|
|
5400
|
+
];
|
|
5401
|
+
function workerFlags(input) {
|
|
5402
|
+
if (input.lines <= 1) {
|
|
5403
|
+
return [];
|
|
5404
|
+
}
|
|
5405
|
+
const given = input.args.some(
|
|
5406
|
+
(arg) => LIMITS[input.runner].includes(arg.split("=")[0])
|
|
5407
|
+
);
|
|
5408
|
+
const configured = CONFIG_LIMITS.some((word) => input.configSource?.includes(word) === true);
|
|
5409
|
+
if (given || configured) {
|
|
5410
|
+
return [];
|
|
5411
|
+
}
|
|
5412
|
+
return [`--maxWorkers=${Math.max(1, Math.floor(input.cores / input.lines))}`];
|
|
5413
|
+
}
|
|
4670
5414
|
var REACHES = [
|
|
4671
5415
|
"process.",
|
|
4672
5416
|
"process?.",
|
|
@@ -4690,6 +5434,11 @@ var REACHES = [
|
|
|
4690
5434
|
"WebSocket",
|
|
4691
5435
|
"XMLHttpRequest",
|
|
4692
5436
|
"navigator",
|
|
5437
|
+
// What crypto keeps as a setting of its module, for every spec file of a worker after it.
|
|
5438
|
+
"setFips",
|
|
5439
|
+
"setEngine",
|
|
5440
|
+
"setDefaultEncoding",
|
|
5441
|
+
"DEFAULT_ENCODING",
|
|
4693
5442
|
"Atomics",
|
|
4694
5443
|
"WeakRef",
|
|
4695
5444
|
"FinalizationRegistry",
|
|
@@ -4698,10 +5447,35 @@ var REACHES = [
|
|
|
4698
5447
|
];
|
|
4699
5448
|
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
|
|
4700
5449
|
var COMPUTED = "loads a module whose name is computed";
|
|
5450
|
+
var STRING = /(^|[:,([={?+&|;])(\s*)(['"])((?:\\.|(?!\3)[^\\\n])*)\3(?=\s*(?:[,}\]);:+?&|]|$))/gm;
|
|
5451
|
+
function withoutStrings(source) {
|
|
5452
|
+
return source.replace(
|
|
5453
|
+
STRING,
|
|
5454
|
+
(all, before, space, quote, inside) => (
|
|
5455
|
+
// In a template (`{"home": "${home()}"}`) the quotes are text, and what is
|
|
5456
|
+
// between them may be code.
|
|
5457
|
+
inside.includes("${") ? all : `${before}${space}${quote}${" ".repeat(inside.length)}${quote}`
|
|
5458
|
+
)
|
|
5459
|
+
);
|
|
5460
|
+
}
|
|
5461
|
+
var PURE_BUILTINS = [
|
|
5462
|
+
"assert",
|
|
5463
|
+
"assert/strict",
|
|
5464
|
+
"crypto",
|
|
5465
|
+
"path",
|
|
5466
|
+
"path/posix",
|
|
5467
|
+
"path/win32",
|
|
5468
|
+
"punycode",
|
|
5469
|
+
"querystring",
|
|
5470
|
+
"string_decoder",
|
|
5471
|
+
"url",
|
|
5472
|
+
"util/types"
|
|
5473
|
+
];
|
|
4701
5474
|
var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
|
|
4702
5475
|
function reachesOutside(file, trusted) {
|
|
4703
5476
|
const dense = file.source.replace(/\s/g, "");
|
|
4704
|
-
const
|
|
5477
|
+
const code = withoutStrings(file.source).replace(/\s/g, "");
|
|
5478
|
+
const mark = REACHES.find((text) => code.includes(text));
|
|
4705
5479
|
if (mark !== void 0) {
|
|
4706
5480
|
return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
|
|
4707
5481
|
}
|
|
@@ -4710,7 +5484,9 @@ function reachesOutside(file, trusted) {
|
|
|
4710
5484
|
}
|
|
4711
5485
|
for (const { specifier, builtin } of file.imports) {
|
|
4712
5486
|
if (builtin) {
|
|
4713
|
-
|
|
5487
|
+
if (!PURE_BUILTINS.includes(specifier.replace(/^node:/, ""))) {
|
|
5488
|
+
return `imports ${specifier}`;
|
|
5489
|
+
}
|
|
4714
5490
|
} else if (![...RUNNER_PACKAGES, ...trusted].some(
|
|
4715
5491
|
(name) => specifier === name || specifier.startsWith(`${name}/`)
|
|
4716
5492
|
)) {
|
|
@@ -4844,8 +5620,8 @@ function cleanHold(input) {
|
|
|
4844
5620
|
|
|
4845
5621
|
// src/touch.ts
|
|
4846
5622
|
import { createHash as createHash2 } from "crypto";
|
|
4847
|
-
import
|
|
4848
|
-
import
|
|
5623
|
+
import fs13 from "fs";
|
|
5624
|
+
import path13 from "path";
|
|
4849
5625
|
import { fileURLToPath } from "url";
|
|
4850
5626
|
var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
|
|
4851
5627
|
function isObject(value) {
|
|
@@ -4879,7 +5655,7 @@ function isCounters(value) {
|
|
|
4879
5655
|
function readNotes(directory) {
|
|
4880
5656
|
const read2 = (file) => {
|
|
4881
5657
|
try {
|
|
4882
|
-
return JSON.parse(
|
|
5658
|
+
return JSON.parse(fs13.readFileSync(path13.join(directory, file), "utf8"));
|
|
4883
5659
|
} catch {
|
|
4884
5660
|
return null;
|
|
4885
5661
|
}
|
|
@@ -4887,8 +5663,8 @@ function readNotes(directory) {
|
|
|
4887
5663
|
const run = read2("run.json");
|
|
4888
5664
|
let notes = [];
|
|
4889
5665
|
try {
|
|
4890
|
-
for (const name of
|
|
4891
|
-
const note = read2(
|
|
5666
|
+
for (const name of fs13.readdirSync(path13.join(directory, "specs"))) {
|
|
5667
|
+
const note = read2(path13.join("specs", name));
|
|
4892
5668
|
if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
|
|
4893
5669
|
throw new Error("not a note");
|
|
4894
5670
|
}
|
|
@@ -4900,7 +5676,7 @@ function readNotes(directory) {
|
|
|
4900
5676
|
return {
|
|
4901
5677
|
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,
|
|
4902
5678
|
notes,
|
|
4903
|
-
totals: countersOfAll(read2(
|
|
5679
|
+
totals: countersOfAll(read2(path13.join("coverage", "coverage-final.json")))
|
|
4904
5680
|
};
|
|
4905
5681
|
}
|
|
4906
5682
|
|
|
@@ -4915,7 +5691,7 @@ function resolveGateCommand(manifest) {
|
|
|
4915
5691
|
if (manifest.settings.gateCommand !== null) {
|
|
4916
5692
|
return manifest.settings.gateCommand;
|
|
4917
5693
|
}
|
|
4918
|
-
const exists = (name) =>
|
|
5694
|
+
const exists = (name) => fs14.existsSync(path14.join(manifest.dir, name));
|
|
4919
5695
|
const jest = JEST_CONFIGS.find(exists);
|
|
4920
5696
|
const vitest = VITEST_CONFIGS.find(exists);
|
|
4921
5697
|
if (jest && vitest) {
|
|
@@ -4936,7 +5712,7 @@ function resolveGateCommand(manifest) {
|
|
|
4936
5712
|
var TOUCH_PREFIX = ".test-gates-touch-";
|
|
4937
5713
|
function realPath(file) {
|
|
4938
5714
|
try {
|
|
4939
|
-
return
|
|
5715
|
+
return fs14.realpathSync(file);
|
|
4940
5716
|
} catch {
|
|
4941
5717
|
return file;
|
|
4942
5718
|
}
|
|
@@ -4944,13 +5720,16 @@ function realPath(file) {
|
|
|
4944
5720
|
function shareOneRun(manifest, targets, runGate) {
|
|
4945
5721
|
let touchDir;
|
|
4946
5722
|
try {
|
|
4947
|
-
for (const name of
|
|
4948
|
-
const left =
|
|
4949
|
-
|
|
4950
|
-
|
|
5723
|
+
for (const name of fs14.readdirSync(manifest.dir)) {
|
|
5724
|
+
const left = path14.join(manifest.dir, name);
|
|
5725
|
+
try {
|
|
5726
|
+
if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
|
|
5727
|
+
fs14.rmSync(left, { recursive: true, force: true });
|
|
5728
|
+
}
|
|
5729
|
+
} catch {
|
|
4951
5730
|
}
|
|
4952
5731
|
}
|
|
4953
|
-
touchDir =
|
|
5732
|
+
touchDir = fs14.mkdtempSync(path14.join(manifest.dir, TOUCH_PREFIX));
|
|
4954
5733
|
} catch {
|
|
4955
5734
|
return { reason: "the subproject cannot be written to" };
|
|
4956
5735
|
}
|
|
@@ -4961,14 +5740,14 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4961
5740
|
return { reason: "the gate run with every spec does not pass" };
|
|
4962
5741
|
}
|
|
4963
5742
|
const base = realPath(manifest.dir);
|
|
4964
|
-
const absolute = (file) => realPath(
|
|
5743
|
+
const absolute = (file) => realPath(path14.resolve(base, file));
|
|
4965
5744
|
const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
|
|
4966
5745
|
const left = readNotes(touchDir);
|
|
4967
5746
|
plan = planControls({
|
|
4968
5747
|
all: manifest.gates.map((gate) => ({
|
|
4969
5748
|
path: gate.path,
|
|
4970
|
-
file:
|
|
4971
|
-
specFile:
|
|
5749
|
+
file: path14.join(base, gate.path),
|
|
5750
|
+
specFile: path14.join(base, gate.spec ?? "")
|
|
4972
5751
|
})),
|
|
4973
5752
|
targets: targets.map((gate) => gate.path),
|
|
4974
5753
|
run: left.run,
|
|
@@ -4977,7 +5756,7 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4977
5756
|
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
4978
5757
|
});
|
|
4979
5758
|
} finally {
|
|
4980
|
-
|
|
5759
|
+
fs14.rmSync(touchDir, { recursive: true, force: true });
|
|
4981
5760
|
}
|
|
4982
5761
|
if (plan.reason !== null) {
|
|
4983
5762
|
return { reason: plan.reason };
|
|
@@ -4987,15 +5766,15 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4987
5766
|
return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
|
|
4988
5767
|
}
|
|
4989
5768
|
function installedVersion2(start, name) {
|
|
4990
|
-
let current =
|
|
5769
|
+
let current = path14.resolve(start);
|
|
4991
5770
|
for (; ; ) {
|
|
4992
5771
|
try {
|
|
4993
5772
|
const manifest = JSON.parse(
|
|
4994
|
-
|
|
5773
|
+
fs14.readFileSync(path14.join(current, "node_modules", name, "package.json"), "utf8")
|
|
4995
5774
|
);
|
|
4996
5775
|
return typeof manifest.version === "string" ? manifest.version : null;
|
|
4997
5776
|
} catch {
|
|
4998
|
-
const parent =
|
|
5777
|
+
const parent = path14.dirname(current);
|
|
4999
5778
|
if (parent === current) {
|
|
5000
5779
|
return null;
|
|
5001
5780
|
}
|
|
@@ -5024,24 +5803,39 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
5024
5803
|
const results = /* @__PURE__ */ new Map();
|
|
5025
5804
|
let root;
|
|
5026
5805
|
try {
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5806
|
+
root = fs14.realpathSync(fs14.mkdtempSync(path14.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
5807
|
+
} catch {
|
|
5808
|
+
return results;
|
|
5809
|
+
}
|
|
5810
|
+
try {
|
|
5811
|
+
for (const name of fs14.readdirSync(os2.tmpdir())) {
|
|
5812
|
+
const left = path14.join(os2.tmpdir(), name);
|
|
5813
|
+
try {
|
|
5814
|
+
if (name.startsWith(JOBS_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
|
|
5815
|
+
fs14.rmSync(left, { recursive: true, force: true });
|
|
5816
|
+
}
|
|
5817
|
+
} catch {
|
|
5031
5818
|
}
|
|
5032
5819
|
}
|
|
5033
|
-
root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
5034
5820
|
} catch {
|
|
5035
|
-
return results;
|
|
5036
5821
|
}
|
|
5037
5822
|
const queue = [...gates];
|
|
5823
|
+
const lines = Math.min(jobs, gates.length);
|
|
5824
|
+
const workers = workerFlags({
|
|
5825
|
+
runner,
|
|
5826
|
+
args,
|
|
5827
|
+
configSource: gateConfigSource(manifest, args),
|
|
5828
|
+
lines,
|
|
5829
|
+
// (os.availableParallelism is in Node 18.14 and later.)
|
|
5830
|
+
cores: os2.availableParallelism?.() ?? os2.cpus().length
|
|
5831
|
+
});
|
|
5038
5832
|
const line = async (dir) => {
|
|
5039
5833
|
for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
|
|
5040
5834
|
const spec = gate.spec;
|
|
5041
5835
|
results.set(
|
|
5042
5836
|
gate,
|
|
5043
5837
|
await new Promise((resolve) => {
|
|
5044
|
-
const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
|
|
5838
|
+
const child = spawn(executable, [...args, ...jobFlags(runner, dir), ...workers], {
|
|
5045
5839
|
cwd: manifest.dir,
|
|
5046
5840
|
env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
|
|
5047
5841
|
shell: process.platform === "win32"
|
|
@@ -5060,13 +5854,10 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
5060
5854
|
};
|
|
5061
5855
|
try {
|
|
5062
5856
|
await Promise.all(
|
|
5063
|
-
Array.from(
|
|
5064
|
-
{ length: Math.min(jobs, gates.length) },
|
|
5065
|
-
(_unused, index) => line(path12.join(root, `line-${index}`))
|
|
5066
|
-
)
|
|
5857
|
+
Array.from({ length: lines }, (_unused, index) => line(path14.join(root, `line-${index}`)))
|
|
5067
5858
|
);
|
|
5068
5859
|
} finally {
|
|
5069
|
-
|
|
5860
|
+
fs14.rmSync(root, { recursive: true, force: true });
|
|
5070
5861
|
}
|
|
5071
5862
|
return results;
|
|
5072
5863
|
}
|
|
@@ -5095,7 +5886,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
5095
5886
|
const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
|
|
5096
5887
|
const [program, ...args] = command;
|
|
5097
5888
|
const executable = findLocalBin(manifest.dir, program) ?? program;
|
|
5098
|
-
const runGate = (env) =>
|
|
5889
|
+
const runGate = (env) => spawnSync6(executable, args, {
|
|
5099
5890
|
cwd: manifest.dir,
|
|
5100
5891
|
env: {
|
|
5101
5892
|
...process.env,
|
|
@@ -5173,6 +5964,11 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
5173
5964
|
...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
|
|
5174
5965
|
].sort((left, right) => right.length - left.length);
|
|
5175
5966
|
const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
|
|
5967
|
+
if (results.size === 0) {
|
|
5968
|
+
io2.out(
|
|
5969
|
+
`test-gates selfcheck: one negative control at a time (no directory for the runs next to others could be made in ${os2.tmpdir()})`
|
|
5970
|
+
);
|
|
5971
|
+
}
|
|
5176
5972
|
for (const [gate, result] of results) {
|
|
5177
5973
|
const output = `${result.stdout}${result.stderr}`;
|
|
5178
5974
|
const clean = cleanHold({
|
|
@@ -5190,7 +5986,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
5190
5986
|
}
|
|
5191
5987
|
}
|
|
5192
5988
|
const again = atOnce.length - heldAtOnce.size;
|
|
5193
|
-
if (again > 0) {
|
|
5989
|
+
if (again > 0 && results.size > 0) {
|
|
5194
5990
|
io2.out(
|
|
5195
5991
|
`test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
|
|
5196
5992
|
);
|
|
@@ -5278,7 +6074,9 @@ Options:
|
|
|
5278
6074
|
--affected mutation: run again only the gates that a change since the last
|
|
5279
6075
|
run can affect, and reuse the results of the others. Every
|
|
5280
6076
|
mutant is run when CI is set
|
|
5281
|
-
--shard <index>/<total> selfcheck: one of <total> parts of the gates, for parallel CI
|
|
6077
|
+
--shard <index>/<total> selfcheck, mutation: one of <total> parts of the gates, for parallel CI
|
|
6078
|
+
jobs. For mutation the gates are sorted and dealt out in turn; every
|
|
6079
|
+
part has to run. Accepted when CI is set
|
|
5282
6080
|
--jobs <n> selfcheck: run <n> negative controls at the same time. One that does
|
|
5283
6081
|
not hold cleanly there is run again on its own
|
|
5284
6082
|
--quick selfcheck: one run for the gates only their own spec touches.
|
|
@@ -5313,7 +6111,7 @@ function main(argv) {
|
|
|
5313
6111
|
case "doctor":
|
|
5314
6112
|
return runDoctor(args.dir, io);
|
|
5315
6113
|
case "mutation":
|
|
5316
|
-
return runMutation(args.dir, args.rest, io, args.gates, args.affected);
|
|
6114
|
+
return runMutation(args.dir, args.rest, io, args.gates, args.affected, args.shard);
|
|
5317
6115
|
case "mutation-result":
|
|
5318
6116
|
return runMutationResult(args.dir, args.report, io);
|
|
5319
6117
|
case "selfcheck":
|