@t09tanaka/test-gates 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +103 -24
- package/dist/{chunk-XMZ6NBV7.js → chunk-AK2NTFQO.js} +5 -0
- package/dist/cli.js +1697 -517
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/jest.js +1 -1
- package/dist/stryker.cjs +15 -2
- package/dist/stryker.d.cts +15 -1
- package/dist/stryker.d.ts +15 -1
- package/dist/stryker.js +9 -2
- package/dist/vitest.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
|
|
6
6
|
// src/core/args.ts
|
|
7
|
-
var COMMANDS = [
|
|
7
|
+
var COMMANDS = [
|
|
8
|
+
"check",
|
|
9
|
+
"mutation",
|
|
10
|
+
"mutation-result",
|
|
11
|
+
"selfcheck",
|
|
12
|
+
"lcov",
|
|
13
|
+
"doctor"
|
|
14
|
+
];
|
|
8
15
|
var UsageError = class extends Error {
|
|
9
16
|
};
|
|
10
17
|
var VALUE_OPTIONS = ["--dir", "--report", "--file", "--gate", "--shard", "--jobs"];
|
|
@@ -21,10 +28,11 @@ function parseShard(value) {
|
|
|
21
28
|
}
|
|
22
29
|
var OPTIONS_BY_COMMAND = {
|
|
23
30
|
check: ["--dir"],
|
|
24
|
-
mutation: ["--dir", "--gate"],
|
|
31
|
+
mutation: ["--dir", "--gate", "--affected"],
|
|
25
32
|
"mutation-result": ["--dir", "--report"],
|
|
26
33
|
selfcheck: ["--dir", "--all", "--first", "--gate", "--shard", "--quick", "--jobs"],
|
|
27
|
-
lcov: ["--dir", "--file"]
|
|
34
|
+
lcov: ["--dir", "--file"],
|
|
35
|
+
doctor: ["--dir"]
|
|
28
36
|
};
|
|
29
37
|
function consume(parsed, argv, index, allowed) {
|
|
30
38
|
const arg = argv[index];
|
|
@@ -77,6 +85,10 @@ function consume(parsed, argv, index, allowed) {
|
|
|
77
85
|
parsed.quick = true;
|
|
78
86
|
return next;
|
|
79
87
|
}
|
|
88
|
+
if (name === "--affected") {
|
|
89
|
+
parsed.affected = true;
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
80
92
|
const mode = name === "--all" ? "all" : "first";
|
|
81
93
|
if (parsed.mode !== null && parsed.mode !== mode) {
|
|
82
94
|
throw new UsageError("--all and --first cannot be combined");
|
|
@@ -104,6 +116,7 @@ function parseArgs(argv) {
|
|
|
104
116
|
gates: [],
|
|
105
117
|
shard: null,
|
|
106
118
|
quick: false,
|
|
119
|
+
affected: false,
|
|
107
120
|
jobs: null,
|
|
108
121
|
rest: []
|
|
109
122
|
};
|
|
@@ -128,14 +141,22 @@ function parseArgs(argv) {
|
|
|
128
141
|
if (selections.length > 1) {
|
|
129
142
|
throw new UsageError(`${selections.join(" and ")} cannot be combined`);
|
|
130
143
|
}
|
|
144
|
+
if (parsed.affected && parsed.gates.length > 0) {
|
|
145
|
+
throw new UsageError(
|
|
146
|
+
"--affected and --gate cannot be combined: --gate runs the gates it names, --affected decides which gates run"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
131
149
|
const misplaced = parsed.rest.find((arg) => /^--(gate|shard|quick|jobs)(=|$)/.test(arg));
|
|
132
150
|
if (parsed.command === "selfcheck" && misplaced !== void 0) {
|
|
133
151
|
throw new UsageError(
|
|
134
152
|
`${misplaced.split("=")[0]} after "--" would go to the gate command. Write it before "--"`
|
|
135
153
|
);
|
|
136
154
|
}
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
const forStryker = parsed.rest.find((arg) => /^--(gate|affected)(=|$)/.test(arg));
|
|
156
|
+
if (forStryker !== void 0) {
|
|
157
|
+
throw new UsageError(
|
|
158
|
+
`${forStryker.split("=")[0]} after "--" would go to Stryker. Write it before "--"`
|
|
159
|
+
);
|
|
139
160
|
}
|
|
140
161
|
if (parsed.command === "selfcheck" && parsed.rest.length === 0 && argv.includes("--")) {
|
|
141
162
|
throw new UsageError('"--" must be followed by the gate command');
|
|
@@ -1003,13 +1024,13 @@ function judgeSelfcheck(input) {
|
|
|
1003
1024
|
function selectControls(input) {
|
|
1004
1025
|
const { gates, mode, only, shard } = input;
|
|
1005
1026
|
if (only.length > 0) {
|
|
1006
|
-
const unknown = only.filter((
|
|
1027
|
+
const unknown = only.filter((path13) => !gates.includes(path13));
|
|
1007
1028
|
if (unknown.length > 0) {
|
|
1008
1029
|
return {
|
|
1009
1030
|
error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
|
|
1010
1031
|
};
|
|
1011
1032
|
}
|
|
1012
|
-
return { selected: gates.filter((
|
|
1033
|
+
return { selected: gates.filter((path13) => only.includes(path13)) };
|
|
1013
1034
|
}
|
|
1014
1035
|
if (shard !== null) {
|
|
1015
1036
|
if (shard.total > gates.length) {
|
|
@@ -1049,6 +1070,9 @@ function quickRefused(env) {
|
|
|
1049
1070
|
function gateRunRefused(env) {
|
|
1050
1071
|
return inCi(env) ? "--gate is refused in CI (the variable CI is set): a mutation run of a part of the gates is not the final check. Run mutation without it" : null;
|
|
1051
1072
|
}
|
|
1073
|
+
function affectedRefused(env) {
|
|
1074
|
+
return inCi(env) ? "CI is set, and there every mutant is run (the final check)" : null;
|
|
1075
|
+
}
|
|
1052
1076
|
function gateRunCaveat(judged, all) {
|
|
1053
1077
|
return `. Only ${judged} of ${all} gate(s) were run and judged (--gate), and a change in one gate can make a mutant of another survive: run without --gate for the final check`;
|
|
1054
1078
|
}
|
|
@@ -1384,179 +1408,34 @@ function runCheck(dir, io2) {
|
|
|
1384
1408
|
return 0;
|
|
1385
1409
|
}
|
|
1386
1410
|
|
|
1387
|
-
// src/commands/
|
|
1388
|
-
import
|
|
1389
|
-
import
|
|
1411
|
+
// src/commands/doctor.ts
|
|
1412
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1413
|
+
import fs9 from "fs";
|
|
1414
|
+
import path9 from "path";
|
|
1390
1415
|
|
|
1391
|
-
// src/core/
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1396
|
-
if (normalized.startsWith(`${root}/`)) {
|
|
1397
|
-
normalized = normalized.slice(root.length + 1);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
return normalized.replace(/^\.\//, "");
|
|
1401
|
-
}
|
|
1402
|
-
function parseLcov(text, rootDir) {
|
|
1403
|
-
const files = /* @__PURE__ */ new Map();
|
|
1404
|
-
let record = null;
|
|
1405
|
-
const closeRecord = () => {
|
|
1406
|
-
if (record && !record.hasDa) {
|
|
1407
|
-
record.file.found += record.found;
|
|
1408
|
-
record.file.hit += record.hit;
|
|
1409
|
-
}
|
|
1410
|
-
record = null;
|
|
1411
|
-
};
|
|
1412
|
-
for (const rawLine of text.split("\n")) {
|
|
1413
|
-
const line = rawLine.trim();
|
|
1414
|
-
if (line.startsWith("SF:")) {
|
|
1415
|
-
closeRecord();
|
|
1416
|
-
const path11 = normalizePath(line.slice(3), rootDir);
|
|
1417
|
-
const file = files.get(path11) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
1418
|
-
files.set(path11, file);
|
|
1419
|
-
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
1420
|
-
} else if (line === "end_of_record") {
|
|
1421
|
-
closeRecord();
|
|
1422
|
-
} else if (record) {
|
|
1423
|
-
const open = record;
|
|
1424
|
-
if (line.startsWith("DA:")) {
|
|
1425
|
-
const [lineNumber, hits] = line.slice(3).split(",");
|
|
1426
|
-
const number = Number(lineNumber);
|
|
1427
|
-
const count = Number(hits);
|
|
1428
|
-
if (Number.isInteger(number) && Number.isFinite(count)) {
|
|
1429
|
-
open.hasDa = true;
|
|
1430
|
-
open.file.lines.set(number, Math.max(open.file.lines.get(number) ?? 0, count));
|
|
1431
|
-
}
|
|
1432
|
-
} else if (line.startsWith("LF:")) {
|
|
1433
|
-
open.found = Number(line.slice(3)) || 0;
|
|
1434
|
-
} else if (line.startsWith("LH:")) {
|
|
1435
|
-
open.hit = Number(line.slice(3)) || 0;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
closeRecord();
|
|
1440
|
-
const result = /* @__PURE__ */ new Map();
|
|
1441
|
-
for (const [file, accumulator] of files) {
|
|
1442
|
-
const uncovered = [...accumulator.lines.entries()].filter(([, hits]) => hits <= 0).map(([lineNumber]) => lineNumber).sort((a, b) => a - b);
|
|
1443
|
-
result.set(file, {
|
|
1444
|
-
found: accumulator.lines.size + accumulator.found,
|
|
1445
|
-
hit: accumulator.lines.size - uncovered.length + accumulator.hit,
|
|
1446
|
-
uncovered
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
return result;
|
|
1450
|
-
}
|
|
1451
|
-
function judgeLcov(gatePaths, coverage) {
|
|
1452
|
-
const violations = [];
|
|
1453
|
-
const results = [];
|
|
1454
|
-
for (const gatePath of gatePaths) {
|
|
1455
|
-
const file = coverage.get(normalizePath(gatePath));
|
|
1456
|
-
if (!file) {
|
|
1457
|
-
violations.push({ file: gatePath, message: "no record in the lcov file" });
|
|
1458
|
-
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
1459
|
-
continue;
|
|
1460
|
-
}
|
|
1461
|
-
if (file.found === 0) {
|
|
1462
|
-
violations.push({ file: gatePath, message: "no instrumented lines in the lcov record" });
|
|
1463
|
-
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
const ok = file.hit === file.found;
|
|
1467
|
-
if (!ok) {
|
|
1468
|
-
const shown = file.uncovered.slice(0, 20).join(", ");
|
|
1469
|
-
const more = file.uncovered.length > 20 ? ", \u2026" : "";
|
|
1470
|
-
violations.push({
|
|
1471
|
-
file: gatePath,
|
|
1472
|
-
...file.uncovered[0] === void 0 ? {} : { line: file.uncovered[0] },
|
|
1473
|
-
message: `${file.hit}/${file.found} lines covered` + (shown === "" ? "" : ` (uncovered: ${shown}${more})`)
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
results.push({ path: gatePath, found: file.found, hit: file.hit, ok });
|
|
1477
|
-
}
|
|
1478
|
-
return { violations, results };
|
|
1479
|
-
}
|
|
1480
|
-
function summarizeLcov(coverage, exclude) {
|
|
1481
|
-
let found = 0;
|
|
1482
|
-
let hit = 0;
|
|
1483
|
-
for (const [file, lines] of coverage) {
|
|
1484
|
-
if (exclude.some((regex) => regex.test(file))) {
|
|
1485
|
-
continue;
|
|
1486
|
-
}
|
|
1487
|
-
found += lines.found;
|
|
1488
|
-
hit += lines.hit;
|
|
1489
|
-
}
|
|
1490
|
-
return { found, hit };
|
|
1416
|
+
// src/core/reuse-guard.ts
|
|
1417
|
+
var RECORD_FORMAT = 2;
|
|
1418
|
+
function isStringRecord(value) {
|
|
1419
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1491
1420
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (file === null) {
|
|
1498
|
-
throw new ManifestError(
|
|
1499
|
-
"no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
|
|
1500
|
-
);
|
|
1501
|
-
}
|
|
1502
|
-
const lcovPath = path4.resolve(manifest.dir, file);
|
|
1503
|
-
let text;
|
|
1504
|
-
try {
|
|
1505
|
-
text = fs4.readFileSync(lcovPath, "utf8");
|
|
1506
|
-
} catch (error) {
|
|
1507
|
-
io2.err(
|
|
1508
|
-
`test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
|
|
1509
|
-
);
|
|
1510
|
-
return 1;
|
|
1421
|
+
var isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1422
|
+
function parseAffected(value) {
|
|
1423
|
+
const { tests, gates } = value ?? {};
|
|
1424
|
+
if (!isStringList(tests) || typeof gates !== "object" || gates === null || Array.isArray(gates)) {
|
|
1425
|
+
return null;
|
|
1511
1426
|
}
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
violations.push({
|
|
1518
|
-
file: gate.path,
|
|
1519
|
-
message: "file not found (the check is case-sensitive). Update test-gates.json if it moved"
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
return exists;
|
|
1523
|
-
});
|
|
1524
|
-
const coverage = parseLcov(text, manifest.dir);
|
|
1525
|
-
const total = summarizeLcov(coverage, manifest.settings.lcovSummaryExclude);
|
|
1526
|
-
io2.out(
|
|
1527
|
-
total.found === 0 ? "reference: no instrumented lines in the lcov file" : `reference: overall line coverage ${(total.hit / total.found * 100).toFixed(2)}% (${total.hit}/${total.found}, not gated)`
|
|
1528
|
-
);
|
|
1529
|
-
const verdict = judgeLcov(
|
|
1530
|
-
existing.map((gate) => gate.path),
|
|
1531
|
-
coverage
|
|
1532
|
-
);
|
|
1533
|
-
for (const result of verdict.results) {
|
|
1534
|
-
if (result.ok) {
|
|
1535
|
-
io2.out(` \u2713 ${result.path}: ${result.hit}/${result.found} lines`);
|
|
1427
|
+
const parsed = /* @__PURE__ */ Object.create(null);
|
|
1428
|
+
for (const [gate, entry] of Object.entries(gates)) {
|
|
1429
|
+
const { specs, files } = entry ?? {};
|
|
1430
|
+
if (!isStringList(specs) || !isStringList(files)) {
|
|
1431
|
+
return null;
|
|
1536
1432
|
}
|
|
1433
|
+
parsed[gate] = { specs, files };
|
|
1537
1434
|
}
|
|
1538
|
-
|
|
1539
|
-
if (violations.length > 0) {
|
|
1540
|
-
printViolations(io2, "lcov", violations);
|
|
1541
|
-
return 1;
|
|
1542
|
-
}
|
|
1543
|
-
io2.out(`test-gates lcov: OK (${verdict.results.length} gate(s) at 100% line coverage)`);
|
|
1544
|
-
return 0;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// src/commands/mutation.ts
|
|
1548
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
1549
|
-
import fs7 from "fs";
|
|
1550
|
-
import path7 from "path";
|
|
1551
|
-
import { pathToFileURL } from "url";
|
|
1552
|
-
|
|
1553
|
-
// src/core/reuse-guard.ts
|
|
1554
|
-
var RECORD_FORMAT = 1;
|
|
1555
|
-
function isStringRecord(value) {
|
|
1556
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1435
|
+
return { tests, gates: parsed };
|
|
1557
1436
|
}
|
|
1558
1437
|
function parseRecord(value) {
|
|
1559
|
-
const { format, incremental, snapshot } = value ?? {};
|
|
1438
|
+
const { format, incremental, snapshot, affected } = value ?? {};
|
|
1560
1439
|
if (format !== RECORD_FORMAT || typeof incremental !== "string") {
|
|
1561
1440
|
return null;
|
|
1562
1441
|
}
|
|
@@ -1567,13 +1446,18 @@ function parseRecord(value) {
|
|
|
1567
1446
|
if (mode !== "git" && mode !== "walk") {
|
|
1568
1447
|
return null;
|
|
1569
1448
|
}
|
|
1570
|
-
if (!
|
|
1449
|
+
if (!isStringList(args) || !isStringRecord(environment) || !isStringRecord(files)) {
|
|
1571
1450
|
return null;
|
|
1572
1451
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1452
|
+
const record = { format, incremental, snapshot: { mode, environment, args, files } };
|
|
1453
|
+
if (affected !== void 0) {
|
|
1454
|
+
const parsed = parseAffected(affected);
|
|
1455
|
+
if (parsed === null) {
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
record.affected = parsed;
|
|
1575
1459
|
}
|
|
1576
|
-
return
|
|
1460
|
+
return record;
|
|
1577
1461
|
}
|
|
1578
1462
|
function listed(names) {
|
|
1579
1463
|
const shown = names.slice(0, 3).join(", ");
|
|
@@ -1582,33 +1466,42 @@ function listed(names) {
|
|
|
1582
1466
|
function differingKeys(before, after) {
|
|
1583
1467
|
return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).sort();
|
|
1584
1468
|
}
|
|
1585
|
-
function
|
|
1469
|
+
function readRecord(input) {
|
|
1586
1470
|
if (input.record === void 0) {
|
|
1587
|
-
return "there is no record of the run that wrote the incremental file";
|
|
1471
|
+
return { refused: "there is no record of the run that wrote the incremental file" };
|
|
1588
1472
|
}
|
|
1589
1473
|
const record = parseRecord(input.record);
|
|
1590
1474
|
if (record === null) {
|
|
1591
|
-
return "the record of the last run is not one this version can read";
|
|
1475
|
+
return { refused: "the record of the last run is not one this version can read" };
|
|
1592
1476
|
}
|
|
1593
1477
|
if (record.incremental !== input.incremental) {
|
|
1594
|
-
return "the incremental file is not the one the last recorded run wrote";
|
|
1478
|
+
return { refused: "the incremental file is not the one the last recorded run wrote" };
|
|
1595
1479
|
}
|
|
1596
1480
|
const before = record.snapshot;
|
|
1597
1481
|
const now = input.snapshot;
|
|
1598
1482
|
if (before.mode !== now.mode) {
|
|
1599
|
-
return
|
|
1483
|
+
return {
|
|
1484
|
+
refused: `the files are listed in another way than in the last run (${before.mode}, now ${now.mode})`
|
|
1485
|
+
};
|
|
1600
1486
|
}
|
|
1601
1487
|
const environment = differingKeys(before.environment, now.environment);
|
|
1602
1488
|
if (environment.length > 0) {
|
|
1603
1489
|
const changes = environment.map(
|
|
1604
1490
|
(key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
|
|
1605
1491
|
);
|
|
1606
|
-
return `the tools changed since the last run: ${listed(changes)}
|
|
1492
|
+
return { refused: `the tools changed since the last run: ${listed(changes)}` };
|
|
1607
1493
|
}
|
|
1608
1494
|
if (JSON.stringify(before.args) !== JSON.stringify(now.args)) {
|
|
1609
|
-
return "the arguments for Stryker are not those of the last run";
|
|
1495
|
+
return { refused: "the arguments for Stryker are not those of the last run" };
|
|
1496
|
+
}
|
|
1497
|
+
return { record };
|
|
1498
|
+
}
|
|
1499
|
+
function refuseReuse(input) {
|
|
1500
|
+
const read2 = readRecord(input);
|
|
1501
|
+
if ("refused" in read2) {
|
|
1502
|
+
return read2.refused;
|
|
1610
1503
|
}
|
|
1611
|
-
const files = differingKeys(
|
|
1504
|
+
const files = differingKeys(read2.record.snapshot.files, input.snapshot.files);
|
|
1612
1505
|
if (files.length > 0) {
|
|
1613
1506
|
return `changed since the last run: ${listed(files)}`;
|
|
1614
1507
|
}
|
|
@@ -1639,6 +1532,232 @@ function comparableArgs(args) {
|
|
|
1639
1532
|
return args.filter((arg) => arg !== "--force");
|
|
1640
1533
|
}
|
|
1641
1534
|
|
|
1535
|
+
// src/core/affected.ts
|
|
1536
|
+
var CONFIG_NAMES = [
|
|
1537
|
+
/^package\.json$/,
|
|
1538
|
+
/^(?:package-lock|npm-shrinkwrap)\.json$/,
|
|
1539
|
+
/^test-gates\.json$/,
|
|
1540
|
+
/^[jt]sconfig(?:\..*)?\.json$/,
|
|
1541
|
+
/\.config\.[^.]+$/,
|
|
1542
|
+
/\.config\.[^.]+\.[^.]+$/,
|
|
1543
|
+
/^(?:jest|vitest|vite|stryker|babel)[.-]/,
|
|
1544
|
+
/^\./,
|
|
1545
|
+
// Declarations need no import to take effect.
|
|
1546
|
+
/\.d\.[cm]?ts$/
|
|
1547
|
+
];
|
|
1548
|
+
var CONFIG_DIRECTORIES = ["__mocks__", "node_modules"];
|
|
1549
|
+
var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
|
|
1550
|
+
var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
|
|
1551
|
+
function kindOf(key, subproject) {
|
|
1552
|
+
const inside = subproject === "" || key.startsWith(`${subproject}/`);
|
|
1553
|
+
const local = inside && subproject !== "" ? key.slice(subproject.length + 1) : key;
|
|
1554
|
+
const segments = local.split("/");
|
|
1555
|
+
const name = segments[segments.length - 1];
|
|
1556
|
+
if (segments.some((segment) => CONFIG_DIRECTORIES.includes(segment)) || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
|
|
1557
|
+
return "config";
|
|
1558
|
+
}
|
|
1559
|
+
if (DOCUMENT.test(name)) {
|
|
1560
|
+
return "document";
|
|
1561
|
+
}
|
|
1562
|
+
if (!inside || segments.length === 1 || !PASSIVE.test(name)) {
|
|
1563
|
+
return "other";
|
|
1564
|
+
}
|
|
1565
|
+
return "passive";
|
|
1566
|
+
}
|
|
1567
|
+
function gateDependencies(input) {
|
|
1568
|
+
const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
|
|
1569
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1570
|
+
for (const gate of input.gates) {
|
|
1571
|
+
const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
|
|
1572
|
+
const specs = /* @__PURE__ */ new Set([...gate.specs, ...covering]);
|
|
1573
|
+
for (const test of input.tests) {
|
|
1574
|
+
if (closureOf(test).includes(gate.key)) {
|
|
1575
|
+
specs.add(test);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const files = new Set(closureOf(gate.key));
|
|
1579
|
+
for (const spec of specs) {
|
|
1580
|
+
closureOf(spec).forEach((file) => files.add(file));
|
|
1581
|
+
}
|
|
1582
|
+
result[gate.key] = { specs: [...specs].sort(), files: [...files].sort() };
|
|
1583
|
+
}
|
|
1584
|
+
return result;
|
|
1585
|
+
}
|
|
1586
|
+
function planAffected(input) {
|
|
1587
|
+
const recorded = input.before.affected;
|
|
1588
|
+
if (recorded === void 0) {
|
|
1589
|
+
return {
|
|
1590
|
+
full: "the last run did not write down what each gate depends on (it ran without --affected)"
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const changed = differingKeys(input.before.files, input.now);
|
|
1594
|
+
const reached = new Set(input.gates.flatMap((gate) => gate.files));
|
|
1595
|
+
Object.values(recorded.gates).forEach((gate) => gate.files.forEach((file) => reached.add(file)));
|
|
1596
|
+
const config = new Set(input.config);
|
|
1597
|
+
const everywhere = changed.filter((key) => {
|
|
1598
|
+
const kind = kindOf(key, input.subproject);
|
|
1599
|
+
return config.has(key) || kind === "config" || kind === "other" && !reached.has(key);
|
|
1600
|
+
});
|
|
1601
|
+
if (everywhere.length > 0) {
|
|
1602
|
+
return { full: `changed since the last run, and may affect every gate: ${listed(everywhere)}` };
|
|
1603
|
+
}
|
|
1604
|
+
const rerun = [];
|
|
1605
|
+
const reused = [];
|
|
1606
|
+
for (const gate of input.gates) {
|
|
1607
|
+
const last = Object.hasOwn(recorded.gates, gate.path) ? recorded.gates[gate.path] : void 0;
|
|
1608
|
+
if (last === void 0) {
|
|
1609
|
+
rerun.push({ gate: gate.path, changed: [] });
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
|
|
1613
|
+
const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
|
|
1614
|
+
if (differing.length > 0) {
|
|
1615
|
+
rerun.push({ gate: gate.path, changed: differing });
|
|
1616
|
+
} else {
|
|
1617
|
+
reused.push(gate.path);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return { rerun, reused };
|
|
1621
|
+
}
|
|
1622
|
+
function withoutDots(file) {
|
|
1623
|
+
return file.split("/").filter((part) => part !== ".").join("/");
|
|
1624
|
+
}
|
|
1625
|
+
function timeoutRanges(last, gates) {
|
|
1626
|
+
const ranges = [];
|
|
1627
|
+
const files = last.files ?? {};
|
|
1628
|
+
for (const named of gates) {
|
|
1629
|
+
const gate = withoutDots(named);
|
|
1630
|
+
const mutants = Object.hasOwn(files, gate) ? files[gate]?.mutants : void 0;
|
|
1631
|
+
if (mutants === void 0) {
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
for (const mutant of mutants) {
|
|
1635
|
+
if (mutant.status === "Timeout") {
|
|
1636
|
+
const { start, end } = mutant.location;
|
|
1637
|
+
ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return [...new Set(ranges)];
|
|
1642
|
+
}
|
|
1643
|
+
function describePlan(plan, shown = 5, timeouts = 0) {
|
|
1644
|
+
if ("full" in plan) {
|
|
1645
|
+
return [
|
|
1646
|
+
`test-gates mutation --affected: running every mutant again (--force), the results of the last run are not reused: ${plan.full}`
|
|
1647
|
+
];
|
|
1648
|
+
}
|
|
1649
|
+
const total = plan.rerun.length + plan.reused.length;
|
|
1650
|
+
const lines = [
|
|
1651
|
+
`test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
|
|
1652
|
+
];
|
|
1653
|
+
for (const { gate, changed } of plan.rerun.slice(0, shown)) {
|
|
1654
|
+
lines.push(
|
|
1655
|
+
changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (plan.rerun.length > shown) {
|
|
1659
|
+
lines.push(` and ${plan.rerun.length - shown} more`);
|
|
1660
|
+
}
|
|
1661
|
+
if (timeouts > 0) {
|
|
1662
|
+
lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
|
|
1663
|
+
}
|
|
1664
|
+
return lines;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/core/doctor.ts
|
|
1668
|
+
var NOT_THE_HELPER = "the runner config is not made with";
|
|
1669
|
+
function diagnose(facts) {
|
|
1670
|
+
const findings = [];
|
|
1671
|
+
if (!facts.incremental) {
|
|
1672
|
+
findings.push({
|
|
1673
|
+
level: "problem",
|
|
1674
|
+
text: "Stryker does not run in incremental mode: every run is one of every mutant",
|
|
1675
|
+
fix: "make the Stryker config with createStrykerGatesConfig, or set incremental: true"
|
|
1676
|
+
});
|
|
1677
|
+
return findings;
|
|
1678
|
+
}
|
|
1679
|
+
if (facts.forced) {
|
|
1680
|
+
findings.push({
|
|
1681
|
+
level: "problem",
|
|
1682
|
+
text: "the Stryker config sets force: every run is one of every mutant",
|
|
1683
|
+
fix: "remove force from the Stryker config; pass -- --force where a full run is meant (CI)"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (facts.failure !== null) {
|
|
1687
|
+
findings.push({
|
|
1688
|
+
level: "problem",
|
|
1689
|
+
text: `the results of a run are never reused: ${facts.failure}`,
|
|
1690
|
+
fix: facts.failure.includes("points outside") ? "remove the path, or let it lead into the repository. A path that leads outside of it to nothing is compared as missing" : null
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
if (facts.widened.length > 0) {
|
|
1694
|
+
findings.push({
|
|
1695
|
+
level: "problem",
|
|
1696
|
+
text: `every file of the repository is compared, so a change anywhere in it makes the next run a full one. Because of: ${facts.widened.join("; ")}`,
|
|
1697
|
+
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"
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
if (facts.scope !== null && "every" in facts.scope) {
|
|
1701
|
+
findings.push({
|
|
1702
|
+
level: "note",
|
|
1703
|
+
text: `the paths in every file of the subproject are followed, also in files the run does not load: ${facts.scope.every}`,
|
|
1704
|
+
fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
if (facts.hooks.length === 0) {
|
|
1708
|
+
findings.push({
|
|
1709
|
+
level: "note",
|
|
1710
|
+
text: "no git hook runs test-gates mutation for this subproject",
|
|
1711
|
+
fix: null
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
for (const hook of facts.hooks.filter((one) => !one.affected)) {
|
|
1715
|
+
findings.push({
|
|
1716
|
+
level: "problem",
|
|
1717
|
+
text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
|
|
1718
|
+
fix: "pass --affected in the hook (npm run test:gates:mutation -- --affected); keep the run in CI without it"
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
return findings;
|
|
1722
|
+
}
|
|
1723
|
+
function formatFindings(findings) {
|
|
1724
|
+
const lines = [];
|
|
1725
|
+
for (const finding of findings) {
|
|
1726
|
+
lines.push(`${finding.level === "problem" ? "problem" : "note"}: ${finding.text}`);
|
|
1727
|
+
if (finding.fix !== null) {
|
|
1728
|
+
lines.push(` fix: ${finding.fix}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const problems = findings.filter((finding) => finding.level === "problem").length;
|
|
1732
|
+
lines.push(
|
|
1733
|
+
problems === 0 ? "test-gates doctor: OK" : `test-gates doctor: ${problems} problem(s) that make runs full ones`
|
|
1734
|
+
);
|
|
1735
|
+
return { lines, status: problems === 0 ? 0 : 1 };
|
|
1736
|
+
}
|
|
1737
|
+
var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
|
|
1738
|
+
var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
|
|
1739
|
+
var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
|
|
1740
|
+
function hookUses(input) {
|
|
1741
|
+
const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
|
|
1742
|
+
const uses = [];
|
|
1743
|
+
lines.forEach((line, index) => {
|
|
1744
|
+
if (!RUNS_MUTATION.test(line)) {
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const named = TARGET.exec(line) ?? CD.exec(line);
|
|
1748
|
+
const target = named?.[2] ?? lines.slice(0, index).reverse().map((before) => CD.exec(before)?.[2]).find((found) => found !== void 0);
|
|
1749
|
+
const normalized = target?.split("/").filter((part) => part !== "." && part !== "").join("/");
|
|
1750
|
+
const ours = normalized === void 0 || normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
|
|
1751
|
+
if (ours) {
|
|
1752
|
+
uses.push({
|
|
1753
|
+
file: input.file,
|
|
1754
|
+
affected: /--affected\b/.test(line) || /test:gates:mutation\b/.test(line) && input.script.includes("--affected")
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
return uses;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1642
1761
|
// src/snapshot.ts
|
|
1643
1762
|
import { spawnSync } from "child_process";
|
|
1644
1763
|
import { createHash } from "crypto";
|
|
@@ -1646,19 +1765,169 @@ import fs5 from "fs";
|
|
|
1646
1765
|
import { createRequire } from "module";
|
|
1647
1766
|
import os from "os";
|
|
1648
1767
|
import path5 from "path";
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1768
|
+
|
|
1769
|
+
// src/commands/loaded-files.ts
|
|
1770
|
+
import fs4 from "fs";
|
|
1771
|
+
import { builtinModules } from "module";
|
|
1772
|
+
import path4 from "path";
|
|
1773
|
+
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
1774
|
+
var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
|
|
1775
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1776
|
+
var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
1777
|
+
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
1778
|
+
function isFile(file) {
|
|
1779
|
+
try {
|
|
1780
|
+
return fs4.statSync(file).isFile();
|
|
1781
|
+
} catch {
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function resolveFile(base, directory = true) {
|
|
1786
|
+
const candidates = [
|
|
1787
|
+
base,
|
|
1788
|
+
...EXTENSIONS.map((extension) => base + extension),
|
|
1789
|
+
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
1790
|
+
...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
1791
|
+
...directory ? EXTENSIONS.map((extension) => path4.join(base, `index${extension}`)) : []
|
|
1792
|
+
];
|
|
1793
|
+
return candidates.find(isFile) ?? null;
|
|
1794
|
+
}
|
|
1795
|
+
function installed(specifier, from) {
|
|
1796
|
+
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
1797
|
+
for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
|
|
1798
|
+
if (fs4.existsSync(path4.join(current, "node_modules", name))) {
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
if (current === path4.dirname(current)) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function loadedFiles(manifest, configFile) {
|
|
1807
|
+
const aliases = manifest.settings.importAliases;
|
|
1808
|
+
const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
|
|
1809
|
+
const target = (specifier, from) => {
|
|
1810
|
+
if (specifier.startsWith(".")) {
|
|
1811
|
+
return resolveFile(path4.resolve(path4.dirname(from), specifier));
|
|
1812
|
+
}
|
|
1813
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
1814
|
+
return alias === void 0 ? null : resolveFile(
|
|
1815
|
+
path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
1816
|
+
);
|
|
1817
|
+
};
|
|
1818
|
+
const read2 = (file) => {
|
|
1819
|
+
const source = fs4.readFileSync(file, "utf8");
|
|
1820
|
+
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
1821
|
+
const specifiers = [
|
|
1822
|
+
.../* @__PURE__ */ new Set([
|
|
1823
|
+
...imports.map((ref) => ref.module),
|
|
1824
|
+
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
1825
|
+
])
|
|
1826
|
+
];
|
|
1827
|
+
const strings = [...source.matchAll(QUOTED)].map((match) => match[2]).filter((text) => !specifiers.includes(text));
|
|
1828
|
+
return { source, specifiers, strings };
|
|
1829
|
+
};
|
|
1830
|
+
const named = /* @__PURE__ */ new Set();
|
|
1831
|
+
const configs = configFile === null ? [] : [path4.resolve(manifest.dir, configFile)];
|
|
1832
|
+
const seenConfigs = new Set(configs);
|
|
1833
|
+
const entry = configs[0];
|
|
1834
|
+
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
1835
|
+
const { specifiers, strings } = read2(file);
|
|
1836
|
+
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
1837
|
+
for (const text of paths) {
|
|
1838
|
+
const stripped = text.replace(/^<rootDir>\//, "");
|
|
1839
|
+
const found = resolveFile(path4.resolve(path4.dirname(file), stripped), false) ?? resolveFile(path4.resolve(manifest.dir, stripped), false);
|
|
1840
|
+
if (found !== null && SCRIPT.test(found)) {
|
|
1841
|
+
named.add(found);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
|
|
1845
|
+
const found = target(specifier, file);
|
|
1846
|
+
if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
|
|
1847
|
+
seenConfigs.add(found);
|
|
1848
|
+
configs.push(found);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const known = /* @__PURE__ */ new Map();
|
|
1853
|
+
const visit = (file) => {
|
|
1854
|
+
const done = known.get(file);
|
|
1855
|
+
if (done !== void 0) {
|
|
1856
|
+
return done;
|
|
1857
|
+
}
|
|
1858
|
+
const { source, specifiers, strings } = read2(file);
|
|
1859
|
+
const imports = [];
|
|
1860
|
+
const next = [];
|
|
1861
|
+
let lost = COMPONENT.test(file) ? `${relative(file)} is a component, which can use others that it does not import` : null;
|
|
1862
|
+
for (const specifier of specifiers) {
|
|
1863
|
+
if (isProjectImport(specifier, aliases)) {
|
|
1864
|
+
const found = target(specifier, file);
|
|
1865
|
+
if (found === null) {
|
|
1866
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
1867
|
+
} else {
|
|
1868
|
+
next.push(found);
|
|
1869
|
+
}
|
|
1870
|
+
} else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
|
|
1871
|
+
imports.push({ specifier, builtin: true });
|
|
1872
|
+
} else if (installed(specifier, file)) {
|
|
1873
|
+
imports.push({ specifier, builtin: false });
|
|
1874
|
+
} else {
|
|
1875
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
1879
|
+
const found = resolveFile(path4.resolve(path4.dirname(file), text));
|
|
1880
|
+
if (found !== null) {
|
|
1881
|
+
next.push(found);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const visited = {
|
|
1885
|
+
file: { path: relative(file), source, imports },
|
|
1886
|
+
// A component imports like a script (see COMPONENT); data imports nothing.
|
|
1887
|
+
next: next.filter((found) => SCRIPT.test(found) || COMPONENT.test(found)),
|
|
1888
|
+
lost
|
|
1889
|
+
};
|
|
1890
|
+
known.set(file, visited);
|
|
1891
|
+
return visited;
|
|
1892
|
+
};
|
|
1893
|
+
const closure = (spec, starts) => {
|
|
1894
|
+
const queue = [...starts];
|
|
1895
|
+
const seen = new Set(queue);
|
|
1896
|
+
const files = [];
|
|
1897
|
+
let lost = null;
|
|
1898
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
1899
|
+
const visited = visit(file);
|
|
1900
|
+
files.push(visited.file.path);
|
|
1901
|
+
lost ??= visited.lost;
|
|
1902
|
+
for (const found of visited.next.filter((one) => !seen.has(one))) {
|
|
1903
|
+
seen.add(found);
|
|
1904
|
+
queue.push(found);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return { spec, files, lost };
|
|
1908
|
+
};
|
|
1909
|
+
const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
|
|
1910
|
+
const roots = [
|
|
1911
|
+
closure(null, [...named]),
|
|
1912
|
+
...specs.map((spec) => closure(spec, [path4.join(manifest.dir, spec)]))
|
|
1913
|
+
];
|
|
1914
|
+
return { files: [...known.values()].map((visited) => visited.file), roots };
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/snapshot.ts
|
|
1918
|
+
var SnapshotError = class extends Error {
|
|
1919
|
+
};
|
|
1920
|
+
var TOOLS = [
|
|
1921
|
+
"@stryker-mutator/core",
|
|
1922
|
+
"@stryker-mutator/jest-runner",
|
|
1923
|
+
"@stryker-mutator/vitest-runner",
|
|
1924
|
+
"@stryker-mutator/typescript-checker",
|
|
1925
|
+
"jest",
|
|
1926
|
+
"babel-jest",
|
|
1927
|
+
"ts-jest",
|
|
1928
|
+
"@swc/jest",
|
|
1929
|
+
"vitest",
|
|
1930
|
+
"vite",
|
|
1662
1931
|
"@vitest/coverage-istanbul",
|
|
1663
1932
|
"@vitest/coverage-v8",
|
|
1664
1933
|
"typescript"
|
|
@@ -1678,8 +1947,14 @@ var LOCK_FILES = [
|
|
|
1678
1947
|
"bun.lockb"
|
|
1679
1948
|
];
|
|
1680
1949
|
var NEVER = ["node_modules", ".git"];
|
|
1950
|
+
var WIDENED_SHOWN = 5;
|
|
1681
1951
|
var SCAN_LIMIT = 8 * 1024 * 1024;
|
|
1682
1952
|
var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
|
|
1953
|
+
var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
|
|
1954
|
+
var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
|
|
1955
|
+
var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
|
|
1956
|
+
var LITERAL = /(['"])([^'"`\n]*)\1/g;
|
|
1957
|
+
var isUpward = (segment) => segment.split("/").includes("..");
|
|
1683
1958
|
function hashOf(content) {
|
|
1684
1959
|
return createHash("sha256").update(content).digest("hex");
|
|
1685
1960
|
}
|
|
@@ -1697,8 +1972,21 @@ function isInside(parent, child) {
|
|
|
1697
1972
|
function toPosix(file) {
|
|
1698
1973
|
return file.split(path5.sep).join("/");
|
|
1699
1974
|
}
|
|
1975
|
+
function gitEnvironment(env) {
|
|
1976
|
+
if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
|
|
1977
|
+
return env;
|
|
1978
|
+
}
|
|
1979
|
+
const rest = { ...env };
|
|
1980
|
+
delete rest.GIT_DIR;
|
|
1981
|
+
return rest;
|
|
1982
|
+
}
|
|
1700
1983
|
function git(cwd, args) {
|
|
1701
|
-
const result = spawnSync("git", args, {
|
|
1984
|
+
const result = spawnSync("git", args, {
|
|
1985
|
+
cwd,
|
|
1986
|
+
env: gitEnvironment(process.env),
|
|
1987
|
+
encoding: "utf8",
|
|
1988
|
+
maxBuffer: 1024 * 1024 * 1024
|
|
1989
|
+
});
|
|
1702
1990
|
return result.error || result.status !== 0 ? null : result.stdout;
|
|
1703
1991
|
}
|
|
1704
1992
|
function gitLister(root) {
|
|
@@ -1831,7 +2119,7 @@ function ownVersion() {
|
|
|
1831
2119
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
1832
2120
|
return version;
|
|
1833
2121
|
}
|
|
1834
|
-
function
|
|
2122
|
+
function snapshotOf(input) {
|
|
1835
2123
|
const dir = fs5.realpathSync(input.dir);
|
|
1836
2124
|
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1837
2125
|
let root = top === null ? dir : fs5.realpathSync(top.trim());
|
|
@@ -1845,6 +2133,7 @@ function takeSnapshot(input) {
|
|
|
1845
2133
|
}
|
|
1846
2134
|
const listRoot = root;
|
|
1847
2135
|
const list = lister;
|
|
2136
|
+
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path5.join(listRoot, file)));
|
|
1848
2137
|
const walk = walkLister(listRoot);
|
|
1849
2138
|
const written = input.written.map((file) => {
|
|
1850
2139
|
const target = path5.resolve(dir, file);
|
|
@@ -1860,34 +2149,75 @@ function takeSnapshot(input) {
|
|
|
1860
2149
|
}
|
|
1861
2150
|
const files = /* @__PURE__ */ Object.create(null);
|
|
1862
2151
|
const regions = [dir];
|
|
2152
|
+
const widened = [];
|
|
1863
2153
|
const queued = new Set(regions);
|
|
1864
|
-
const enqueue = (target, from) => {
|
|
1865
|
-
const
|
|
2154
|
+
const enqueue = (target, from, reference, anchored = false) => {
|
|
2155
|
+
const above2 = target !== dir && isInside(target, dir) && isInside(listRoot, target);
|
|
2156
|
+
if (above2 && anchored) {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const directory = above2 ? listRoot : target;
|
|
1866
2160
|
if (!isInside(listRoot, directory)) {
|
|
2161
|
+
const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
|
|
1867
2162
|
throw new SnapshotError(
|
|
1868
|
-
`${toPosix(path5.relative(dir, from))} points outside of what can be listed (${directory})`
|
|
2163
|
+
`${toPosix(path5.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
|
|
1869
2164
|
);
|
|
1870
2165
|
}
|
|
2166
|
+
if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
|
|
2167
|
+
const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
|
|
2168
|
+
widened.push(`${toPosix(path5.relative(listRoot, from))}${written2}`);
|
|
2169
|
+
}
|
|
1871
2170
|
if (![...queued].some((region) => isInside(region, directory))) {
|
|
1872
2171
|
queued.add(directory);
|
|
1873
2172
|
regions.push(directory);
|
|
1874
2173
|
}
|
|
1875
2174
|
};
|
|
1876
|
-
const follow = (
|
|
1877
|
-
const fixed = literal.includes("${") ? literal.slice(0, literal.lastIndexOf("/", literal.indexOf("${")) + 1) : literal;
|
|
1878
|
-
if (!fixed.startsWith("../")) {
|
|
1879
|
-
enqueue(listRoot, file);
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
2175
|
+
const follow = (fixed, open, anchored, file, reference) => {
|
|
1882
2176
|
const target = path5.resolve(path5.dirname(file), fixed);
|
|
1883
2177
|
let isDirectory = false;
|
|
1884
2178
|
try {
|
|
1885
2179
|
isDirectory = fs5.statSync(target).isDirectory();
|
|
1886
2180
|
} catch {
|
|
1887
2181
|
}
|
|
2182
|
+
if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
|
|
2183
|
+
files[toPosix(path5.relative(listRoot, target))] = "missing";
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
1888
2186
|
const directory = isDirectory ? target : path5.dirname(target);
|
|
1889
2187
|
if (fs5.existsSync(directory)) {
|
|
1890
|
-
enqueue(directory, file);
|
|
2188
|
+
enqueue(directory, file, reference, anchored && !open);
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
const scan = (text, file) => {
|
|
2192
|
+
const taken = [];
|
|
2193
|
+
for (const match of text.matchAll(FROM_HERE)) {
|
|
2194
|
+
const literals = match[1];
|
|
2195
|
+
const segments = [...literals.matchAll(LITERAL)].map((literal) => literal[2]);
|
|
2196
|
+
if (!segments.some(isUpward)) {
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
const start = (match.index ?? 0) + match[0].indexOf(literals);
|
|
2200
|
+
taken.push([start, start + literals.length]);
|
|
2201
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
2202
|
+
const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
|
|
2203
|
+
const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
|
|
2204
|
+
follow(path5.posix.join(...segments), open, true, file, reference);
|
|
2205
|
+
}
|
|
2206
|
+
for (const match of text.matchAll(UPWARD)) {
|
|
2207
|
+
const at = match.index ?? 0;
|
|
2208
|
+
if (taken.some(([start, end]) => at >= start && at < end)) {
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const literal = match[2];
|
|
2212
|
+
const here = HERE.exec(literal);
|
|
2213
|
+
const relative = here === null ? literal : `.${literal.slice(here[0].length)}`;
|
|
2214
|
+
const open = relative.includes("${");
|
|
2215
|
+
const fixed = open ? relative.slice(0, relative.lastIndexOf("/", relative.indexOf("${")) + 1) : relative;
|
|
2216
|
+
if (here === null && !fixed.startsWith("../") || open && isUpward(relative.slice(relative.indexOf("${")))) {
|
|
2217
|
+
enqueue(listRoot, file, literal);
|
|
2218
|
+
} else {
|
|
2219
|
+
follow(fixed, open, here !== null, file, literal);
|
|
2220
|
+
}
|
|
1891
2221
|
}
|
|
1892
2222
|
};
|
|
1893
2223
|
const add = (relative) => {
|
|
@@ -1919,11 +2249,10 @@ function takeSnapshot(input) {
|
|
|
1919
2249
|
} else {
|
|
1920
2250
|
const content = fs5.readFileSync(file);
|
|
1921
2251
|
files[relative] = hashOf(content);
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
}
|
|
2252
|
+
const document = DOCUMENTS.includes(path5.extname(file).toLowerCase());
|
|
2253
|
+
const loaded = followed === null || !isInside(dir, file) || followed.has(file);
|
|
2254
|
+
if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
|
|
2255
|
+
scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
|
|
1927
2256
|
}
|
|
1928
2257
|
}
|
|
1929
2258
|
};
|
|
@@ -1965,17 +2294,477 @@ function takeSnapshot(input) {
|
|
|
1965
2294
|
environment[name] = version;
|
|
1966
2295
|
}
|
|
1967
2296
|
}
|
|
1968
|
-
return {
|
|
1969
|
-
mode,
|
|
1970
|
-
environment,
|
|
1971
|
-
args: input.args,
|
|
1972
|
-
files
|
|
1973
|
-
};
|
|
2297
|
+
return { snapshot: { mode, environment, args: input.args, files }, root: listRoot, widened };
|
|
1974
2298
|
}
|
|
1975
2299
|
|
|
1976
|
-
// src/commands/mutation
|
|
2300
|
+
// src/commands/mutation.ts
|
|
2301
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2302
|
+
import fs8 from "fs";
|
|
2303
|
+
import path8 from "path";
|
|
2304
|
+
import { pathToFileURL } from "url";
|
|
2305
|
+
|
|
2306
|
+
// src/affected-files.ts
|
|
1977
2307
|
import fs6 from "fs";
|
|
2308
|
+
import { builtinModules as builtinModules2 } from "module";
|
|
1978
2309
|
import path6 from "path";
|
|
2310
|
+
var AffectedError = class extends Error {
|
|
2311
|
+
};
|
|
2312
|
+
var RUNTIME_PATHS = /\b(?:__dirname|__filename|require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:url|dirname|filename|glob|resolve)\b/;
|
|
2313
|
+
var FILE_MODULES = /* @__PURE__ */ new Set([
|
|
2314
|
+
"fs",
|
|
2315
|
+
"fs/promises",
|
|
2316
|
+
"child_process",
|
|
2317
|
+
"module",
|
|
2318
|
+
"worker_threads",
|
|
2319
|
+
"fs-extra",
|
|
2320
|
+
"graceful-fs",
|
|
2321
|
+
"glob",
|
|
2322
|
+
"fast-glob",
|
|
2323
|
+
"globby"
|
|
2324
|
+
]);
|
|
2325
|
+
var bare = (specifier) => specifier.replace(/^node:/, "");
|
|
2326
|
+
function linkedPackage(specifier, from) {
|
|
2327
|
+
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2328
|
+
for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
|
|
2329
|
+
const candidate = path6.join(current, "node_modules", name);
|
|
2330
|
+
if (fs6.existsSync(candidate)) {
|
|
2331
|
+
const target = realpath(candidate);
|
|
2332
|
+
return target.split(path6.sep).includes("node_modules") ? null : target;
|
|
2333
|
+
}
|
|
2334
|
+
if (current === path6.dirname(current)) {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
var toPosix2 = (file) => file.split(path6.sep).join("/");
|
|
2340
|
+
function realpath(file) {
|
|
2341
|
+
try {
|
|
2342
|
+
return fs6.realpathSync.native(file);
|
|
2343
|
+
} catch {
|
|
2344
|
+
return file;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function runnerConfigs(dir, stryker, options) {
|
|
2348
|
+
const files = [path6.resolve(dir, stryker)];
|
|
2349
|
+
const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
|
|
2350
|
+
if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
|
|
2351
|
+
throw new AffectedError(
|
|
2352
|
+
`--affected knows the test runners jest and vitest, the Stryker config uses ${String(options.testRunner)}`
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
if (typeof named === "string") {
|
|
2356
|
+
files.push(path6.resolve(dir, named));
|
|
2357
|
+
} else {
|
|
2358
|
+
const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
|
|
2359
|
+
for (const name of names) {
|
|
2360
|
+
for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
|
|
2361
|
+
const file = path6.join(dir, name + extension);
|
|
2362
|
+
if (fs6.existsSync(file)) {
|
|
2363
|
+
files.push(file);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return { files: files.filter((file) => fs6.existsSync(file)).map(realpath) };
|
|
2369
|
+
}
|
|
2370
|
+
function fileEdges(file, context) {
|
|
2371
|
+
const { dir, aliases, cache } = context;
|
|
2372
|
+
const known = cache.get(file);
|
|
2373
|
+
if (known !== void 0) {
|
|
2374
|
+
return known;
|
|
2375
|
+
}
|
|
2376
|
+
const shown = (one) => toPosix2(path6.relative(dir, one));
|
|
2377
|
+
const target = (specifier, from) => {
|
|
2378
|
+
if (specifier.startsWith(".")) {
|
|
2379
|
+
return resolveFile(path6.resolve(path6.dirname(from), specifier));
|
|
2380
|
+
}
|
|
2381
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2382
|
+
return alias === void 0 ? null : resolveFile(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
|
|
2383
|
+
};
|
|
2384
|
+
const found = { loads: [], unknown: null, computed: false, unnamed: [] };
|
|
2385
|
+
cache.set(file, found);
|
|
2386
|
+
const snapshotFile = path6.join(
|
|
2387
|
+
path6.dirname(file),
|
|
2388
|
+
"__snapshots__",
|
|
2389
|
+
`${path6.basename(file)}.snap`
|
|
2390
|
+
);
|
|
2391
|
+
if (fs6.existsSync(snapshotFile)) {
|
|
2392
|
+
found.loads.push(realpath(snapshotFile));
|
|
2393
|
+
}
|
|
2394
|
+
if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
|
|
2395
|
+
return found;
|
|
2396
|
+
}
|
|
2397
|
+
const source = fs6.readFileSync(file, "utf8");
|
|
2398
|
+
if (COMPONENT.test(file)) {
|
|
2399
|
+
found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
|
|
2400
|
+
}
|
|
2401
|
+
const uncheckable = findUncheckableImports(source)[0];
|
|
2402
|
+
const runtime = RUNTIME_PATHS.exec(source);
|
|
2403
|
+
if (uncheckable !== void 0) {
|
|
2404
|
+
found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
|
|
2405
|
+
found.computed = true;
|
|
2406
|
+
} else if (runtime !== null) {
|
|
2407
|
+
found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
|
|
2408
|
+
}
|
|
2409
|
+
const specifiers = /* @__PURE__ */ new Set([
|
|
2410
|
+
...parseImports(source).map((ref) => ref.module),
|
|
2411
|
+
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
2412
|
+
]);
|
|
2413
|
+
for (const specifier of specifiers) {
|
|
2414
|
+
if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
2415
|
+
const resolved = resolveFile(
|
|
2416
|
+
specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
|
|
2417
|
+
);
|
|
2418
|
+
if (resolved === null) {
|
|
2419
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
|
|
2420
|
+
} else {
|
|
2421
|
+
found.loads.push(realpath(resolved));
|
|
2422
|
+
}
|
|
2423
|
+
} else if (isProjectImport(specifier, aliases)) {
|
|
2424
|
+
const resolved = target(specifier, file);
|
|
2425
|
+
if (resolved === null) {
|
|
2426
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
|
|
2427
|
+
} else {
|
|
2428
|
+
found.loads.push(realpath(resolved));
|
|
2429
|
+
}
|
|
2430
|
+
} else if (FILE_MODULES.has(bare(specifier))) {
|
|
2431
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
|
|
2432
|
+
} else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
|
|
2433
|
+
if (!installed(specifier, file)) {
|
|
2434
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`;
|
|
2435
|
+
found.unnamed.push(specifier);
|
|
2436
|
+
} else if (linkedPackage(specifier, file) !== null) {
|
|
2437
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`;
|
|
2438
|
+
found.computed = true;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
for (const match of source.matchAll(QUOTED)) {
|
|
2443
|
+
const text = match[2];
|
|
2444
|
+
if (text.startsWith(".") && !specifiers.has(text)) {
|
|
2445
|
+
const resolved = resolveFile(path6.resolve(path6.dirname(file), text));
|
|
2446
|
+
if (resolved !== null) {
|
|
2447
|
+
found.loads.push(realpath(resolved));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return found;
|
|
2452
|
+
}
|
|
2453
|
+
function affectedInputs(manifest, input) {
|
|
2454
|
+
const dir = realpath(manifest.dir);
|
|
2455
|
+
const aliases = manifest.settings.importAliases;
|
|
2456
|
+
const placed = [...input.tests, ...Object.values(input.covering).flat()];
|
|
2457
|
+
const absolute = placed.find((file) => path6.isAbsolute(file) || file.startsWith(".."));
|
|
2458
|
+
if (absolute !== void 0) {
|
|
2459
|
+
throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
|
|
2460
|
+
}
|
|
2461
|
+
const keyOf2 = (file) => {
|
|
2462
|
+
const relative = path6.relative(input.root, realpath(file));
|
|
2463
|
+
if (relative.startsWith("..") || path6.isAbsolute(relative)) {
|
|
2464
|
+
throw new AffectedError(`${file} is outside of the files that are compared`);
|
|
2465
|
+
}
|
|
2466
|
+
return toPosix2(relative);
|
|
2467
|
+
};
|
|
2468
|
+
const shown = (file) => toPosix2(path6.relative(dir, file));
|
|
2469
|
+
const edges = /* @__PURE__ */ new Map();
|
|
2470
|
+
const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
|
|
2471
|
+
const closure = (start, strict) => {
|
|
2472
|
+
const seen = /* @__PURE__ */ new Set([start]);
|
|
2473
|
+
const queue = [start];
|
|
2474
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2475
|
+
const found = edgesOf(file);
|
|
2476
|
+
if (strict && found.unknown !== null) {
|
|
2477
|
+
throw new AffectedError(found.unknown);
|
|
2478
|
+
}
|
|
2479
|
+
for (const next of found.loads) {
|
|
2480
|
+
if (!seen.has(next)) {
|
|
2481
|
+
seen.add(next);
|
|
2482
|
+
queue.push(next);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return [...seen];
|
|
2487
|
+
};
|
|
2488
|
+
const isCompared = (file) => {
|
|
2489
|
+
const relative = path6.relative(input.root, realpath(file));
|
|
2490
|
+
return !relative.startsWith("..") && !path6.isAbsolute(relative) && toPosix2(relative) in input.files;
|
|
2491
|
+
};
|
|
2492
|
+
const keysOf = (files) => files.map((file) => {
|
|
2493
|
+
const key = keyOf2(file);
|
|
2494
|
+
if (!(key in input.files)) {
|
|
2495
|
+
throw new AffectedError(
|
|
2496
|
+
`${shown(file)} is loaded by a gate or a spec, but is not among the compared files (does git ignore it?)`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
return key;
|
|
2500
|
+
});
|
|
2501
|
+
const config = /* @__PURE__ */ new Set();
|
|
2502
|
+
const named = [];
|
|
2503
|
+
for (const file of input.configs.files) {
|
|
2504
|
+
for (const loaded of closure(file, false).filter(isCompared)) {
|
|
2505
|
+
config.add(loaded);
|
|
2506
|
+
if (!SCRIPT.test(loaded)) {
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
|
|
2510
|
+
const text = match[2].replace(/^<rootDir>\//, "");
|
|
2511
|
+
const found = resolveFile(path6.resolve(path6.dirname(loaded), text), false) ?? resolveFile(path6.resolve(dir, text), false);
|
|
2512
|
+
if (found !== null && !text.includes("*") && isCompared(found)) {
|
|
2513
|
+
named.push(realpath(found));
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
const gatePaths = new Map(
|
|
2519
|
+
manifest.gates.map((gate) => [realpath(path6.join(dir, gate.path)), gate.path])
|
|
2520
|
+
);
|
|
2521
|
+
const tests = [
|
|
2522
|
+
.../* @__PURE__ */ new Set([
|
|
2523
|
+
...input.tests,
|
|
2524
|
+
...manifest.gates.flatMap((gate) => gate.existingSpecs),
|
|
2525
|
+
...Object.values(input.covering).flat()
|
|
2526
|
+
])
|
|
2527
|
+
];
|
|
2528
|
+
const runAsSuch = new Set(
|
|
2529
|
+
[...tests, ...manifest.gates.map((gate) => gate.path)].map(
|
|
2530
|
+
(file) => realpath(path6.join(dir, file))
|
|
2531
|
+
)
|
|
2532
|
+
);
|
|
2533
|
+
const subprojectKey = toPosix2(path6.relative(input.root, dir));
|
|
2534
|
+
const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
|
|
2535
|
+
for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
|
|
2536
|
+
for (const loaded of closure(file, true)) {
|
|
2537
|
+
const gate = gatePaths.get(loaded);
|
|
2538
|
+
if (gate !== void 0) {
|
|
2539
|
+
throw new AffectedError(
|
|
2540
|
+
`${gate} is loaded through the test runner config for every spec (${shown(file)})`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
config.add(loaded);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const fromSubproject = (file) => path6.join(dir, file);
|
|
2547
|
+
const closures = /* @__PURE__ */ Object.create(null);
|
|
2548
|
+
for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
|
|
2549
|
+
const absolute2 = fromSubproject(file);
|
|
2550
|
+
if (!fs6.existsSync(absolute2)) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), true));
|
|
2554
|
+
}
|
|
2555
|
+
const keyed = (files) => files.map((file) => toPosix2(path6.relative(input.root, path6.join(dir, file))));
|
|
2556
|
+
const dependencies = gateDependencies({
|
|
2557
|
+
gates: manifest.gates.map((gate) => ({
|
|
2558
|
+
key: keyed([gate.path])[0],
|
|
2559
|
+
specs: keyed(gate.existingSpecs)
|
|
2560
|
+
})),
|
|
2561
|
+
tests: keyed(tests),
|
|
2562
|
+
covering: Object.fromEntries(
|
|
2563
|
+
Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
|
|
2564
|
+
),
|
|
2565
|
+
closures
|
|
2566
|
+
});
|
|
2567
|
+
const subproject = toPosix2(path6.relative(input.root, dir));
|
|
2568
|
+
const prefix = subproject === "" ? "" : `${subproject}/`;
|
|
2569
|
+
const gates = /* @__PURE__ */ Object.create(null);
|
|
2570
|
+
for (const gate of manifest.gates) {
|
|
2571
|
+
const key = keyed([gate.path])[0];
|
|
2572
|
+
const found = dependencies[key];
|
|
2573
|
+
gates[gate.path] = {
|
|
2574
|
+
key,
|
|
2575
|
+
specs: found.specs.map(
|
|
2576
|
+
(spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
|
|
2577
|
+
),
|
|
2578
|
+
files: found.files
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
return { subproject, config: keysOf([...config]).sort(), gates };
|
|
2582
|
+
}
|
|
2583
|
+
var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
|
|
2584
|
+
var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
|
|
2585
|
+
var PROJECTS = /\b(?:projects|workspace)\b/;
|
|
2586
|
+
var HELPERS = ["createJestGatesConfig", "createVitestGatesConfig"];
|
|
2587
|
+
function isHelperResult(text) {
|
|
2588
|
+
const names = new Set(HELPERS);
|
|
2589
|
+
for (const match of text.matchAll(
|
|
2590
|
+
/\b(?:createJestGatesConfig|createVitestGatesConfig)\s*(?::|\bas\b)\s*([\w$]+)/g
|
|
2591
|
+
)) {
|
|
2592
|
+
names.add(match[1]);
|
|
2593
|
+
}
|
|
2594
|
+
const escape = (name2) => name2.replace(/\$/g, "\\$");
|
|
2595
|
+
const call = `(?:defineConfig\\s*\\(\\s*)?(?:${[...names].map(escape).join("|")})\\s*\\(`;
|
|
2596
|
+
const exported = "(?:export\\s+default|module\\.exports\\s*=)\\s*";
|
|
2597
|
+
const reached = (name2) => new RegExp(`\\b${escape(name2)}\\s*(?:\\.|\\[)`).test(text);
|
|
2598
|
+
if (new RegExp(`${exported}${call}`).test(text)) {
|
|
2599
|
+
return !reached("module.exports") && !/\bexports\s*(?:\.|\[)/.test(text.replace(/\bmodule\.exports\s*=/g, ""));
|
|
2600
|
+
}
|
|
2601
|
+
const variable = new RegExp(`\\b(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*${call}`).exec(text);
|
|
2602
|
+
if (variable === null) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
const name = escape(variable[1]);
|
|
2606
|
+
const uses = text.match(new RegExp(`(?<![\\w$.])${name}(?![\\w$])`, "g")) ?? [];
|
|
2607
|
+
return uses.length === 2 && new RegExp(`${exported}${name}\\s*(?:;|$)`, "m").test(text);
|
|
2608
|
+
}
|
|
2609
|
+
var READ_BY_NAME = [
|
|
2610
|
+
/^test-gates\.json$/,
|
|
2611
|
+
/^[jt]sconfig(?:\..+)?\.json$/,
|
|
2612
|
+
/^babel\.config\.[cm]?[jt]s(?:on)?$/,
|
|
2613
|
+
/^\.babelrc(?:\.[cm]?js(?:on)?)?$/,
|
|
2614
|
+
/^\.swcrc$/
|
|
2615
|
+
];
|
|
2616
|
+
function loadedScope(manifest, configs, listed2) {
|
|
2617
|
+
const dir = realpath(manifest.dir);
|
|
2618
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2619
|
+
const context = { dir, aliases: manifest.settings.importAliases, cache };
|
|
2620
|
+
const runner = configs.files.slice(1);
|
|
2621
|
+
const texts = runner.map((file) => withoutComments(fs6.readFileSync(file, "utf8")));
|
|
2622
|
+
if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
|
|
2623
|
+
return { every: NOT_THE_HELPER2 };
|
|
2624
|
+
}
|
|
2625
|
+
if (!texts.every(isHelperResult)) {
|
|
2626
|
+
return {
|
|
2627
|
+
every: "what the runner config exports is not what the helper returns (merged with another config, or changed after the call)"
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
const parts = new Set(runner.map(realpath));
|
|
2631
|
+
for (const file of [...parts]) {
|
|
2632
|
+
if (SCRIPT.test(file)) {
|
|
2633
|
+
fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
if ([...parts].some((file) => PROJECTS.test(withoutComments(fs6.readFileSync(file, "utf8"))))) {
|
|
2637
|
+
return {
|
|
2638
|
+
every: "the runner config, or a file it is put together from, names projects or a workspace"
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
const named = fs6.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path6.join(dir, name));
|
|
2642
|
+
const mocks = path6.join(dir, "__mocks__");
|
|
2643
|
+
const seeds = [
|
|
2644
|
+
...configs.files,
|
|
2645
|
+
...named,
|
|
2646
|
+
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path6.join(dir, file)),
|
|
2647
|
+
...listed2.filter((file) => file.startsWith(`${mocks}${path6.sep}`))
|
|
2648
|
+
].filter((file) => fs6.existsSync(file)).map(realpath);
|
|
2649
|
+
const naming = new Set(
|
|
2650
|
+
[...configs.files, ...named].filter((file) => fs6.existsSync(file)).map(realpath)
|
|
2651
|
+
);
|
|
2652
|
+
const seen = new Set(seeds);
|
|
2653
|
+
const queue = [...seeds];
|
|
2654
|
+
const add = (file) => {
|
|
2655
|
+
const real = realpath(file);
|
|
2656
|
+
if (!seen.has(real)) {
|
|
2657
|
+
seen.add(real);
|
|
2658
|
+
queue.push(real);
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2662
|
+
if (fs6.statSync(file).isDirectory()) {
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
const edges = fileEdges(file, context);
|
|
2666
|
+
if (edges.computed) {
|
|
2667
|
+
return { every: edges.unknown ?? "a file loads modules that are not followed" };
|
|
2668
|
+
}
|
|
2669
|
+
edges.loads.forEach(add);
|
|
2670
|
+
for (const specifier of edges.unnamed) {
|
|
2671
|
+
endingAs(specifier, listed2).forEach(add);
|
|
2672
|
+
}
|
|
2673
|
+
const mock = path6.join(path6.dirname(file), "__mocks__", path6.basename(file));
|
|
2674
|
+
if (fs6.existsSync(mock)) {
|
|
2675
|
+
add(mock);
|
|
2676
|
+
}
|
|
2677
|
+
if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
|
|
2678
|
+
for (const named2 of namedIn(file)) {
|
|
2679
|
+
const text = named2.replace(/^<rootDir>\//, "");
|
|
2680
|
+
const found = resolveFile(path6.resolve(path6.dirname(file), text), false) ?? resolveFile(path6.resolve(dir, text), false);
|
|
2681
|
+
if (found !== null && !text.includes("*")) {
|
|
2682
|
+
if (!SCRIPT.test(found)) {
|
|
2683
|
+
naming.add(realpath(found));
|
|
2684
|
+
}
|
|
2685
|
+
add(found);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return { files: seen };
|
|
2691
|
+
}
|
|
2692
|
+
function endingAs(specifier, files) {
|
|
2693
|
+
const parts = specifier.split("/");
|
|
2694
|
+
const rest = (parts.length > 1 ? parts.slice(1).join("/") : specifier.replace(/^[@#~$]+/, "")).replace(/\.[cm]?[jt]sx?$/, "");
|
|
2695
|
+
if (rest === "") {
|
|
2696
|
+
return [];
|
|
2697
|
+
}
|
|
2698
|
+
return files.filter((file) => {
|
|
2699
|
+
const bare2 = toPosix2(file).replace(/\.[^./]+$/, "");
|
|
2700
|
+
return [bare2, bare2.replace(/\/index$/, "")].some((one) => one.endsWith(`/${rest}`));
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
function namedIn(file) {
|
|
2704
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
2705
|
+
if (SCRIPT.test(file)) {
|
|
2706
|
+
return [...text.matchAll(QUOTED)].map((match) => match[2]);
|
|
2707
|
+
}
|
|
2708
|
+
return ["extends", "plugins", "presets"].flatMap((key) => valuesOf(text, key));
|
|
2709
|
+
}
|
|
2710
|
+
function valuesOf(text, key) {
|
|
2711
|
+
const found = [];
|
|
2712
|
+
for (const match of text.matchAll(new RegExp(`"${key}"\\s*:\\s*`, "g"))) {
|
|
2713
|
+
let at = (match.index ?? 0) + match[0].length;
|
|
2714
|
+
if (text[at] === '"') {
|
|
2715
|
+
const end = text.indexOf('"', at + 1);
|
|
2716
|
+
found.push(text.slice(at + 1, end === -1 ? void 0 : end));
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
if (text[at] !== "[") {
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
let depth = 0;
|
|
2723
|
+
for (; at < text.length; at += 1) {
|
|
2724
|
+
const char = text[at];
|
|
2725
|
+
if (char === '"') {
|
|
2726
|
+
const end = text.indexOf('"', at + 1);
|
|
2727
|
+
found.push(text.slice(at + 1, end === -1 ? void 0 : end));
|
|
2728
|
+
at = end === -1 ? text.length : end;
|
|
2729
|
+
} else if (char === "[") {
|
|
2730
|
+
depth += 1;
|
|
2731
|
+
} else if (char === "]") {
|
|
2732
|
+
depth -= 1;
|
|
2733
|
+
if (depth === 0) {
|
|
2734
|
+
break;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return found;
|
|
2740
|
+
}
|
|
2741
|
+
function withoutComments(text) {
|
|
2742
|
+
let out = "";
|
|
2743
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
2744
|
+
const char = text[at];
|
|
2745
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2746
|
+
let end = at + 1;
|
|
2747
|
+
while (end < text.length && text[end] !== char && text[end] !== "\n") {
|
|
2748
|
+
end += text[end] === "\\" ? 2 : 1;
|
|
2749
|
+
}
|
|
2750
|
+
out += text.slice(at, end + 1);
|
|
2751
|
+
at = end;
|
|
2752
|
+
} else if (char === "/" && text[at + 1] === "/") {
|
|
2753
|
+
const end = text.indexOf("\n", at);
|
|
2754
|
+
at = end === -1 ? text.length : end - 1;
|
|
2755
|
+
} else if (char === "/" && text[at + 1] === "*") {
|
|
2756
|
+
const end = text.indexOf("*/", at + 2);
|
|
2757
|
+
at = end === -1 ? text.length : end + 1;
|
|
2758
|
+
} else {
|
|
2759
|
+
out += char;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
return out;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/commands/mutation-result.ts
|
|
2766
|
+
import fs7 from "fs";
|
|
2767
|
+
import path7 from "path";
|
|
1979
2768
|
|
|
1980
2769
|
// src/core/mutation-result.ts
|
|
1981
2770
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -1984,7 +2773,7 @@ var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
|
|
|
1984
2773
|
function normalizeCode(text) {
|
|
1985
2774
|
return text.replace(/\s+/g, " ").trim();
|
|
1986
2775
|
}
|
|
1987
|
-
function
|
|
2776
|
+
function normalizePath(file) {
|
|
1988
2777
|
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1989
2778
|
}
|
|
1990
2779
|
function normalizeNewlines(text) {
|
|
@@ -2050,10 +2839,10 @@ function judgeMutationReport(input) {
|
|
|
2050
2839
|
};
|
|
2051
2840
|
const files = /* @__PURE__ */ new Map();
|
|
2052
2841
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
2053
|
-
files.set(
|
|
2842
|
+
files.set(normalizePath(file), result);
|
|
2054
2843
|
}
|
|
2055
2844
|
for (const gate of input.gates) {
|
|
2056
|
-
const gatePath =
|
|
2845
|
+
const gatePath = normalizePath(gate.path);
|
|
2057
2846
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
2058
2847
|
violations.push(...shapeViolations);
|
|
2059
2848
|
violations.push(
|
|
@@ -2225,17 +3014,17 @@ function formatSummary(summary) {
|
|
|
2225
3014
|
|
|
2226
3015
|
// src/commands/mutation-result.ts
|
|
2227
3016
|
function reportPathOf(manifest, override) {
|
|
2228
|
-
return
|
|
3017
|
+
return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
|
|
2229
3018
|
}
|
|
2230
|
-
function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
3019
|
+
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
|
|
2231
3020
|
const manifest = readManifest(dir);
|
|
2232
3021
|
const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
|
|
2233
3022
|
const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
|
|
2234
3023
|
const reportPath = reportPathOf(manifest, reportOverride);
|
|
2235
|
-
const shown =
|
|
3024
|
+
const shown = path7.relative(manifest.dir, reportPath) || reportPath;
|
|
2236
3025
|
let report;
|
|
2237
3026
|
try {
|
|
2238
|
-
report = JSON.parse(
|
|
3027
|
+
report = JSON.parse(fs7.readFileSync(reportPath, "utf8"));
|
|
2239
3028
|
} catch (error) {
|
|
2240
3029
|
io2.err(
|
|
2241
3030
|
`test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
|
|
@@ -2252,12 +3041,13 @@ function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
|
2252
3041
|
maxTimeouts: manifest.settings.maxTimeouts,
|
|
2253
3042
|
readSource: (gatePath) => {
|
|
2254
3043
|
try {
|
|
2255
|
-
return
|
|
3044
|
+
return fs7.readFileSync(path7.join(manifest.dir, gatePath), "utf8");
|
|
2256
3045
|
} catch {
|
|
2257
3046
|
return void 0;
|
|
2258
3047
|
}
|
|
2259
3048
|
}
|
|
2260
3049
|
});
|
|
3050
|
+
seen({ report, verdict });
|
|
2261
3051
|
const summary = formatSummary(verdict.summary);
|
|
2262
3052
|
if (verdict.violations.length === 0 && verdict.survivors.length === 0) {
|
|
2263
3053
|
io2.out(`test-gates mutation: OK (${summary})${caveat}`);
|
|
@@ -2319,15 +3109,15 @@ function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
|
2319
3109
|
// src/commands/mutation.ts
|
|
2320
3110
|
var CONFIG_MARK = "\0test-gates-stryker-config\0";
|
|
2321
3111
|
function loadStrykerConfig(manifest, config) {
|
|
2322
|
-
const file =
|
|
3112
|
+
const file = path8.resolve(manifest.dir, config);
|
|
2323
3113
|
if (file.endsWith(".json")) {
|
|
2324
3114
|
try {
|
|
2325
|
-
return JSON.parse(
|
|
3115
|
+
return JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
2326
3116
|
} catch {
|
|
2327
3117
|
return void 0;
|
|
2328
3118
|
}
|
|
2329
3119
|
}
|
|
2330
|
-
const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
3120
|
+
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,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
2331
3121
|
const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
|
|
2332
3122
|
cwd: manifest.dir,
|
|
2333
3123
|
encoding: "utf8",
|
|
@@ -2341,7 +3131,7 @@ function loadStrykerConfig(manifest, config) {
|
|
|
2341
3131
|
}
|
|
2342
3132
|
function modifiedAt(file) {
|
|
2343
3133
|
try {
|
|
2344
|
-
const stat =
|
|
3134
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
2345
3135
|
return `${stat.mtimeNs} ${stat.ino}`;
|
|
2346
3136
|
} catch {
|
|
2347
3137
|
return null;
|
|
@@ -2350,145 +3140,612 @@ function modifiedAt(file) {
|
|
|
2350
3140
|
function guardRecordPath(incrementalFile) {
|
|
2351
3141
|
return `${incrementalFile}.test-gates.json`;
|
|
2352
3142
|
}
|
|
2353
|
-
function
|
|
2354
|
-
const
|
|
3143
|
+
function coveringSpecs(report, gates) {
|
|
3144
|
+
const fileOf = /* @__PURE__ */ new Map();
|
|
3145
|
+
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
3146
|
+
for (const test of entry.tests ?? []) {
|
|
3147
|
+
fileOf.set(test.id, file);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
const covering = /* @__PURE__ */ Object.create(null);
|
|
3151
|
+
for (const gate of gates) {
|
|
3152
|
+
const specs = /* @__PURE__ */ new Set();
|
|
3153
|
+
let placed = true;
|
|
3154
|
+
const key = withoutDots(gate);
|
|
3155
|
+
for (const mutant of report.files?.[key]?.mutants ?? []) {
|
|
3156
|
+
for (const id of [...mutant.coveredBy ?? [], ...mutant.killedBy ?? []]) {
|
|
3157
|
+
const file = fileOf.get(id);
|
|
3158
|
+
if (file === void 0) {
|
|
3159
|
+
placed = false;
|
|
3160
|
+
} else {
|
|
3161
|
+
specs.add(file);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
covering[gate] = placed ? [...specs].sort() : null;
|
|
3166
|
+
}
|
|
3167
|
+
return covering;
|
|
3168
|
+
}
|
|
3169
|
+
function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
2355
3170
|
const loaded = loadStrykerConfig(manifest, config);
|
|
2356
3171
|
const options = incrementalOptions(loaded, strykerArgs);
|
|
3172
|
+
const base = fs8.realpathSync(manifest.dir);
|
|
3173
|
+
const configs = () => runnerConfigs(base, config, loaded ?? {});
|
|
3174
|
+
const decision = {
|
|
3175
|
+
incremental: false,
|
|
3176
|
+
incrementalFile: null,
|
|
3177
|
+
recordFile: null,
|
|
3178
|
+
incrementalHash: null,
|
|
3179
|
+
forced: options.force,
|
|
3180
|
+
snapshot: null,
|
|
3181
|
+
failure: null,
|
|
3182
|
+
root: base,
|
|
3183
|
+
widened: [],
|
|
3184
|
+
scope: null,
|
|
3185
|
+
force: null,
|
|
3186
|
+
plan: null,
|
|
3187
|
+
last: void 0,
|
|
3188
|
+
ci: affected ? affectedRefused(process.env) : null,
|
|
3189
|
+
configs
|
|
3190
|
+
};
|
|
2357
3191
|
if (loaded !== void 0 && !options.incremental) {
|
|
2358
|
-
return
|
|
3192
|
+
return decision;
|
|
2359
3193
|
}
|
|
2360
|
-
|
|
2361
|
-
const incrementalFile =
|
|
3194
|
+
decision.incremental = true;
|
|
3195
|
+
const incrementalFile = path8.resolve(base, options.incrementalFile);
|
|
2362
3196
|
const recordFile = guardRecordPath(incrementalFile);
|
|
3197
|
+
decision.incrementalFile = incrementalFile;
|
|
3198
|
+
decision.recordFile = recordFile;
|
|
2363
3199
|
let snapshot = null;
|
|
3200
|
+
let root = base;
|
|
2364
3201
|
let failure = "the Stryker config could not be loaded to see where the incremental file is";
|
|
2365
3202
|
if (loaded !== void 0) {
|
|
3203
|
+
const scope = (listed2) => {
|
|
3204
|
+
try {
|
|
3205
|
+
const found = loadedScope(manifest, configs(), listed2);
|
|
3206
|
+
decision.scope = "files" in found ? { files: found.files.size } : found;
|
|
3207
|
+
return "files" in found ? found.files : null;
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
decision.scope = { every: error.message };
|
|
3210
|
+
return null;
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
2366
3213
|
try {
|
|
2367
|
-
|
|
3214
|
+
let widened;
|
|
3215
|
+
({ snapshot, root, widened } = snapshotOf({
|
|
3216
|
+
scope,
|
|
2368
3217
|
dir: manifest.dir,
|
|
2369
3218
|
written: [
|
|
2370
3219
|
incrementalFile,
|
|
2371
3220
|
recordFile,
|
|
2372
|
-
|
|
2373
|
-
|
|
3221
|
+
path8.resolve(base, options.tempDirName),
|
|
3222
|
+
path8.resolve(base, manifest.settings.strykerReportFile)
|
|
2374
3223
|
],
|
|
2375
3224
|
args: comparableArgs(strykerArgs)
|
|
2376
|
-
});
|
|
3225
|
+
}));
|
|
3226
|
+
decision.widened = widened;
|
|
2377
3227
|
} catch (error) {
|
|
2378
3228
|
failure = `the files the run depends on could not be listed (${error.message})`;
|
|
2379
3229
|
}
|
|
2380
3230
|
}
|
|
2381
|
-
|
|
3231
|
+
decision.snapshot = snapshot;
|
|
3232
|
+
decision.failure = snapshot === null ? failure : null;
|
|
3233
|
+
decision.root = root;
|
|
2382
3234
|
const incremental = hashFile(incrementalFile);
|
|
3235
|
+
decision.incrementalHash = incremental;
|
|
2383
3236
|
if (part) {
|
|
2384
|
-
|
|
2385
|
-
return
|
|
2386
|
-
force: incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null,
|
|
2387
|
-
record: () => void 0
|
|
2388
|
-
};
|
|
3237
|
+
decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
|
|
3238
|
+
return decision;
|
|
2389
3239
|
}
|
|
3240
|
+
let stored;
|
|
3241
|
+
try {
|
|
3242
|
+
stored = JSON.parse(fs8.readFileSync(recordFile, "utf8"));
|
|
3243
|
+
} catch {
|
|
3244
|
+
stored = void 0;
|
|
3245
|
+
}
|
|
3246
|
+
const ci = decision.ci;
|
|
3247
|
+
let force = null;
|
|
3248
|
+
let plan = null;
|
|
3249
|
+
let last;
|
|
2390
3250
|
if (incremental !== null && !options.force) {
|
|
2391
3251
|
if (snapshot === null) {
|
|
2392
3252
|
force = failure;
|
|
3253
|
+
} else if (!affected) {
|
|
3254
|
+
force = refuseReuse({ record: stored, incremental, snapshot });
|
|
3255
|
+
} else if (ci !== null) {
|
|
3256
|
+
force = ci;
|
|
2393
3257
|
} else {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
}
|
|
2398
|
-
|
|
3258
|
+
const read2 = readRecord({ record: stored, incremental, snapshot });
|
|
3259
|
+
if ("refused" in read2) {
|
|
3260
|
+
force = read2.refused;
|
|
3261
|
+
} else {
|
|
3262
|
+
last = read2.record.affected;
|
|
3263
|
+
try {
|
|
3264
|
+
const inputs = affectedInputs(manifest, {
|
|
3265
|
+
root,
|
|
3266
|
+
files: snapshot.files,
|
|
3267
|
+
configs: configs(),
|
|
3268
|
+
tests: last?.tests ?? [],
|
|
3269
|
+
covering: Object.fromEntries(
|
|
3270
|
+
Object.entries(last?.gates ?? {}).map(([gate, entry]) => [gate, entry.specs])
|
|
3271
|
+
)
|
|
3272
|
+
});
|
|
3273
|
+
plan = planAffected({
|
|
3274
|
+
before: { files: read2.record.snapshot.files, affected: last },
|
|
3275
|
+
now: snapshot.files,
|
|
3276
|
+
subproject: inputs.subproject,
|
|
3277
|
+
config: inputs.config,
|
|
3278
|
+
gates: manifest.gates.map((gate) => ({
|
|
3279
|
+
path: gate.path,
|
|
3280
|
+
files: inputs.gates[gate.path]?.files ?? []
|
|
3281
|
+
}))
|
|
3282
|
+
});
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
if (!(error instanceof AffectedError)) {
|
|
3285
|
+
throw error;
|
|
3286
|
+
}
|
|
3287
|
+
plan = { full: `what a gate depends on cannot be told: ${error.message}` };
|
|
3288
|
+
}
|
|
3289
|
+
if ("full" in plan) {
|
|
3290
|
+
force = plan.full;
|
|
3291
|
+
}
|
|
2399
3292
|
}
|
|
2400
|
-
force = refuseReuse({ record, incremental, snapshot });
|
|
2401
3293
|
}
|
|
3294
|
+
} else if (affected && incremental === null && !options.force) {
|
|
3295
|
+
force = "there is no incremental file of an earlier run";
|
|
3296
|
+
}
|
|
3297
|
+
decision.force = force;
|
|
3298
|
+
decision.plan = plan;
|
|
3299
|
+
decision.last = last;
|
|
3300
|
+
return decision;
|
|
3301
|
+
}
|
|
3302
|
+
function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
3303
|
+
const nothing = { force: null, mutate: null, said: [], record: () => void 0 };
|
|
3304
|
+
const decision = decideReuse(manifest, config, strykerArgs, part, affected);
|
|
3305
|
+
if (!decision.incremental) {
|
|
3306
|
+
return affected ? {
|
|
3307
|
+
...nothing,
|
|
3308
|
+
said: [
|
|
3309
|
+
"test-gates mutation --affected: Stryker does not run in incremental mode, so every mutant is run"
|
|
3310
|
+
]
|
|
3311
|
+
} : nothing;
|
|
3312
|
+
}
|
|
3313
|
+
const { snapshot, root, plan, last, ci, configs, force } = decision;
|
|
3314
|
+
const incrementalFile = decision.incrementalFile;
|
|
3315
|
+
const recordFile = decision.recordFile;
|
|
3316
|
+
const incremental = decision.incrementalHash;
|
|
3317
|
+
const options = { force: decision.forced };
|
|
3318
|
+
if (part) {
|
|
3319
|
+
fs8.rmSync(recordFile, { force: true });
|
|
3320
|
+
return { ...nothing, force };
|
|
2402
3321
|
}
|
|
2403
|
-
|
|
3322
|
+
fs8.rmSync(recordFile, { force: true });
|
|
2404
3323
|
const before = modifiedAt(incrementalFile);
|
|
3324
|
+
const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
|
|
3325
|
+
const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
|
|
3326
|
+
let ranges = [];
|
|
3327
|
+
if (everyGate === null && incremental !== null && !options.force) {
|
|
3328
|
+
const kept = rerun === null ? manifest.gates.map((gate) => gate.path) : manifest.gates.map((gate) => gate.path).filter((gate) => !rerun.includes(gate));
|
|
3329
|
+
try {
|
|
3330
|
+
ranges = timeoutRanges(
|
|
3331
|
+
JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
|
|
3332
|
+
kept
|
|
3333
|
+
);
|
|
3334
|
+
} catch {
|
|
3335
|
+
ranges = [];
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
|
|
3339
|
+
let said = [];
|
|
3340
|
+
if (affected && !options.force) {
|
|
3341
|
+
if (force !== null) {
|
|
3342
|
+
said = describePlan({ full: force });
|
|
3343
|
+
} else if (plan !== null) {
|
|
3344
|
+
said = describePlan(plan, 5, ranges.length);
|
|
3345
|
+
}
|
|
3346
|
+
} else if (ranges.length > 0) {
|
|
3347
|
+
said = [
|
|
3348
|
+
`test-gates mutation: ${ranges.length} mutant(s) that timed out in the last run are run again, the other results are reused`
|
|
3349
|
+
];
|
|
3350
|
+
}
|
|
2405
3351
|
return {
|
|
2406
|
-
|
|
2407
|
-
|
|
3352
|
+
// Every gate is run again: the same as a run of every mutant.
|
|
3353
|
+
force: everyGate,
|
|
3354
|
+
mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
|
|
3355
|
+
said,
|
|
3356
|
+
record: (judged) => {
|
|
2408
3357
|
const written = hashFile(incrementalFile);
|
|
2409
3358
|
if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
|
|
2410
3359
|
return;
|
|
2411
3360
|
}
|
|
2412
3361
|
const record = { format: RECORD_FORMAT, incremental: written, snapshot };
|
|
3362
|
+
if (affected && ci === null && judged !== null) {
|
|
3363
|
+
record.affected = affectedRecord(judged, rerun, last);
|
|
3364
|
+
}
|
|
2413
3365
|
try {
|
|
2414
|
-
|
|
3366
|
+
fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
|
|
2415
3367
|
`);
|
|
2416
3368
|
} catch {
|
|
2417
3369
|
}
|
|
2418
3370
|
}
|
|
2419
|
-
};
|
|
3371
|
+
};
|
|
3372
|
+
function affectedRecord(judged, ran, previous) {
|
|
3373
|
+
const gates = manifest.gates.map((gate) => gate.path);
|
|
3374
|
+
const dirty = new Set(
|
|
3375
|
+
[...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
|
|
3376
|
+
);
|
|
3377
|
+
const fresh = coveringSpecs(judged.report, gates);
|
|
3378
|
+
const covering = /* @__PURE__ */ Object.create(null);
|
|
3379
|
+
for (const gate of gates) {
|
|
3380
|
+
const specs = ran === null || ran.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
|
|
3381
|
+
if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
|
|
3382
|
+
covering[gate] = specs;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
try {
|
|
3386
|
+
const inputs = affectedInputs(manifest, {
|
|
3387
|
+
root,
|
|
3388
|
+
files: snapshot.files,
|
|
3389
|
+
configs: configs(),
|
|
3390
|
+
tests: Object.keys(judged.report.testFiles ?? {}),
|
|
3391
|
+
covering
|
|
3392
|
+
});
|
|
3393
|
+
const recorded = /* @__PURE__ */ Object.create(null);
|
|
3394
|
+
for (const gate of Object.keys(covering)) {
|
|
3395
|
+
const entry = inputs.gates[gate];
|
|
3396
|
+
if (entry !== void 0) {
|
|
3397
|
+
recorded[gate] = { specs: entry.specs, files: entry.files };
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
|
|
3401
|
+
} catch (error) {
|
|
3402
|
+
if (error instanceof AffectedError) {
|
|
3403
|
+
return void 0;
|
|
3404
|
+
}
|
|
3405
|
+
throw error;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
3410
|
+
const part = only.length > 0;
|
|
3411
|
+
if (affected && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
3412
|
+
throw new UsageError(
|
|
3413
|
+
"--affected and --mutate cannot be combined: --affected sets --mutate itself"
|
|
3414
|
+
);
|
|
3415
|
+
}
|
|
3416
|
+
if (part) {
|
|
3417
|
+
const refused = gateRunRefused(process.env);
|
|
3418
|
+
if (refused !== null) {
|
|
3419
|
+
throw new UsageError(refused);
|
|
3420
|
+
}
|
|
3421
|
+
if (strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
3422
|
+
throw new UsageError("--gate and --mutate cannot be combined: --gate sets --mutate itself");
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
const manifest = readManifest(dir);
|
|
3426
|
+
const chosen = selectControls({
|
|
3427
|
+
gates: manifest.gates.map((gate) => gate.path),
|
|
3428
|
+
mode: "all",
|
|
3429
|
+
only,
|
|
3430
|
+
shard: null
|
|
3431
|
+
});
|
|
3432
|
+
if ("error" in chosen) {
|
|
3433
|
+
throw new UsageError(chosen.error);
|
|
3434
|
+
}
|
|
3435
|
+
const config = findStrykerConfig(manifest);
|
|
3436
|
+
if (config === null) {
|
|
3437
|
+
throw new ManifestError(
|
|
3438
|
+
"no Stryker config found (stryker.gates.config.mjs). Create one, or set settings.stryker.configFile"
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
const stryker = findLocalBin(manifest.dir, "stryker");
|
|
3442
|
+
if (stryker === null) {
|
|
3443
|
+
throw new ManifestError(
|
|
3444
|
+
"stryker is not installed in this project (node_modules/.bin/stryker). Add @stryker-mutator/core as a devDependency"
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
const reportFile = reportPathOf(manifest, null);
|
|
3448
|
+
fs8.rmSync(reportFile, { force: true });
|
|
3449
|
+
const guard = guardIncremental(manifest, config, strykerArgs, part, affected);
|
|
3450
|
+
if (guard.said.length > 0) {
|
|
3451
|
+
guard.said.forEach((line) => io2.out(line));
|
|
3452
|
+
} else if (guard.force !== null) {
|
|
3453
|
+
io2.out(
|
|
3454
|
+
`test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
|
|
3458
|
+
const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
|
|
3459
|
+
const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
|
|
3460
|
+
cwd: manifest.dir,
|
|
3461
|
+
stdio: "inherit",
|
|
3462
|
+
shell: process.platform === "win32"
|
|
3463
|
+
});
|
|
3464
|
+
if (result.error) {
|
|
3465
|
+
io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
|
|
3466
|
+
return 1;
|
|
3467
|
+
}
|
|
3468
|
+
if (result.status !== 0) {
|
|
3469
|
+
io2.err(
|
|
3470
|
+
`test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
|
|
3471
|
+
);
|
|
3472
|
+
return 1;
|
|
3473
|
+
}
|
|
3474
|
+
let judged = null;
|
|
3475
|
+
const status = runMutationResult(dir, null, io2, part ? chosen.selected : [], (seen) => {
|
|
3476
|
+
judged = seen;
|
|
3477
|
+
});
|
|
3478
|
+
guard.record(judged);
|
|
3479
|
+
if (part) {
|
|
3480
|
+
fs8.rmSync(reportFile, { force: true });
|
|
3481
|
+
}
|
|
3482
|
+
return status;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/commands/doctor.ts
|
|
3486
|
+
function read(file) {
|
|
3487
|
+
try {
|
|
3488
|
+
return fs9.readFileSync(file, "utf8");
|
|
3489
|
+
} catch {
|
|
3490
|
+
return null;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function git2(dir, args) {
|
|
3494
|
+
const result = spawnSync3("git", args, {
|
|
3495
|
+
cwd: dir,
|
|
3496
|
+
env: gitEnvironment(process.env),
|
|
3497
|
+
encoding: "utf8"
|
|
3498
|
+
});
|
|
3499
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
3500
|
+
}
|
|
3501
|
+
function hooksOf(dir) {
|
|
3502
|
+
const root = git2(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
|
|
3503
|
+
const hooksDir = git2(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
|
|
3504
|
+
const candidates = [
|
|
3505
|
+
...["pre-push", "pre-commit"].map((name) => path9.join(root, ".husky", name)),
|
|
3506
|
+
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path9.join(hooksDir, name)),
|
|
3507
|
+
...["lefthook.yml", ".lefthook.yml"].map((name) => path9.join(root, name))
|
|
3508
|
+
];
|
|
3509
|
+
let script = "";
|
|
3510
|
+
try {
|
|
3511
|
+
const manifest = JSON.parse(read(path9.join(dir, "package.json")) ?? "{}");
|
|
3512
|
+
script = manifest.scripts?.["test:gates:mutation"] ?? "";
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3515
|
+
const subproject = path9.relative(fs9.realpathSync(root), dir).split(path9.sep).join("/");
|
|
3516
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3517
|
+
const uses = [];
|
|
3518
|
+
for (const file of candidates) {
|
|
3519
|
+
const text = read(file);
|
|
3520
|
+
if (text === null) {
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
const real = fs9.realpathSync(file);
|
|
3524
|
+
if (seen.has(real)) {
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
seen.add(real);
|
|
3528
|
+
const shown = path9.relative(root, file).split(path9.sep).join("/");
|
|
3529
|
+
uses.push(...hookUses({ file: shown, text, subproject, script }));
|
|
3530
|
+
}
|
|
3531
|
+
return uses;
|
|
3532
|
+
}
|
|
3533
|
+
function runDoctor(dir, io2) {
|
|
3534
|
+
const manifest = readManifest(dir);
|
|
3535
|
+
const config = findStrykerConfig(manifest);
|
|
3536
|
+
if (config === null) {
|
|
3537
|
+
throw new ManifestError(
|
|
3538
|
+
"no Stryker config found (stryker.gates.config.mjs). Create one, or set settings.stryker.configFile"
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
const plain2 = decideReuse(manifest, config, [], false, false);
|
|
3542
|
+
const affected = decideReuse(manifest, config, [], false, true);
|
|
3543
|
+
io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
|
|
3544
|
+
if (plain2.snapshot !== null) {
|
|
3545
|
+
const subproject = path9.relative(plain2.root, fs9.realpathSync(manifest.dir));
|
|
3546
|
+
const prefix = subproject === "" ? "" : `${subproject.split(path9.sep).join("/")}/`;
|
|
3547
|
+
const keys = Object.keys(plain2.snapshot.files);
|
|
3548
|
+
const outside = keys.filter((key) => !key.startsWith(prefix)).length;
|
|
3549
|
+
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";
|
|
3550
|
+
io2.out(` compared: ${keys.length} files, ${outside} outside the subproject${followed}`);
|
|
3551
|
+
}
|
|
3552
|
+
if (plain2.incremental) {
|
|
3553
|
+
const reuse = plain2.forced ? "the Stryker config sets force" : plain2.force ?? (plain2.incrementalHash === null ? "there is no incremental file of an earlier run" : null);
|
|
3554
|
+
io2.out(
|
|
3555
|
+
reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
|
|
3556
|
+
);
|
|
3557
|
+
if (affected.ci !== null) {
|
|
3558
|
+
io2.out(` next run with --affected: every mutant, ${affected.ci}`);
|
|
3559
|
+
} else if (affected.plan !== null && "rerun" in affected.plan) {
|
|
3560
|
+
const [first = "", ...rest] = describePlan(affected.plan);
|
|
3561
|
+
io2.out(
|
|
3562
|
+
` next run with --affected: ${first.replace(/^test-gates mutation --affected: /, "")}`
|
|
3563
|
+
);
|
|
3564
|
+
rest.forEach((line) => io2.out(` ${line}`));
|
|
3565
|
+
} else {
|
|
3566
|
+
const why = affected.forced ? "the Stryker config sets force" : affected.force;
|
|
3567
|
+
io2.out(` next run with --affected: every mutant, ${why ?? "there is nothing to reuse"}`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const { lines, status } = formatFindings(
|
|
3571
|
+
diagnose({
|
|
3572
|
+
incremental: plain2.incremental,
|
|
3573
|
+
forced: plain2.forced,
|
|
3574
|
+
failure: plain2.failure,
|
|
3575
|
+
widened: plain2.widened,
|
|
3576
|
+
scope: plain2.scope,
|
|
3577
|
+
hooks: hooksOf(fs9.realpathSync(manifest.dir))
|
|
3578
|
+
})
|
|
3579
|
+
);
|
|
3580
|
+
lines.forEach((line) => io2.out(line));
|
|
3581
|
+
return status;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/commands/lcov.ts
|
|
3585
|
+
import fs10 from "fs";
|
|
3586
|
+
import path10 from "path";
|
|
3587
|
+
|
|
3588
|
+
// src/core/lcov.ts
|
|
3589
|
+
function normalizePath2(file, rootDir) {
|
|
3590
|
+
let normalized = file.replace(/\\/g, "/");
|
|
3591
|
+
if (rootDir !== void 0) {
|
|
3592
|
+
const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3593
|
+
if (normalized.startsWith(`${root}/`)) {
|
|
3594
|
+
normalized = normalized.slice(root.length + 1);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return normalized.replace(/^\.\//, "");
|
|
3598
|
+
}
|
|
3599
|
+
function parseLcov(text, rootDir) {
|
|
3600
|
+
const files = /* @__PURE__ */ new Map();
|
|
3601
|
+
let record = null;
|
|
3602
|
+
const closeRecord = () => {
|
|
3603
|
+
if (record && !record.hasDa) {
|
|
3604
|
+
record.file.found += record.found;
|
|
3605
|
+
record.file.hit += record.hit;
|
|
3606
|
+
}
|
|
3607
|
+
record = null;
|
|
3608
|
+
};
|
|
3609
|
+
for (const rawLine of text.split("\n")) {
|
|
3610
|
+
const line = rawLine.trim();
|
|
3611
|
+
if (line.startsWith("SF:")) {
|
|
3612
|
+
closeRecord();
|
|
3613
|
+
const path13 = normalizePath2(line.slice(3), rootDir);
|
|
3614
|
+
const file = files.get(path13) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
3615
|
+
files.set(path13, file);
|
|
3616
|
+
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
3617
|
+
} else if (line === "end_of_record") {
|
|
3618
|
+
closeRecord();
|
|
3619
|
+
} else if (record) {
|
|
3620
|
+
const open = record;
|
|
3621
|
+
if (line.startsWith("DA:")) {
|
|
3622
|
+
const [lineNumber, hits] = line.slice(3).split(",");
|
|
3623
|
+
const number = Number(lineNumber);
|
|
3624
|
+
const count = Number(hits);
|
|
3625
|
+
if (Number.isInteger(number) && Number.isFinite(count)) {
|
|
3626
|
+
open.hasDa = true;
|
|
3627
|
+
open.file.lines.set(number, Math.max(open.file.lines.get(number) ?? 0, count));
|
|
3628
|
+
}
|
|
3629
|
+
} else if (line.startsWith("LF:")) {
|
|
3630
|
+
open.found = Number(line.slice(3)) || 0;
|
|
3631
|
+
} else if (line.startsWith("LH:")) {
|
|
3632
|
+
open.hit = Number(line.slice(3)) || 0;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
closeRecord();
|
|
3637
|
+
const result = /* @__PURE__ */ new Map();
|
|
3638
|
+
for (const [file, accumulator] of files) {
|
|
3639
|
+
const uncovered = [...accumulator.lines.entries()].filter(([, hits]) => hits <= 0).map(([lineNumber]) => lineNumber).sort((a, b) => a - b);
|
|
3640
|
+
result.set(file, {
|
|
3641
|
+
found: accumulator.lines.size + accumulator.found,
|
|
3642
|
+
hit: accumulator.lines.size - uncovered.length + accumulator.hit,
|
|
3643
|
+
uncovered
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
return result;
|
|
2420
3647
|
}
|
|
2421
|
-
function
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3648
|
+
function judgeLcov(gatePaths, coverage) {
|
|
3649
|
+
const violations = [];
|
|
3650
|
+
const results = [];
|
|
3651
|
+
for (const gatePath of gatePaths) {
|
|
3652
|
+
const file = coverage.get(normalizePath2(gatePath));
|
|
3653
|
+
if (!file) {
|
|
3654
|
+
violations.push({ file: gatePath, message: "no record in the lcov file" });
|
|
3655
|
+
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
3656
|
+
continue;
|
|
2427
3657
|
}
|
|
2428
|
-
if (
|
|
2429
|
-
|
|
3658
|
+
if (file.found === 0) {
|
|
3659
|
+
violations.push({ file: gatePath, message: "no instrumented lines in the lcov record" });
|
|
3660
|
+
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
3661
|
+
continue;
|
|
2430
3662
|
}
|
|
3663
|
+
const ok = file.hit === file.found;
|
|
3664
|
+
if (!ok) {
|
|
3665
|
+
const shown = file.uncovered.slice(0, 20).join(", ");
|
|
3666
|
+
const more = file.uncovered.length > 20 ? ", \u2026" : "";
|
|
3667
|
+
violations.push({
|
|
3668
|
+
file: gatePath,
|
|
3669
|
+
...file.uncovered[0] === void 0 ? {} : { line: file.uncovered[0] },
|
|
3670
|
+
message: `${file.hit}/${file.found} lines covered` + (shown === "" ? "" : ` (uncovered: ${shown}${more})`)
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
results.push({ path: gatePath, found: file.found, hit: file.hit, ok });
|
|
2431
3674
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
if (config === null) {
|
|
2444
|
-
throw new ManifestError(
|
|
2445
|
-
"no Stryker config found (stryker.gates.config.mjs). Create one, or set settings.stryker.configFile"
|
|
2446
|
-
);
|
|
3675
|
+
return { violations, results };
|
|
3676
|
+
}
|
|
3677
|
+
function summarizeLcov(coverage, exclude) {
|
|
3678
|
+
let found = 0;
|
|
3679
|
+
let hit = 0;
|
|
3680
|
+
for (const [file, lines] of coverage) {
|
|
3681
|
+
if (exclude.some((regex) => regex.test(file))) {
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
found += lines.found;
|
|
3685
|
+
hit += lines.hit;
|
|
2447
3686
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
3687
|
+
return { found, hit };
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// src/commands/lcov.ts
|
|
3691
|
+
function runLcov(dir, fileOption, io2) {
|
|
3692
|
+
const manifest = readManifest(dir);
|
|
3693
|
+
const file = fileOption ?? manifest.settings.lcovFile;
|
|
3694
|
+
if (file === null) {
|
|
2450
3695
|
throw new ManifestError(
|
|
2451
|
-
"
|
|
3696
|
+
"no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
|
|
2452
3697
|
);
|
|
2453
3698
|
}
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
3699
|
+
const lcovPath = path10.resolve(manifest.dir, file);
|
|
3700
|
+
let text;
|
|
3701
|
+
try {
|
|
3702
|
+
text = fs10.readFileSync(lcovPath, "utf8");
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
io2.err(
|
|
3705
|
+
`test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
|
|
2460
3706
|
);
|
|
3707
|
+
return 1;
|
|
2461
3708
|
}
|
|
2462
|
-
const
|
|
2463
|
-
const
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3709
|
+
const violations = [...manifest.violations];
|
|
3710
|
+
const listing = /* @__PURE__ */ new Map();
|
|
3711
|
+
const existing = manifest.gates.filter((gate) => {
|
|
3712
|
+
const exists = existsExact(manifest.dir, gate.path, "file", listing);
|
|
3713
|
+
if (!exists) {
|
|
3714
|
+
violations.push({
|
|
3715
|
+
file: gate.path,
|
|
3716
|
+
message: "file not found (the check is case-sensitive). Update test-gates.json if it moved"
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
return exists;
|
|
2468
3720
|
});
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3721
|
+
const coverage = parseLcov(text, manifest.dir);
|
|
3722
|
+
const total = summarizeLcov(coverage, manifest.settings.lcovSummaryExclude);
|
|
3723
|
+
io2.out(
|
|
3724
|
+
total.found === 0 ? "reference: no instrumented lines in the lcov file" : `reference: overall line coverage ${(total.hit / total.found * 100).toFixed(2)}% (${total.hit}/${total.found}, not gated)`
|
|
3725
|
+
);
|
|
3726
|
+
const verdict = judgeLcov(
|
|
3727
|
+
existing.map((gate) => gate.path),
|
|
3728
|
+
coverage
|
|
3729
|
+
);
|
|
3730
|
+
for (const result of verdict.results) {
|
|
3731
|
+
if (result.ok) {
|
|
3732
|
+
io2.out(` \u2713 ${result.path}: ${result.hit}/${result.found} lines`);
|
|
3733
|
+
}
|
|
2472
3734
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
);
|
|
3735
|
+
violations.push(...verdict.violations);
|
|
3736
|
+
if (violations.length > 0) {
|
|
3737
|
+
printViolations(io2, "lcov", violations);
|
|
2477
3738
|
return 1;
|
|
2478
3739
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
if (part) {
|
|
2482
|
-
fs7.rmSync(reportFile, { force: true });
|
|
2483
|
-
}
|
|
2484
|
-
return status;
|
|
3740
|
+
io2.out(`test-gates lcov: OK (${verdict.results.length} gate(s) at 100% line coverage)`);
|
|
3741
|
+
return 0;
|
|
2485
3742
|
}
|
|
2486
3743
|
|
|
2487
3744
|
// src/commands/selfcheck.ts
|
|
2488
|
-
import { spawn, spawnSync as
|
|
2489
|
-
import
|
|
3745
|
+
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
3746
|
+
import fs12 from "fs";
|
|
2490
3747
|
import os2 from "os";
|
|
2491
|
-
import
|
|
3748
|
+
import path12 from "path";
|
|
2492
3749
|
|
|
2493
3750
|
// src/core/jobs.ts
|
|
2494
3751
|
var SWITCHES = {
|
|
@@ -2557,7 +3814,8 @@ var REACHES = [
|
|
|
2557
3814
|
"eval(",
|
|
2558
3815
|
"Function("
|
|
2559
3816
|
];
|
|
2560
|
-
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])/;
|
|
3817
|
+
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
|
|
3818
|
+
var COMPUTED = "loads a module whose name is computed";
|
|
2561
3819
|
var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
|
|
2562
3820
|
function reachesOutside(file, trusted) {
|
|
2563
3821
|
const dense = file.source.replace(/\s/g, "");
|
|
@@ -2566,7 +3824,7 @@ function reachesOutside(file, trusted) {
|
|
|
2566
3824
|
return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
|
|
2567
3825
|
}
|
|
2568
3826
|
if (COMPUTED_LOAD.test(dense)) {
|
|
2569
|
-
return
|
|
3827
|
+
return COMPUTED;
|
|
2570
3828
|
}
|
|
2571
3829
|
for (const { specifier, builtin } of file.imports) {
|
|
2572
3830
|
if (builtin) {
|
|
@@ -2590,17 +3848,47 @@ function jobsRefused(input) {
|
|
|
2590
3848
|
if (input.configSource === null || !input.configSource.includes(helper)) {
|
|
2591
3849
|
return `the gate config does not use ${helper}`;
|
|
2592
3850
|
}
|
|
2593
|
-
if (input.lost !== null) {
|
|
2594
|
-
return `${input.lost} cannot be followed, so what the gate run loads is not known`;
|
|
2595
|
-
}
|
|
2596
|
-
for (const file of input.files) {
|
|
2597
|
-
const reach = reachesOutside(file, input.trusted);
|
|
2598
|
-
if (reach !== null) {
|
|
2599
|
-
return `${file.path} ${reach}, and may meet what another run is doing`;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
3851
|
return null;
|
|
2603
3852
|
}
|
|
3853
|
+
function jobsPlan(input) {
|
|
3854
|
+
const byPath = new Map(input.files.map((file) => [file.path, file]));
|
|
3855
|
+
const causes = input.roots.flatMap((root) => {
|
|
3856
|
+
const loaded = root.files.flatMap((name) => {
|
|
3857
|
+
const file = byPath.get(name);
|
|
3858
|
+
return file === void 0 ? [] : [file];
|
|
3859
|
+
});
|
|
3860
|
+
let computed = false;
|
|
3861
|
+
const reaches = loaded.flatMap((file) => {
|
|
3862
|
+
const reach = reachesOutside(file, input.trusted);
|
|
3863
|
+
const said = reach === null ? [] : [`${file.path} ${reach}`];
|
|
3864
|
+
if (!COMPUTED_LOAD.test(file.source.replace(/\s/g, ""))) {
|
|
3865
|
+
return said;
|
|
3866
|
+
}
|
|
3867
|
+
computed = true;
|
|
3868
|
+
return reach === COMPUTED ? said : [...said, `${file.path} ${COMPUTED}`];
|
|
3869
|
+
});
|
|
3870
|
+
const reasons = [
|
|
3871
|
+
...root.lost === null ? [] : [`${root.lost} cannot be followed`],
|
|
3872
|
+
...reaches
|
|
3873
|
+
];
|
|
3874
|
+
if (reasons.length === 0) {
|
|
3875
|
+
return [];
|
|
3876
|
+
}
|
|
3877
|
+
const any = root.spec === null || root.lost !== null || computed;
|
|
3878
|
+
const gates = input.gates.filter(
|
|
3879
|
+
(gate) => gate.spec !== root.spec && (any || root.files.includes(withoutDots(gate.path)))
|
|
3880
|
+
).map((gate) => gate.path);
|
|
3881
|
+
const by = root.spec === null ? "run with what the gate config names" : `${any ? "maybe loaded" : "loaded"} by ${root.spec}`;
|
|
3882
|
+
return gates.length === 0 ? [] : [{ gates, reason: `${by}, in which ${reasons.join("; ")}` }];
|
|
3883
|
+
});
|
|
3884
|
+
const kept = new Set(causes.flatMap((cause) => cause.gates));
|
|
3885
|
+
const paths = input.gates.map((gate) => gate.path);
|
|
3886
|
+
return {
|
|
3887
|
+
atOnce: paths.filter((gate) => !kept.has(gate)),
|
|
3888
|
+
alone: paths.filter((gate) => kept.has(gate)),
|
|
3889
|
+
causes
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
2604
3892
|
var TROUBLE = [
|
|
2605
3893
|
"fail",
|
|
2606
3894
|
"fails",
|
|
@@ -2674,8 +3962,8 @@ function cleanHold(input) {
|
|
|
2674
3962
|
|
|
2675
3963
|
// src/touch.ts
|
|
2676
3964
|
import { createHash as createHash2 } from "crypto";
|
|
2677
|
-
import
|
|
2678
|
-
import
|
|
3965
|
+
import fs11 from "fs";
|
|
3966
|
+
import path11 from "path";
|
|
2679
3967
|
import { fileURLToPath } from "url";
|
|
2680
3968
|
var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
|
|
2681
3969
|
function isObject(value) {
|
|
@@ -2707,18 +3995,18 @@ function isCounters(value) {
|
|
|
2707
3995
|
);
|
|
2708
3996
|
}
|
|
2709
3997
|
function readNotes(directory) {
|
|
2710
|
-
const
|
|
3998
|
+
const read2 = (file) => {
|
|
2711
3999
|
try {
|
|
2712
|
-
return JSON.parse(
|
|
4000
|
+
return JSON.parse(fs11.readFileSync(path11.join(directory, file), "utf8"));
|
|
2713
4001
|
} catch {
|
|
2714
4002
|
return null;
|
|
2715
4003
|
}
|
|
2716
4004
|
};
|
|
2717
|
-
const run =
|
|
4005
|
+
const run = read2("run.json");
|
|
2718
4006
|
let notes = [];
|
|
2719
4007
|
try {
|
|
2720
|
-
for (const name of
|
|
2721
|
-
const note =
|
|
4008
|
+
for (const name of fs11.readdirSync(path11.join(directory, "specs"))) {
|
|
4009
|
+
const note = read2(path11.join("specs", name));
|
|
2722
4010
|
if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
|
|
2723
4011
|
throw new Error("not a note");
|
|
2724
4012
|
}
|
|
@@ -2730,135 +4018,8 @@ function readNotes(directory) {
|
|
|
2730
4018
|
return {
|
|
2731
4019
|
run: isObject(run) && typeof run.runner === "string" && typeof run.version === "string" && typeof run.provider === "string" && typeof run.pool === "string" && typeof run.isolate === "boolean" && typeof run.projects === "number" && (run.problem === null || typeof run.problem === "string") ? run : null,
|
|
2732
4020
|
notes,
|
|
2733
|
-
totals: countersOfAll(
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
// src/commands/loaded-files.ts
|
|
2738
|
-
import fs9 from "fs";
|
|
2739
|
-
import { builtinModules } from "module";
|
|
2740
|
-
import path9 from "path";
|
|
2741
|
-
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
2742
|
-
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2743
|
-
var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
2744
|
-
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
2745
|
-
function isFile(file) {
|
|
2746
|
-
try {
|
|
2747
|
-
return fs9.statSync(file).isFile();
|
|
2748
|
-
} catch {
|
|
2749
|
-
return false;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
function resolveFile(base, directory = true) {
|
|
2753
|
-
const candidates = [
|
|
2754
|
-
base,
|
|
2755
|
-
...EXTENSIONS.map((extension) => base + extension),
|
|
2756
|
-
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
2757
|
-
...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
2758
|
-
...directory ? EXTENSIONS.map((extension) => path9.join(base, `index${extension}`)) : []
|
|
2759
|
-
];
|
|
2760
|
-
return candidates.find(isFile) ?? null;
|
|
2761
|
-
}
|
|
2762
|
-
function installed(specifier, from) {
|
|
2763
|
-
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2764
|
-
for (let current = path9.dirname(from); ; current = path9.dirname(current)) {
|
|
2765
|
-
if (fs9.existsSync(path9.join(current, "node_modules", name))) {
|
|
2766
|
-
return true;
|
|
2767
|
-
}
|
|
2768
|
-
if (current === path9.dirname(current)) {
|
|
2769
|
-
return false;
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
function loadedFiles(manifest, configFile) {
|
|
2774
|
-
const aliases = manifest.settings.importAliases;
|
|
2775
|
-
const relative = (file) => path9.relative(manifest.dir, file).split(path9.sep).join("/");
|
|
2776
|
-
const target = (specifier, from) => {
|
|
2777
|
-
if (specifier.startsWith(".")) {
|
|
2778
|
-
return resolveFile(path9.resolve(path9.dirname(from), specifier));
|
|
2779
|
-
}
|
|
2780
|
-
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2781
|
-
return alias === void 0 ? null : resolveFile(
|
|
2782
|
-
path9.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
2783
|
-
);
|
|
2784
|
-
};
|
|
2785
|
-
const read = (file) => {
|
|
2786
|
-
const source = fs9.readFileSync(file, "utf8");
|
|
2787
|
-
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
2788
|
-
const specifiers = [
|
|
2789
|
-
.../* @__PURE__ */ new Set([
|
|
2790
|
-
...imports.map((ref) => ref.module),
|
|
2791
|
-
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
2792
|
-
])
|
|
2793
|
-
];
|
|
2794
|
-
const strings = [...source.matchAll(QUOTED)].map((match) => match[2]).filter((text) => !specifiers.includes(text));
|
|
2795
|
-
return { source, specifiers, strings };
|
|
2796
|
-
};
|
|
2797
|
-
const named = /* @__PURE__ */ new Set();
|
|
2798
|
-
const configs = configFile === null ? [] : [path9.resolve(manifest.dir, configFile)];
|
|
2799
|
-
const seenConfigs = new Set(configs);
|
|
2800
|
-
const entry = configs[0];
|
|
2801
|
-
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
2802
|
-
const { specifiers, strings } = read(file);
|
|
2803
|
-
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
2804
|
-
for (const text of paths) {
|
|
2805
|
-
const stripped = text.replace(/^<rootDir>\//, "");
|
|
2806
|
-
const found = resolveFile(path9.resolve(path9.dirname(file), stripped), false) ?? resolveFile(path9.resolve(manifest.dir, stripped), false);
|
|
2807
|
-
if (found !== null && SCRIPT.test(found)) {
|
|
2808
|
-
named.add(found);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
|
|
2812
|
-
const found = target(specifier, file);
|
|
2813
|
-
if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
|
|
2814
|
-
seenConfigs.add(found);
|
|
2815
|
-
configs.push(found);
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
const queue = [
|
|
2820
|
-
...manifest.gates.flatMap(
|
|
2821
|
-
(gate) => gate.existingSpecs.map((spec) => path9.join(manifest.dir, spec))
|
|
2822
|
-
),
|
|
2823
|
-
...named
|
|
2824
|
-
];
|
|
2825
|
-
const seen = new Set(queue);
|
|
2826
|
-
const follow = (found) => {
|
|
2827
|
-
if (SCRIPT.test(found) && !seen.has(found)) {
|
|
2828
|
-
seen.add(found);
|
|
2829
|
-
queue.push(found);
|
|
2830
|
-
}
|
|
4021
|
+
totals: countersOfAll(read2(path11.join("coverage", "coverage-final.json")))
|
|
2831
4022
|
};
|
|
2832
|
-
const files = [];
|
|
2833
|
-
let lost = null;
|
|
2834
|
-
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2835
|
-
const { source, specifiers, strings } = read(file);
|
|
2836
|
-
const imports = [];
|
|
2837
|
-
for (const specifier of specifiers) {
|
|
2838
|
-
if (isProjectImport(specifier, aliases)) {
|
|
2839
|
-
const found = target(specifier, file);
|
|
2840
|
-
if (found === null) {
|
|
2841
|
-
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
2842
|
-
} else {
|
|
2843
|
-
follow(found);
|
|
2844
|
-
}
|
|
2845
|
-
} else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
|
|
2846
|
-
imports.push({ specifier, builtin: true });
|
|
2847
|
-
} else if (installed(specifier, file)) {
|
|
2848
|
-
imports.push({ specifier, builtin: false });
|
|
2849
|
-
} else {
|
|
2850
|
-
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
2854
|
-
const found = resolveFile(path9.resolve(path9.dirname(file), text));
|
|
2855
|
-
if (found !== null) {
|
|
2856
|
-
follow(found);
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
files.push({ path: relative(file), source, imports });
|
|
2860
|
-
}
|
|
2861
|
-
return { files, lost };
|
|
2862
4023
|
}
|
|
2863
4024
|
|
|
2864
4025
|
// src/commands/selfcheck.ts
|
|
@@ -2872,7 +4033,7 @@ function resolveGateCommand(manifest) {
|
|
|
2872
4033
|
if (manifest.settings.gateCommand !== null) {
|
|
2873
4034
|
return manifest.settings.gateCommand;
|
|
2874
4035
|
}
|
|
2875
|
-
const exists = (name) =>
|
|
4036
|
+
const exists = (name) => fs12.existsSync(path12.join(manifest.dir, name));
|
|
2876
4037
|
const jest = JEST_CONFIGS.find(exists);
|
|
2877
4038
|
const vitest = VITEST_CONFIGS.find(exists);
|
|
2878
4039
|
if (jest && vitest) {
|
|
@@ -2893,7 +4054,7 @@ function resolveGateCommand(manifest) {
|
|
|
2893
4054
|
var TOUCH_PREFIX = ".test-gates-touch-";
|
|
2894
4055
|
function realPath(file) {
|
|
2895
4056
|
try {
|
|
2896
|
-
return
|
|
4057
|
+
return fs12.realpathSync(file);
|
|
2897
4058
|
} catch {
|
|
2898
4059
|
return file;
|
|
2899
4060
|
}
|
|
@@ -2901,13 +4062,13 @@ function realPath(file) {
|
|
|
2901
4062
|
function shareOneRun(manifest, targets, runGate) {
|
|
2902
4063
|
let touchDir;
|
|
2903
4064
|
try {
|
|
2904
|
-
for (const name of
|
|
2905
|
-
const left =
|
|
2906
|
-
if (name.startsWith(TOUCH_PREFIX) && Date.now() -
|
|
2907
|
-
|
|
4065
|
+
for (const name of fs12.readdirSync(manifest.dir)) {
|
|
4066
|
+
const left = path12.join(manifest.dir, name);
|
|
4067
|
+
if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
|
|
4068
|
+
fs12.rmSync(left, { recursive: true, force: true });
|
|
2908
4069
|
}
|
|
2909
4070
|
}
|
|
2910
|
-
touchDir =
|
|
4071
|
+
touchDir = fs12.mkdtempSync(path12.join(manifest.dir, TOUCH_PREFIX));
|
|
2911
4072
|
} catch {
|
|
2912
4073
|
return { reason: "the subproject cannot be written to" };
|
|
2913
4074
|
}
|
|
@@ -2918,14 +4079,14 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2918
4079
|
return { reason: "the gate run with every spec does not pass" };
|
|
2919
4080
|
}
|
|
2920
4081
|
const base = realPath(manifest.dir);
|
|
2921
|
-
const absolute = (file) => realPath(
|
|
4082
|
+
const absolute = (file) => realPath(path12.resolve(base, file));
|
|
2922
4083
|
const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
|
|
2923
4084
|
const left = readNotes(touchDir);
|
|
2924
4085
|
plan = planControls({
|
|
2925
4086
|
all: manifest.gates.map((gate) => ({
|
|
2926
4087
|
path: gate.path,
|
|
2927
|
-
file:
|
|
2928
|
-
specFile:
|
|
4088
|
+
file: path12.join(base, gate.path),
|
|
4089
|
+
specFile: path12.join(base, gate.spec ?? "")
|
|
2929
4090
|
})),
|
|
2930
4091
|
targets: targets.map((gate) => gate.path),
|
|
2931
4092
|
run: left.run,
|
|
@@ -2934,7 +4095,7 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2934
4095
|
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
2935
4096
|
});
|
|
2936
4097
|
} finally {
|
|
2937
|
-
|
|
4098
|
+
fs12.rmSync(touchDir, { recursive: true, force: true });
|
|
2938
4099
|
}
|
|
2939
4100
|
if (plan.reason !== null) {
|
|
2940
4101
|
return { reason: plan.reason };
|
|
@@ -2944,15 +4105,15 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2944
4105
|
return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
|
|
2945
4106
|
}
|
|
2946
4107
|
function installedVersion2(start, name) {
|
|
2947
|
-
let current =
|
|
4108
|
+
let current = path12.resolve(start);
|
|
2948
4109
|
for (; ; ) {
|
|
2949
4110
|
try {
|
|
2950
4111
|
const manifest = JSON.parse(
|
|
2951
|
-
|
|
4112
|
+
fs12.readFileSync(path12.join(current, "node_modules", name, "package.json"), "utf8")
|
|
2952
4113
|
);
|
|
2953
4114
|
return typeof manifest.version === "string" ? manifest.version : null;
|
|
2954
4115
|
} catch {
|
|
2955
|
-
const parent =
|
|
4116
|
+
const parent = path12.dirname(current);
|
|
2956
4117
|
if (parent === current) {
|
|
2957
4118
|
return null;
|
|
2958
4119
|
}
|
|
@@ -2981,13 +4142,13 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
2981
4142
|
const results = /* @__PURE__ */ new Map();
|
|
2982
4143
|
let root;
|
|
2983
4144
|
try {
|
|
2984
|
-
for (const name of
|
|
2985
|
-
const left =
|
|
2986
|
-
if (name.startsWith(JOBS_PREFIX) && Date.now() -
|
|
2987
|
-
|
|
4145
|
+
for (const name of fs12.readdirSync(os2.tmpdir())) {
|
|
4146
|
+
const left = path12.join(os2.tmpdir(), name);
|
|
4147
|
+
if (name.startsWith(JOBS_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
|
|
4148
|
+
fs12.rmSync(left, { recursive: true, force: true });
|
|
2988
4149
|
}
|
|
2989
4150
|
}
|
|
2990
|
-
root =
|
|
4151
|
+
root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
2991
4152
|
} catch {
|
|
2992
4153
|
return results;
|
|
2993
4154
|
}
|
|
@@ -3019,11 +4180,11 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
3019
4180
|
await Promise.all(
|
|
3020
4181
|
Array.from(
|
|
3021
4182
|
{ length: Math.min(jobs, gates.length) },
|
|
3022
|
-
(_unused, index) => line(
|
|
4183
|
+
(_unused, index) => line(path12.join(root, `line-${index}`))
|
|
3023
4184
|
)
|
|
3024
4185
|
);
|
|
3025
4186
|
} finally {
|
|
3026
|
-
|
|
4187
|
+
fs12.rmSync(root, { recursive: true, force: true });
|
|
3027
4188
|
}
|
|
3028
4189
|
return results;
|
|
3029
4190
|
}
|
|
@@ -3052,7 +4213,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
3052
4213
|
const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
|
|
3053
4214
|
const [program, ...args] = command;
|
|
3054
4215
|
const executable = findLocalBin(manifest.dir, program) ?? program;
|
|
3055
|
-
const runGate = (env) =>
|
|
4216
|
+
const runGate = (env) => spawnSync4(executable, args, {
|
|
3056
4217
|
cwd: manifest.dir,
|
|
3057
4218
|
env: {
|
|
3058
4219
|
...process.env,
|
|
@@ -3098,48 +4259,60 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
3098
4259
|
const jobs = selection.jobs ?? 1;
|
|
3099
4260
|
if (jobs > 1 && waiting.length > 1) {
|
|
3100
4261
|
const known = knownCommand(program, args);
|
|
3101
|
-
const
|
|
3102
|
-
const ask = (loaded) => jobsRefused({
|
|
4262
|
+
const refusal = jobsRefused({
|
|
3103
4263
|
failurePattern: manifest.settings.selfcheckFailurePattern,
|
|
3104
4264
|
command: known,
|
|
3105
|
-
configSource: gateConfigSource(manifest, args)
|
|
3106
|
-
...loaded,
|
|
3107
|
-
trusted: manifest.settings.importMode === "allowlist" ? manifest.settings.importAllow.flatMap(
|
|
3108
|
-
(rule) => rule.module === void 0 ? [] : [rule.module]
|
|
3109
|
-
) : []
|
|
4265
|
+
configSource: gateConfigSource(manifest, args)
|
|
3110
4266
|
});
|
|
3111
|
-
const refusal = ask({ files: [], lost: null }) ?? ask(loadedFiles(manifest, configFile));
|
|
3112
4267
|
if (refusal !== null || !("runner" in known)) {
|
|
3113
4268
|
io2.out(`test-gates selfcheck: one negative control at a time (${refusal})`);
|
|
3114
4269
|
} else {
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
manifest
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
const output = `${result.stdout}${result.stderr}`;
|
|
3124
|
-
const clean = cleanHold({
|
|
3125
|
-
held: judgeSelfcheck({ status: result.status, output, gatePath: gate.path }) === null,
|
|
3126
|
-
signal: result.signal,
|
|
3127
|
-
output,
|
|
3128
|
-
runner: known.runner,
|
|
3129
|
-
specs,
|
|
3130
|
-
gatePath: gate.path,
|
|
3131
|
-
others: manifest.gates.map((other) => other.path).filter((other) => other !== gate.path),
|
|
3132
|
-
names
|
|
3133
|
-
});
|
|
3134
|
-
if (clean) {
|
|
3135
|
-
heldAtOnce.set(gate.path, result.status);
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
const again = waiting.length - heldAtOnce.size;
|
|
3139
|
-
if (again > 0) {
|
|
4270
|
+
const plan = jobsPlan({
|
|
4271
|
+
gates: waiting.map((gate) => ({ path: gate.path, spec: gate.spec })),
|
|
4272
|
+
...loadedFiles(manifest, gateConfigFile(args)),
|
|
4273
|
+
trusted: manifest.settings.importMode === "allowlist" ? manifest.settings.importAllow.flatMap(
|
|
4274
|
+
(rule) => rule.module === void 0 ? [] : [rule.module]
|
|
4275
|
+
) : []
|
|
4276
|
+
});
|
|
4277
|
+
if (plan.alone.length > 0) {
|
|
3140
4278
|
io2.out(
|
|
3141
|
-
`test-gates selfcheck: ${
|
|
4279
|
+
`test-gates selfcheck: ${plan.alone.length} of ${waiting.length} negative control(s) run one at a time, since in their runs something that loads the gate also loads a file that may meet what another run is doing:`
|
|
3142
4280
|
);
|
|
4281
|
+
for (const { gates, reason } of plan.causes) {
|
|
4282
|
+
io2.out(` ${gates.join(", ")}: ${reason}`);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
const atOnce = waiting.filter((gate) => plan.atOnce.includes(gate.path));
|
|
4286
|
+
if (atOnce.length > 1) {
|
|
4287
|
+
const specs = new Set(manifest.gates.map((gate) => gate.spec)).size - 1;
|
|
4288
|
+
const names = [
|
|
4289
|
+
manifest.dir,
|
|
4290
|
+
realPath(manifest.dir),
|
|
4291
|
+
...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
|
|
4292
|
+
].sort((left, right) => right.length - left.length);
|
|
4293
|
+
const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
|
|
4294
|
+
for (const [gate, result] of results) {
|
|
4295
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
4296
|
+
const clean = cleanHold({
|
|
4297
|
+
held: judgeSelfcheck({ status: result.status, output, gatePath: gate.path }) === null,
|
|
4298
|
+
signal: result.signal,
|
|
4299
|
+
output,
|
|
4300
|
+
runner: known.runner,
|
|
4301
|
+
specs,
|
|
4302
|
+
gatePath: gate.path,
|
|
4303
|
+
others: manifest.gates.map((other) => other.path).filter((other) => other !== gate.path),
|
|
4304
|
+
names
|
|
4305
|
+
});
|
|
4306
|
+
if (clean) {
|
|
4307
|
+
heldAtOnce.set(gate.path, result.status);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
const again = atOnce.length - heldAtOnce.size;
|
|
4311
|
+
if (again > 0) {
|
|
4312
|
+
io2.out(
|
|
4313
|
+
`test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
3143
4316
|
}
|
|
3144
4317
|
}
|
|
3145
4318
|
}
|
|
@@ -3210,6 +4383,8 @@ Commands:
|
|
|
3210
4383
|
mutation-result Judge an existing Stryker JSON report
|
|
3211
4384
|
selfcheck [-- <command>] Negative control: leave one spec out, the gate must fail
|
|
3212
4385
|
lcov [--file <lcov.info>] Per-file 100% line coverage from an lcov tracefile
|
|
4386
|
+
doctor Why a mutation run would not reuse the last results, and what
|
|
4387
|
+
to change. Runs nothing; exit 1 when runs are full ones needlessly
|
|
3213
4388
|
|
|
3214
4389
|
Options:
|
|
3215
4390
|
--dir <subproject> Directory that holds test-gates.json (default: .)
|
|
@@ -3218,6 +4393,9 @@ Options:
|
|
|
3218
4393
|
--gate <path> selfcheck: only this gate (may be given more than once).
|
|
3219
4394
|
mutation: only the mutants of this gate, while working on it.
|
|
3220
4395
|
Not the final check, and refused when CI is set
|
|
4396
|
+
--affected mutation: run again only the gates that a change since the last
|
|
4397
|
+
run can affect, and reuse the results of the others. Every
|
|
4398
|
+
mutant is run when CI is set
|
|
3221
4399
|
--shard <index>/<total> selfcheck: one of <total> parts of the gates, for parallel CI jobs
|
|
3222
4400
|
--jobs <n> selfcheck: run <n> negative controls at the same time. One that does
|
|
3223
4401
|
not hold cleanly there is run again on its own
|
|
@@ -3250,8 +4428,10 @@ function main(argv) {
|
|
|
3250
4428
|
switch (args.command) {
|
|
3251
4429
|
case "check":
|
|
3252
4430
|
return runCheck(args.dir, io);
|
|
4431
|
+
case "doctor":
|
|
4432
|
+
return runDoctor(args.dir, io);
|
|
3253
4433
|
case "mutation":
|
|
3254
|
-
return runMutation(args.dir, args.rest, io, args.gates);
|
|
4434
|
+
return runMutation(args.dir, args.rest, io, args.gates, args.affected);
|
|
3255
4435
|
case "mutation-result":
|
|
3256
4436
|
return runMutationResult(args.dir, args.report, io);
|
|
3257
4437
|
case "selfcheck":
|