@t09tanaka/test-gates 0.3.4 → 0.4.1
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 +33 -0
- package/README.md +103 -24
- package/dist/{chunk-XMZ6NBV7.js → chunk-AK2NTFQO.js} +5 -0
- package/dist/cli.js +1733 -515
- 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,239 @@ 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.blind !== null) {
|
|
1701
|
+
findings.push({
|
|
1702
|
+
level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
1703
|
+
text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
|
|
1704
|
+
fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
if (facts.scope !== null && "every" in facts.scope) {
|
|
1708
|
+
findings.push({
|
|
1709
|
+
level: "note",
|
|
1710
|
+
text: `the paths in every file of the subproject are followed, also in files the run does not load: ${facts.scope.every}`,
|
|
1711
|
+
fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
if (facts.hooks.length === 0) {
|
|
1715
|
+
findings.push({
|
|
1716
|
+
level: "note",
|
|
1717
|
+
text: "no git hook runs test-gates mutation for this subproject",
|
|
1718
|
+
fix: null
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
|
|
1722
|
+
findings.push({
|
|
1723
|
+
level: "problem",
|
|
1724
|
+
text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
|
|
1725
|
+
fix: "pass --affected in the hook (npm run test:gates:mutation -- --affected); keep the run in CI without it"
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
return findings;
|
|
1729
|
+
}
|
|
1730
|
+
function formatFindings(findings) {
|
|
1731
|
+
const lines = [];
|
|
1732
|
+
for (const finding of findings) {
|
|
1733
|
+
lines.push(`${finding.level === "problem" ? "problem" : "note"}: ${finding.text}`);
|
|
1734
|
+
if (finding.fix !== null) {
|
|
1735
|
+
lines.push(` fix: ${finding.fix}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const problems = findings.filter((finding) => finding.level === "problem").length;
|
|
1739
|
+
lines.push(
|
|
1740
|
+
problems === 0 ? "test-gates doctor: OK" : `test-gates doctor: ${problems} problem(s) that make runs full ones`
|
|
1741
|
+
);
|
|
1742
|
+
return { lines, status: problems === 0 ? 0 : 1 };
|
|
1743
|
+
}
|
|
1744
|
+
var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
|
|
1745
|
+
var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
|
|
1746
|
+
var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
|
|
1747
|
+
function hookUses(input) {
|
|
1748
|
+
const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
|
|
1749
|
+
const uses = [];
|
|
1750
|
+
lines.forEach((line, index) => {
|
|
1751
|
+
if (!RUNS_MUTATION.test(line)) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const named = TARGET.exec(line) ?? CD.exec(line);
|
|
1755
|
+
const target = named?.[2] ?? lines.slice(0, index).reverse().map((before) => CD.exec(before)?.[2]).find((found) => found !== void 0);
|
|
1756
|
+
const normalized = target?.split("/").filter((part) => part !== "." && part !== "").join("/");
|
|
1757
|
+
const ours = normalized === void 0 || normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
|
|
1758
|
+
if (ours) {
|
|
1759
|
+
uses.push({
|
|
1760
|
+
file: input.file,
|
|
1761
|
+
affected: /--affected\b/.test(line) || /test:gates:mutation\b/.test(line) && input.script.includes("--affected")
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
return uses;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1642
1768
|
// src/snapshot.ts
|
|
1643
1769
|
import { spawnSync } from "child_process";
|
|
1644
1770
|
import { createHash } from "crypto";
|
|
@@ -1646,17 +1772,167 @@ import fs5 from "fs";
|
|
|
1646
1772
|
import { createRequire } from "module";
|
|
1647
1773
|
import os from "os";
|
|
1648
1774
|
import path5 from "path";
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1775
|
+
|
|
1776
|
+
// src/commands/loaded-files.ts
|
|
1777
|
+
import fs4 from "fs";
|
|
1778
|
+
import { builtinModules } from "module";
|
|
1779
|
+
import path4 from "path";
|
|
1780
|
+
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
1781
|
+
var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
|
|
1782
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1783
|
+
var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
1784
|
+
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
1785
|
+
function isFile(file) {
|
|
1786
|
+
try {
|
|
1787
|
+
return fs4.statSync(file).isFile();
|
|
1788
|
+
} catch {
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function resolveFile(base, directory = true) {
|
|
1793
|
+
const candidates = [
|
|
1794
|
+
base,
|
|
1795
|
+
...EXTENSIONS.map((extension) => base + extension),
|
|
1796
|
+
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
1797
|
+
...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
1798
|
+
...directory ? EXTENSIONS.map((extension) => path4.join(base, `index${extension}`)) : []
|
|
1799
|
+
];
|
|
1800
|
+
return candidates.find(isFile) ?? null;
|
|
1801
|
+
}
|
|
1802
|
+
function installed(specifier, from) {
|
|
1803
|
+
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
1804
|
+
for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
|
|
1805
|
+
if (fs4.existsSync(path4.join(current, "node_modules", name))) {
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
if (current === path4.dirname(current)) {
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function loadedFiles(manifest, configFile) {
|
|
1814
|
+
const aliases = manifest.settings.importAliases;
|
|
1815
|
+
const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
|
|
1816
|
+
const target = (specifier, from) => {
|
|
1817
|
+
if (specifier.startsWith(".")) {
|
|
1818
|
+
return resolveFile(path4.resolve(path4.dirname(from), specifier));
|
|
1819
|
+
}
|
|
1820
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
1821
|
+
return alias === void 0 ? null : resolveFile(
|
|
1822
|
+
path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
1823
|
+
);
|
|
1824
|
+
};
|
|
1825
|
+
const read2 = (file) => {
|
|
1826
|
+
const source = fs4.readFileSync(file, "utf8");
|
|
1827
|
+
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
1828
|
+
const specifiers = [
|
|
1829
|
+
.../* @__PURE__ */ new Set([
|
|
1830
|
+
...imports.map((ref) => ref.module),
|
|
1831
|
+
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
1832
|
+
])
|
|
1833
|
+
];
|
|
1834
|
+
const strings = [...source.matchAll(QUOTED)].map((match) => match[2]).filter((text) => !specifiers.includes(text));
|
|
1835
|
+
return { source, specifiers, strings };
|
|
1836
|
+
};
|
|
1837
|
+
const named = /* @__PURE__ */ new Set();
|
|
1838
|
+
const configs = configFile === null ? [] : [path4.resolve(manifest.dir, configFile)];
|
|
1839
|
+
const seenConfigs = new Set(configs);
|
|
1840
|
+
const entry = configs[0];
|
|
1841
|
+
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
1842
|
+
const { specifiers, strings } = read2(file);
|
|
1843
|
+
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
1844
|
+
for (const text of paths) {
|
|
1845
|
+
const stripped = text.replace(/^<rootDir>\//, "");
|
|
1846
|
+
const found = resolveFile(path4.resolve(path4.dirname(file), stripped), false) ?? resolveFile(path4.resolve(manifest.dir, stripped), false);
|
|
1847
|
+
if (found !== null && SCRIPT.test(found)) {
|
|
1848
|
+
named.add(found);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
|
|
1852
|
+
const found = target(specifier, file);
|
|
1853
|
+
if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
|
|
1854
|
+
seenConfigs.add(found);
|
|
1855
|
+
configs.push(found);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const known = /* @__PURE__ */ new Map();
|
|
1860
|
+
const visit = (file) => {
|
|
1861
|
+
const done = known.get(file);
|
|
1862
|
+
if (done !== void 0) {
|
|
1863
|
+
return done;
|
|
1864
|
+
}
|
|
1865
|
+
const { source, specifiers, strings } = read2(file);
|
|
1866
|
+
const imports = [];
|
|
1867
|
+
const next = [];
|
|
1868
|
+
let lost = COMPONENT.test(file) ? `${relative(file)} is a component, which can use others that it does not import` : null;
|
|
1869
|
+
for (const specifier of specifiers) {
|
|
1870
|
+
if (isProjectImport(specifier, aliases)) {
|
|
1871
|
+
const found = target(specifier, file);
|
|
1872
|
+
if (found === null) {
|
|
1873
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
1874
|
+
} else {
|
|
1875
|
+
next.push(found);
|
|
1876
|
+
}
|
|
1877
|
+
} else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
|
|
1878
|
+
imports.push({ specifier, builtin: true });
|
|
1879
|
+
} else if (installed(specifier, file)) {
|
|
1880
|
+
imports.push({ specifier, builtin: false });
|
|
1881
|
+
} else {
|
|
1882
|
+
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
1886
|
+
const found = resolveFile(path4.resolve(path4.dirname(file), text));
|
|
1887
|
+
if (found !== null) {
|
|
1888
|
+
next.push(found);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const visited = {
|
|
1892
|
+
file: { path: relative(file), source, imports },
|
|
1893
|
+
// A component imports like a script (see COMPONENT); data imports nothing.
|
|
1894
|
+
next: next.filter((found) => SCRIPT.test(found) || COMPONENT.test(found)),
|
|
1895
|
+
lost
|
|
1896
|
+
};
|
|
1897
|
+
known.set(file, visited);
|
|
1898
|
+
return visited;
|
|
1899
|
+
};
|
|
1900
|
+
const closure = (spec, starts) => {
|
|
1901
|
+
const queue = [...starts];
|
|
1902
|
+
const seen = new Set(queue);
|
|
1903
|
+
const files = [];
|
|
1904
|
+
let lost = null;
|
|
1905
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
1906
|
+
const visited = visit(file);
|
|
1907
|
+
files.push(visited.file.path);
|
|
1908
|
+
lost ??= visited.lost;
|
|
1909
|
+
for (const found of visited.next.filter((one) => !seen.has(one))) {
|
|
1910
|
+
seen.add(found);
|
|
1911
|
+
queue.push(found);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return { spec, files, lost };
|
|
1915
|
+
};
|
|
1916
|
+
const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
|
|
1917
|
+
const roots = [
|
|
1918
|
+
closure(null, [...named]),
|
|
1919
|
+
...specs.map((spec) => closure(spec, [path4.join(manifest.dir, spec)]))
|
|
1920
|
+
];
|
|
1921
|
+
return { files: [...known.values()].map((visited) => visited.file), roots };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/snapshot.ts
|
|
1925
|
+
var SnapshotError = class extends Error {
|
|
1926
|
+
};
|
|
1927
|
+
var TOOLS = [
|
|
1928
|
+
"@stryker-mutator/core",
|
|
1929
|
+
"@stryker-mutator/jest-runner",
|
|
1930
|
+
"@stryker-mutator/vitest-runner",
|
|
1931
|
+
"@stryker-mutator/typescript-checker",
|
|
1932
|
+
"jest",
|
|
1933
|
+
"babel-jest",
|
|
1934
|
+
"ts-jest",
|
|
1935
|
+
"@swc/jest",
|
|
1660
1936
|
"vitest",
|
|
1661
1937
|
"vite",
|
|
1662
1938
|
"@vitest/coverage-istanbul",
|
|
@@ -1678,8 +1954,14 @@ var LOCK_FILES = [
|
|
|
1678
1954
|
"bun.lockb"
|
|
1679
1955
|
];
|
|
1680
1956
|
var NEVER = ["node_modules", ".git"];
|
|
1957
|
+
var WIDENED_SHOWN = 5;
|
|
1681
1958
|
var SCAN_LIMIT = 8 * 1024 * 1024;
|
|
1682
1959
|
var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
|
|
1960
|
+
var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
|
|
1961
|
+
var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
|
|
1962
|
+
var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
|
|
1963
|
+
var LITERAL = /(['"])([^'"`\n]*)\1/g;
|
|
1964
|
+
var isUpward = (segment) => segment.split("/").includes("..");
|
|
1683
1965
|
function hashOf(content) {
|
|
1684
1966
|
return createHash("sha256").update(content).digest("hex");
|
|
1685
1967
|
}
|
|
@@ -1697,8 +1979,21 @@ function isInside(parent, child) {
|
|
|
1697
1979
|
function toPosix(file) {
|
|
1698
1980
|
return file.split(path5.sep).join("/");
|
|
1699
1981
|
}
|
|
1982
|
+
function gitEnvironment(env) {
|
|
1983
|
+
if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
|
|
1984
|
+
return env;
|
|
1985
|
+
}
|
|
1986
|
+
const rest = { ...env };
|
|
1987
|
+
delete rest.GIT_DIR;
|
|
1988
|
+
return rest;
|
|
1989
|
+
}
|
|
1700
1990
|
function git(cwd, args) {
|
|
1701
|
-
const result = spawnSync("git", args, {
|
|
1991
|
+
const result = spawnSync("git", args, {
|
|
1992
|
+
cwd,
|
|
1993
|
+
env: gitEnvironment(process.env),
|
|
1994
|
+
encoding: "utf8",
|
|
1995
|
+
maxBuffer: 1024 * 1024 * 1024
|
|
1996
|
+
});
|
|
1702
1997
|
return result.error || result.status !== 0 ? null : result.stdout;
|
|
1703
1998
|
}
|
|
1704
1999
|
function gitLister(root) {
|
|
@@ -1831,7 +2126,7 @@ function ownVersion() {
|
|
|
1831
2126
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
1832
2127
|
return version;
|
|
1833
2128
|
}
|
|
1834
|
-
function
|
|
2129
|
+
function snapshotOf(input) {
|
|
1835
2130
|
const dir = fs5.realpathSync(input.dir);
|
|
1836
2131
|
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1837
2132
|
let root = top === null ? dir : fs5.realpathSync(top.trim());
|
|
@@ -1845,6 +2140,7 @@ function takeSnapshot(input) {
|
|
|
1845
2140
|
}
|
|
1846
2141
|
const listRoot = root;
|
|
1847
2142
|
const list = lister;
|
|
2143
|
+
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path5.join(listRoot, file)));
|
|
1848
2144
|
const walk = walkLister(listRoot);
|
|
1849
2145
|
const written = input.written.map((file) => {
|
|
1850
2146
|
const target = path5.resolve(dir, file);
|
|
@@ -1860,34 +2156,75 @@ function takeSnapshot(input) {
|
|
|
1860
2156
|
}
|
|
1861
2157
|
const files = /* @__PURE__ */ Object.create(null);
|
|
1862
2158
|
const regions = [dir];
|
|
2159
|
+
const widened = [];
|
|
1863
2160
|
const queued = new Set(regions);
|
|
1864
|
-
const enqueue = (target, from) => {
|
|
1865
|
-
const
|
|
2161
|
+
const enqueue = (target, from, reference, anchored = false) => {
|
|
2162
|
+
const above2 = target !== dir && isInside(target, dir) && isInside(listRoot, target);
|
|
2163
|
+
if (above2 && anchored) {
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const directory = above2 ? listRoot : target;
|
|
1866
2167
|
if (!isInside(listRoot, directory)) {
|
|
2168
|
+
const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
|
|
1867
2169
|
throw new SnapshotError(
|
|
1868
|
-
`${toPosix(path5.relative(dir, from))} points outside of what can be listed (${directory})`
|
|
2170
|
+
`${toPosix(path5.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
|
|
1869
2171
|
);
|
|
1870
2172
|
}
|
|
2173
|
+
if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
|
|
2174
|
+
const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
|
|
2175
|
+
widened.push(`${toPosix(path5.relative(listRoot, from))}${written2}`);
|
|
2176
|
+
}
|
|
1871
2177
|
if (![...queued].some((region) => isInside(region, directory))) {
|
|
1872
2178
|
queued.add(directory);
|
|
1873
2179
|
regions.push(directory);
|
|
1874
2180
|
}
|
|
1875
2181
|
};
|
|
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
|
-
}
|
|
2182
|
+
const follow = (fixed, open, anchored, file, reference) => {
|
|
1882
2183
|
const target = path5.resolve(path5.dirname(file), fixed);
|
|
1883
2184
|
let isDirectory = false;
|
|
1884
2185
|
try {
|
|
1885
2186
|
isDirectory = fs5.statSync(target).isDirectory();
|
|
1886
2187
|
} catch {
|
|
1887
2188
|
}
|
|
2189
|
+
if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
|
|
2190
|
+
files[toPosix(path5.relative(listRoot, target))] = "missing";
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
1888
2193
|
const directory = isDirectory ? target : path5.dirname(target);
|
|
1889
2194
|
if (fs5.existsSync(directory)) {
|
|
1890
|
-
enqueue(directory, file);
|
|
2195
|
+
enqueue(directory, file, reference, anchored && !open);
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
const scan = (text, file) => {
|
|
2199
|
+
const taken = [];
|
|
2200
|
+
for (const match of text.matchAll(FROM_HERE)) {
|
|
2201
|
+
const literals = match[1];
|
|
2202
|
+
const segments = [...literals.matchAll(LITERAL)].map((literal) => literal[2]);
|
|
2203
|
+
if (!segments.some(isUpward)) {
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const start = (match.index ?? 0) + match[0].indexOf(literals);
|
|
2207
|
+
taken.push([start, start + literals.length]);
|
|
2208
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
2209
|
+
const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
|
|
2210
|
+
const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
|
|
2211
|
+
follow(path5.posix.join(...segments), open, true, file, reference);
|
|
2212
|
+
}
|
|
2213
|
+
for (const match of text.matchAll(UPWARD)) {
|
|
2214
|
+
const at = match.index ?? 0;
|
|
2215
|
+
if (taken.some(([start, end]) => at >= start && at < end)) {
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const literal = match[2];
|
|
2219
|
+
const here = HERE.exec(literal);
|
|
2220
|
+
const relative = here === null ? literal : `.${literal.slice(here[0].length)}`;
|
|
2221
|
+
const open = relative.includes("${");
|
|
2222
|
+
const fixed = open ? relative.slice(0, relative.lastIndexOf("/", relative.indexOf("${")) + 1) : relative;
|
|
2223
|
+
if (here === null && !fixed.startsWith("../") || open && isUpward(relative.slice(relative.indexOf("${")))) {
|
|
2224
|
+
enqueue(listRoot, file, literal);
|
|
2225
|
+
} else {
|
|
2226
|
+
follow(fixed, open, here !== null, file, literal);
|
|
2227
|
+
}
|
|
1891
2228
|
}
|
|
1892
2229
|
};
|
|
1893
2230
|
const add = (relative) => {
|
|
@@ -1919,11 +2256,10 @@ function takeSnapshot(input) {
|
|
|
1919
2256
|
} else {
|
|
1920
2257
|
const content = fs5.readFileSync(file);
|
|
1921
2258
|
files[relative] = hashOf(content);
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
}
|
|
2259
|
+
const document = DOCUMENTS.includes(path5.extname(file).toLowerCase());
|
|
2260
|
+
const loaded = followed === null || !isInside(dir, file) || followed.has(file);
|
|
2261
|
+
if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
|
|
2262
|
+
scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
|
|
1927
2263
|
}
|
|
1928
2264
|
}
|
|
1929
2265
|
};
|
|
@@ -1965,17 +2301,477 @@ function takeSnapshot(input) {
|
|
|
1965
2301
|
environment[name] = version;
|
|
1966
2302
|
}
|
|
1967
2303
|
}
|
|
1968
|
-
return {
|
|
1969
|
-
mode,
|
|
1970
|
-
environment,
|
|
1971
|
-
args: input.args,
|
|
1972
|
-
files
|
|
1973
|
-
};
|
|
2304
|
+
return { snapshot: { mode, environment, args: input.args, files }, root: listRoot, widened };
|
|
1974
2305
|
}
|
|
1975
2306
|
|
|
1976
|
-
// src/
|
|
2307
|
+
// src/affected-files.ts
|
|
1977
2308
|
import fs6 from "fs";
|
|
2309
|
+
import { builtinModules as builtinModules2 } from "module";
|
|
1978
2310
|
import path6 from "path";
|
|
2311
|
+
var AffectedError = class extends Error {
|
|
2312
|
+
};
|
|
2313
|
+
var RUNTIME_PATHS = /\b(?:__dirname|__filename|require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:url|dirname|filename|glob|resolve)\b/;
|
|
2314
|
+
var FILE_MODULES = /* @__PURE__ */ new Set([
|
|
2315
|
+
"fs",
|
|
2316
|
+
"fs/promises",
|
|
2317
|
+
"child_process",
|
|
2318
|
+
"module",
|
|
2319
|
+
"worker_threads",
|
|
2320
|
+
"fs-extra",
|
|
2321
|
+
"graceful-fs",
|
|
2322
|
+
"glob",
|
|
2323
|
+
"fast-glob",
|
|
2324
|
+
"globby"
|
|
2325
|
+
]);
|
|
2326
|
+
var bare = (specifier) => specifier.replace(/^node:/, "");
|
|
2327
|
+
function linkedPackage(specifier, from) {
|
|
2328
|
+
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2329
|
+
for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
|
|
2330
|
+
const candidate = path6.join(current, "node_modules", name);
|
|
2331
|
+
if (fs6.existsSync(candidate)) {
|
|
2332
|
+
const target = realpath(candidate);
|
|
2333
|
+
return target.split(path6.sep).includes("node_modules") ? null : target;
|
|
2334
|
+
}
|
|
2335
|
+
if (current === path6.dirname(current)) {
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
var toPosix2 = (file) => file.split(path6.sep).join("/");
|
|
2341
|
+
function realpath(file) {
|
|
2342
|
+
try {
|
|
2343
|
+
return fs6.realpathSync.native(file);
|
|
2344
|
+
} catch {
|
|
2345
|
+
return file;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function runnerConfigs(dir, stryker, options) {
|
|
2349
|
+
const files = [path6.resolve(dir, stryker)];
|
|
2350
|
+
const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
|
|
2351
|
+
if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
|
|
2352
|
+
throw new AffectedError(
|
|
2353
|
+
`--affected knows the test runners jest and vitest, the Stryker config uses ${String(options.testRunner)}`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
if (typeof named === "string") {
|
|
2357
|
+
files.push(path6.resolve(dir, named));
|
|
2358
|
+
} else {
|
|
2359
|
+
const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
|
|
2360
|
+
for (const name of names) {
|
|
2361
|
+
for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
|
|
2362
|
+
const file = path6.join(dir, name + extension);
|
|
2363
|
+
if (fs6.existsSync(file)) {
|
|
2364
|
+
files.push(file);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return { files: files.filter((file) => fs6.existsSync(file)).map(realpath) };
|
|
2370
|
+
}
|
|
2371
|
+
function fileEdges(file, context) {
|
|
2372
|
+
const { dir, aliases, cache } = context;
|
|
2373
|
+
const known = cache.get(file);
|
|
2374
|
+
if (known !== void 0) {
|
|
2375
|
+
return known;
|
|
2376
|
+
}
|
|
2377
|
+
const shown = (one) => toPosix2(path6.relative(dir, one));
|
|
2378
|
+
const target = (specifier, from) => {
|
|
2379
|
+
if (specifier.startsWith(".")) {
|
|
2380
|
+
return resolveFile(path6.resolve(path6.dirname(from), specifier));
|
|
2381
|
+
}
|
|
2382
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2383
|
+
return alias === void 0 ? null : resolveFile(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
|
|
2384
|
+
};
|
|
2385
|
+
const found = { loads: [], unknown: null, computed: false, unnamed: [] };
|
|
2386
|
+
cache.set(file, found);
|
|
2387
|
+
const snapshotFile = path6.join(
|
|
2388
|
+
path6.dirname(file),
|
|
2389
|
+
"__snapshots__",
|
|
2390
|
+
`${path6.basename(file)}.snap`
|
|
2391
|
+
);
|
|
2392
|
+
if (fs6.existsSync(snapshotFile)) {
|
|
2393
|
+
found.loads.push(realpath(snapshotFile));
|
|
2394
|
+
}
|
|
2395
|
+
if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
|
|
2396
|
+
return found;
|
|
2397
|
+
}
|
|
2398
|
+
const source = fs6.readFileSync(file, "utf8");
|
|
2399
|
+
if (COMPONENT.test(file)) {
|
|
2400
|
+
found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
|
|
2401
|
+
}
|
|
2402
|
+
const uncheckable = findUncheckableImports(source)[0];
|
|
2403
|
+
const runtime = RUNTIME_PATHS.exec(source);
|
|
2404
|
+
if (uncheckable !== void 0) {
|
|
2405
|
+
found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
|
|
2406
|
+
found.computed = true;
|
|
2407
|
+
} else if (runtime !== null) {
|
|
2408
|
+
found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
|
|
2409
|
+
}
|
|
2410
|
+
const specifiers = /* @__PURE__ */ new Set([
|
|
2411
|
+
...parseImports(source).map((ref) => ref.module),
|
|
2412
|
+
...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
2413
|
+
]);
|
|
2414
|
+
for (const specifier of specifiers) {
|
|
2415
|
+
if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
2416
|
+
const resolved = resolveFile(
|
|
2417
|
+
specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
|
|
2418
|
+
);
|
|
2419
|
+
if (resolved === null) {
|
|
2420
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
|
|
2421
|
+
} else {
|
|
2422
|
+
found.loads.push(realpath(resolved));
|
|
2423
|
+
}
|
|
2424
|
+
} else if (isProjectImport(specifier, aliases)) {
|
|
2425
|
+
const resolved = target(specifier, file);
|
|
2426
|
+
if (resolved === null) {
|
|
2427
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
|
|
2428
|
+
} else {
|
|
2429
|
+
found.loads.push(realpath(resolved));
|
|
2430
|
+
}
|
|
2431
|
+
} else if (FILE_MODULES.has(bare(specifier))) {
|
|
2432
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
|
|
2433
|
+
} else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
|
|
2434
|
+
if (!installed(specifier, file)) {
|
|
2435
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`;
|
|
2436
|
+
found.unnamed.push(specifier);
|
|
2437
|
+
} else if (linkedPackage(specifier, file) !== null) {
|
|
2438
|
+
found.unknown ??= `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`;
|
|
2439
|
+
found.computed = true;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
for (const match of source.matchAll(QUOTED)) {
|
|
2444
|
+
const text = match[2];
|
|
2445
|
+
if (text.startsWith(".") && !specifiers.has(text)) {
|
|
2446
|
+
const resolved = resolveFile(path6.resolve(path6.dirname(file), text));
|
|
2447
|
+
if (resolved !== null) {
|
|
2448
|
+
found.loads.push(realpath(resolved));
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return found;
|
|
2453
|
+
}
|
|
2454
|
+
function affectedInputs(manifest, input) {
|
|
2455
|
+
const dir = realpath(manifest.dir);
|
|
2456
|
+
const aliases = manifest.settings.importAliases;
|
|
2457
|
+
const placed = [...input.tests, ...Object.values(input.covering).flat()];
|
|
2458
|
+
const absolute = placed.find((file) => path6.isAbsolute(file) || file.startsWith(".."));
|
|
2459
|
+
if (absolute !== void 0) {
|
|
2460
|
+
throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
|
|
2461
|
+
}
|
|
2462
|
+
const keyOf2 = (file) => {
|
|
2463
|
+
const relative = path6.relative(input.root, realpath(file));
|
|
2464
|
+
if (relative.startsWith("..") || path6.isAbsolute(relative)) {
|
|
2465
|
+
throw new AffectedError(`${file} is outside of the files that are compared`);
|
|
2466
|
+
}
|
|
2467
|
+
return toPosix2(relative);
|
|
2468
|
+
};
|
|
2469
|
+
const shown = (file) => toPosix2(path6.relative(dir, file));
|
|
2470
|
+
const edges = /* @__PURE__ */ new Map();
|
|
2471
|
+
const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
|
|
2472
|
+
const closure = (start, strict) => {
|
|
2473
|
+
const seen = /* @__PURE__ */ new Set([start]);
|
|
2474
|
+
const queue = [start];
|
|
2475
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2476
|
+
const found = edgesOf(file);
|
|
2477
|
+
if (strict && found.unknown !== null) {
|
|
2478
|
+
throw new AffectedError(found.unknown);
|
|
2479
|
+
}
|
|
2480
|
+
for (const next of found.loads) {
|
|
2481
|
+
if (!seen.has(next)) {
|
|
2482
|
+
seen.add(next);
|
|
2483
|
+
queue.push(next);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return [...seen];
|
|
2488
|
+
};
|
|
2489
|
+
const isCompared = (file) => {
|
|
2490
|
+
const relative = path6.relative(input.root, realpath(file));
|
|
2491
|
+
return !relative.startsWith("..") && !path6.isAbsolute(relative) && toPosix2(relative) in input.files;
|
|
2492
|
+
};
|
|
2493
|
+
const keysOf = (files) => files.map((file) => {
|
|
2494
|
+
const key = keyOf2(file);
|
|
2495
|
+
if (!(key in input.files)) {
|
|
2496
|
+
throw new AffectedError(
|
|
2497
|
+
`${shown(file)} is loaded by a gate or a spec, but is not among the compared files (does git ignore it?)`
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
return key;
|
|
2501
|
+
});
|
|
2502
|
+
const config = /* @__PURE__ */ new Set();
|
|
2503
|
+
const named = [];
|
|
2504
|
+
for (const file of input.configs.files) {
|
|
2505
|
+
for (const loaded of closure(file, false).filter(isCompared)) {
|
|
2506
|
+
config.add(loaded);
|
|
2507
|
+
if (!SCRIPT.test(loaded)) {
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
|
|
2511
|
+
const text = match[2].replace(/^<rootDir>\//, "");
|
|
2512
|
+
const found = resolveFile(path6.resolve(path6.dirname(loaded), text), false) ?? resolveFile(path6.resolve(dir, text), false);
|
|
2513
|
+
if (found !== null && !text.includes("*") && isCompared(found)) {
|
|
2514
|
+
named.push(realpath(found));
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
const gatePaths = new Map(
|
|
2520
|
+
manifest.gates.map((gate) => [realpath(path6.join(dir, gate.path)), gate.path])
|
|
2521
|
+
);
|
|
2522
|
+
const tests = [
|
|
2523
|
+
.../* @__PURE__ */ new Set([
|
|
2524
|
+
...input.tests,
|
|
2525
|
+
...manifest.gates.flatMap((gate) => gate.existingSpecs),
|
|
2526
|
+
...Object.values(input.covering).flat()
|
|
2527
|
+
])
|
|
2528
|
+
];
|
|
2529
|
+
const runAsSuch = new Set(
|
|
2530
|
+
[...tests, ...manifest.gates.map((gate) => gate.path)].map(
|
|
2531
|
+
(file) => realpath(path6.join(dir, file))
|
|
2532
|
+
)
|
|
2533
|
+
);
|
|
2534
|
+
const subprojectKey = toPosix2(path6.relative(input.root, dir));
|
|
2535
|
+
const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
|
|
2536
|
+
for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
|
|
2537
|
+
for (const loaded of closure(file, true)) {
|
|
2538
|
+
const gate = gatePaths.get(loaded);
|
|
2539
|
+
if (gate !== void 0) {
|
|
2540
|
+
throw new AffectedError(
|
|
2541
|
+
`${gate} is loaded through the test runner config for every spec (${shown(file)})`
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
config.add(loaded);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
const fromSubproject = (file) => path6.join(dir, file);
|
|
2548
|
+
const closures = /* @__PURE__ */ Object.create(null);
|
|
2549
|
+
for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
|
|
2550
|
+
const absolute2 = fromSubproject(file);
|
|
2551
|
+
if (!fs6.existsSync(absolute2)) {
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), true));
|
|
2555
|
+
}
|
|
2556
|
+
const keyed = (files) => files.map((file) => toPosix2(path6.relative(input.root, path6.join(dir, file))));
|
|
2557
|
+
const dependencies = gateDependencies({
|
|
2558
|
+
gates: manifest.gates.map((gate) => ({
|
|
2559
|
+
key: keyed([gate.path])[0],
|
|
2560
|
+
specs: keyed(gate.existingSpecs)
|
|
2561
|
+
})),
|
|
2562
|
+
tests: keyed(tests),
|
|
2563
|
+
covering: Object.fromEntries(
|
|
2564
|
+
Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
|
|
2565
|
+
),
|
|
2566
|
+
closures
|
|
2567
|
+
});
|
|
2568
|
+
const subproject = toPosix2(path6.relative(input.root, dir));
|
|
2569
|
+
const prefix = subproject === "" ? "" : `${subproject}/`;
|
|
2570
|
+
const gates = /* @__PURE__ */ Object.create(null);
|
|
2571
|
+
for (const gate of manifest.gates) {
|
|
2572
|
+
const key = keyed([gate.path])[0];
|
|
2573
|
+
const found = dependencies[key];
|
|
2574
|
+
gates[gate.path] = {
|
|
2575
|
+
key,
|
|
2576
|
+
specs: found.specs.map(
|
|
2577
|
+
(spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
|
|
2578
|
+
),
|
|
2579
|
+
files: found.files
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
return { subproject, config: keysOf([...config]).sort(), gates };
|
|
2583
|
+
}
|
|
2584
|
+
var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
|
|
2585
|
+
var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
|
|
2586
|
+
var PROJECTS = /\b(?:projects|workspace)\b/;
|
|
2587
|
+
var HELPERS = ["createJestGatesConfig", "createVitestGatesConfig"];
|
|
2588
|
+
function isHelperResult(text) {
|
|
2589
|
+
const names = new Set(HELPERS);
|
|
2590
|
+
for (const match of text.matchAll(
|
|
2591
|
+
/\b(?:createJestGatesConfig|createVitestGatesConfig)\s*(?::|\bas\b)\s*([\w$]+)/g
|
|
2592
|
+
)) {
|
|
2593
|
+
names.add(match[1]);
|
|
2594
|
+
}
|
|
2595
|
+
const escape = (name2) => name2.replace(/\$/g, "\\$");
|
|
2596
|
+
const call = `(?:defineConfig\\s*\\(\\s*)?(?:${[...names].map(escape).join("|")})\\s*\\(`;
|
|
2597
|
+
const exported = "(?:export\\s+default|module\\.exports\\s*=)\\s*";
|
|
2598
|
+
const reached = (name2) => new RegExp(`\\b${escape(name2)}\\s*(?:\\.|\\[)`).test(text);
|
|
2599
|
+
if (new RegExp(`${exported}${call}`).test(text)) {
|
|
2600
|
+
return !reached("module.exports") && !/\bexports\s*(?:\.|\[)/.test(text.replace(/\bmodule\.exports\s*=/g, ""));
|
|
2601
|
+
}
|
|
2602
|
+
const variable = new RegExp(`\\b(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*${call}`).exec(text);
|
|
2603
|
+
if (variable === null) {
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
const name = escape(variable[1]);
|
|
2607
|
+
const uses = text.match(new RegExp(`(?<![\\w$.])${name}(?![\\w$])`, "g")) ?? [];
|
|
2608
|
+
return uses.length === 2 && new RegExp(`${exported}${name}\\s*(?:;|$)`, "m").test(text);
|
|
2609
|
+
}
|
|
2610
|
+
var READ_BY_NAME = [
|
|
2611
|
+
/^test-gates\.json$/,
|
|
2612
|
+
/^[jt]sconfig(?:\..+)?\.json$/,
|
|
2613
|
+
/^babel\.config\.[cm]?[jt]s(?:on)?$/,
|
|
2614
|
+
/^\.babelrc(?:\.[cm]?js(?:on)?)?$/,
|
|
2615
|
+
/^\.swcrc$/
|
|
2616
|
+
];
|
|
2617
|
+
function loadedScope(manifest, configs, listed2) {
|
|
2618
|
+
const dir = realpath(manifest.dir);
|
|
2619
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2620
|
+
const context = { dir, aliases: manifest.settings.importAliases, cache };
|
|
2621
|
+
const runner = configs.files.slice(1);
|
|
2622
|
+
const texts = runner.map((file) => withoutComments(fs6.readFileSync(file, "utf8")));
|
|
2623
|
+
if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
|
|
2624
|
+
return { every: NOT_THE_HELPER2 };
|
|
2625
|
+
}
|
|
2626
|
+
if (!texts.every(isHelperResult)) {
|
|
2627
|
+
return {
|
|
2628
|
+
every: "what the runner config exports is not what the helper returns (merged with another config, or changed after the call)"
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
const parts = new Set(runner.map(realpath));
|
|
2632
|
+
for (const file of [...parts]) {
|
|
2633
|
+
if (SCRIPT.test(file)) {
|
|
2634
|
+
fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if ([...parts].some((file) => PROJECTS.test(withoutComments(fs6.readFileSync(file, "utf8"))))) {
|
|
2638
|
+
return {
|
|
2639
|
+
every: "the runner config, or a file it is put together from, names projects or a workspace"
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
const named = fs6.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path6.join(dir, name));
|
|
2643
|
+
const mocks = path6.join(dir, "__mocks__");
|
|
2644
|
+
const seeds = [
|
|
2645
|
+
...configs.files,
|
|
2646
|
+
...named,
|
|
2647
|
+
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path6.join(dir, file)),
|
|
2648
|
+
...listed2.filter((file) => file.startsWith(`${mocks}${path6.sep}`))
|
|
2649
|
+
].filter((file) => fs6.existsSync(file)).map(realpath);
|
|
2650
|
+
const naming = new Set(
|
|
2651
|
+
[...configs.files, ...named].filter((file) => fs6.existsSync(file)).map(realpath)
|
|
2652
|
+
);
|
|
2653
|
+
const seen = new Set(seeds);
|
|
2654
|
+
const queue = [...seeds];
|
|
2655
|
+
const add = (file) => {
|
|
2656
|
+
const real = realpath(file);
|
|
2657
|
+
if (!seen.has(real)) {
|
|
2658
|
+
seen.add(real);
|
|
2659
|
+
queue.push(real);
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2663
|
+
if (fs6.statSync(file).isDirectory()) {
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
const edges = fileEdges(file, context);
|
|
2667
|
+
if (edges.computed) {
|
|
2668
|
+
return { every: edges.unknown ?? "a file loads modules that are not followed" };
|
|
2669
|
+
}
|
|
2670
|
+
edges.loads.forEach(add);
|
|
2671
|
+
for (const specifier of edges.unnamed) {
|
|
2672
|
+
endingAs(specifier, listed2).forEach(add);
|
|
2673
|
+
}
|
|
2674
|
+
const mock = path6.join(path6.dirname(file), "__mocks__", path6.basename(file));
|
|
2675
|
+
if (fs6.existsSync(mock)) {
|
|
2676
|
+
add(mock);
|
|
2677
|
+
}
|
|
2678
|
+
if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
|
|
2679
|
+
for (const named2 of namedIn(file)) {
|
|
2680
|
+
const text = named2.replace(/^<rootDir>\//, "");
|
|
2681
|
+
const found = resolveFile(path6.resolve(path6.dirname(file), text), false) ?? resolveFile(path6.resolve(dir, text), false);
|
|
2682
|
+
if (found !== null && !text.includes("*")) {
|
|
2683
|
+
if (!SCRIPT.test(found)) {
|
|
2684
|
+
naming.add(realpath(found));
|
|
2685
|
+
}
|
|
2686
|
+
add(found);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return { files: seen };
|
|
2692
|
+
}
|
|
2693
|
+
function endingAs(specifier, files) {
|
|
2694
|
+
const parts = specifier.split("/");
|
|
2695
|
+
const rest = (parts.length > 1 ? parts.slice(1).join("/") : specifier.replace(/^[@#~$]+/, "")).replace(/\.[cm]?[jt]sx?$/, "");
|
|
2696
|
+
if (rest === "") {
|
|
2697
|
+
return [];
|
|
2698
|
+
}
|
|
2699
|
+
return files.filter((file) => {
|
|
2700
|
+
const bare2 = toPosix2(file).replace(/\.[^./]+$/, "");
|
|
2701
|
+
return [bare2, bare2.replace(/\/index$/, "")].some((one) => one.endsWith(`/${rest}`));
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
function namedIn(file) {
|
|
2705
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
2706
|
+
if (SCRIPT.test(file)) {
|
|
2707
|
+
return [...text.matchAll(QUOTED)].map((match) => match[2]);
|
|
2708
|
+
}
|
|
2709
|
+
return ["extends", "plugins", "presets"].flatMap((key) => valuesOf(text, key));
|
|
2710
|
+
}
|
|
2711
|
+
function valuesOf(text, key) {
|
|
2712
|
+
const found = [];
|
|
2713
|
+
for (const match of text.matchAll(new RegExp(`"${key}"\\s*:\\s*`, "g"))) {
|
|
2714
|
+
let at = (match.index ?? 0) + match[0].length;
|
|
2715
|
+
if (text[at] === '"') {
|
|
2716
|
+
const end = text.indexOf('"', at + 1);
|
|
2717
|
+
found.push(text.slice(at + 1, end === -1 ? void 0 : end));
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
if (text[at] !== "[") {
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
let depth = 0;
|
|
2724
|
+
for (; at < text.length; at += 1) {
|
|
2725
|
+
const char = text[at];
|
|
2726
|
+
if (char === '"') {
|
|
2727
|
+
const end = text.indexOf('"', at + 1);
|
|
2728
|
+
found.push(text.slice(at + 1, end === -1 ? void 0 : end));
|
|
2729
|
+
at = end === -1 ? text.length : end;
|
|
2730
|
+
} else if (char === "[") {
|
|
2731
|
+
depth += 1;
|
|
2732
|
+
} else if (char === "]") {
|
|
2733
|
+
depth -= 1;
|
|
2734
|
+
if (depth === 0) {
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return found;
|
|
2741
|
+
}
|
|
2742
|
+
function withoutComments(text) {
|
|
2743
|
+
let out = "";
|
|
2744
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
2745
|
+
const char = text[at];
|
|
2746
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2747
|
+
let end = at + 1;
|
|
2748
|
+
while (end < text.length && text[end] !== char && text[end] !== "\n") {
|
|
2749
|
+
end += text[end] === "\\" ? 2 : 1;
|
|
2750
|
+
}
|
|
2751
|
+
out += text.slice(at, end + 1);
|
|
2752
|
+
at = end;
|
|
2753
|
+
} else if (char === "/" && text[at + 1] === "/") {
|
|
2754
|
+
const end = text.indexOf("\n", at);
|
|
2755
|
+
at = end === -1 ? text.length : end - 1;
|
|
2756
|
+
} else if (char === "/" && text[at + 1] === "*") {
|
|
2757
|
+
const end = text.indexOf("*/", at + 2);
|
|
2758
|
+
at = end === -1 ? text.length : end + 1;
|
|
2759
|
+
} else {
|
|
2760
|
+
out += char;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return out;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// src/commands/mutation.ts
|
|
2767
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2768
|
+
import fs8 from "fs";
|
|
2769
|
+
import path8 from "path";
|
|
2770
|
+
import { pathToFileURL } from "url";
|
|
2771
|
+
|
|
2772
|
+
// src/commands/mutation-result.ts
|
|
2773
|
+
import fs7 from "fs";
|
|
2774
|
+
import path7 from "path";
|
|
1979
2775
|
|
|
1980
2776
|
// src/core/mutation-result.ts
|
|
1981
2777
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -1984,7 +2780,7 @@ var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
|
|
|
1984
2780
|
function normalizeCode(text) {
|
|
1985
2781
|
return text.replace(/\s+/g, " ").trim();
|
|
1986
2782
|
}
|
|
1987
|
-
function
|
|
2783
|
+
function normalizePath(file) {
|
|
1988
2784
|
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1989
2785
|
}
|
|
1990
2786
|
function normalizeNewlines(text) {
|
|
@@ -2050,10 +2846,10 @@ function judgeMutationReport(input) {
|
|
|
2050
2846
|
};
|
|
2051
2847
|
const files = /* @__PURE__ */ new Map();
|
|
2052
2848
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
2053
|
-
files.set(
|
|
2849
|
+
files.set(normalizePath(file), result);
|
|
2054
2850
|
}
|
|
2055
2851
|
for (const gate of input.gates) {
|
|
2056
|
-
const gatePath =
|
|
2852
|
+
const gatePath = normalizePath(gate.path);
|
|
2057
2853
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
2058
2854
|
violations.push(...shapeViolations);
|
|
2059
2855
|
violations.push(
|
|
@@ -2225,17 +3021,17 @@ function formatSummary(summary) {
|
|
|
2225
3021
|
|
|
2226
3022
|
// src/commands/mutation-result.ts
|
|
2227
3023
|
function reportPathOf(manifest, override) {
|
|
2228
|
-
return
|
|
3024
|
+
return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
|
|
2229
3025
|
}
|
|
2230
|
-
function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
3026
|
+
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
|
|
2231
3027
|
const manifest = readManifest(dir);
|
|
2232
3028
|
const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
|
|
2233
3029
|
const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
|
|
2234
3030
|
const reportPath = reportPathOf(manifest, reportOverride);
|
|
2235
|
-
const shown =
|
|
3031
|
+
const shown = path7.relative(manifest.dir, reportPath) || reportPath;
|
|
2236
3032
|
let report;
|
|
2237
3033
|
try {
|
|
2238
|
-
report = JSON.parse(
|
|
3034
|
+
report = JSON.parse(fs7.readFileSync(reportPath, "utf8"));
|
|
2239
3035
|
} catch (error) {
|
|
2240
3036
|
io2.err(
|
|
2241
3037
|
`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 +3048,13 @@ function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
|
2252
3048
|
maxTimeouts: manifest.settings.maxTimeouts,
|
|
2253
3049
|
readSource: (gatePath) => {
|
|
2254
3050
|
try {
|
|
2255
|
-
return
|
|
3051
|
+
return fs7.readFileSync(path7.join(manifest.dir, gatePath), "utf8");
|
|
2256
3052
|
} catch {
|
|
2257
3053
|
return void 0;
|
|
2258
3054
|
}
|
|
2259
3055
|
}
|
|
2260
3056
|
});
|
|
3057
|
+
seen({ report, verdict });
|
|
2261
3058
|
const summary = formatSummary(verdict.summary);
|
|
2262
3059
|
if (verdict.violations.length === 0 && verdict.survivors.length === 0) {
|
|
2263
3060
|
io2.out(`test-gates mutation: OK (${summary})${caveat}`);
|
|
@@ -2319,15 +3116,15 @@ function runMutationResult(dir, reportOverride, io2, only = []) {
|
|
|
2319
3116
|
// src/commands/mutation.ts
|
|
2320
3117
|
var CONFIG_MARK = "\0test-gates-stryker-config\0";
|
|
2321
3118
|
function loadStrykerConfig(manifest, config) {
|
|
2322
|
-
const file =
|
|
3119
|
+
const file = path8.resolve(manifest.dir, config);
|
|
2323
3120
|
if (file.endsWith(".json")) {
|
|
2324
3121
|
try {
|
|
2325
|
-
return JSON.parse(
|
|
3122
|
+
return JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
2326
3123
|
} catch {
|
|
2327
3124
|
return void 0;
|
|
2328
3125
|
}
|
|
2329
3126
|
}
|
|
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)});`;
|
|
3127
|
+
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
3128
|
const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
|
|
2332
3129
|
cwd: manifest.dir,
|
|
2333
3130
|
encoding: "utf8",
|
|
@@ -2341,7 +3138,7 @@ function loadStrykerConfig(manifest, config) {
|
|
|
2341
3138
|
}
|
|
2342
3139
|
function modifiedAt(file) {
|
|
2343
3140
|
try {
|
|
2344
|
-
const stat =
|
|
3141
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
2345
3142
|
return `${stat.mtimeNs} ${stat.ino}`;
|
|
2346
3143
|
} catch {
|
|
2347
3144
|
return null;
|
|
@@ -2350,145 +3147,643 @@ function modifiedAt(file) {
|
|
|
2350
3147
|
function guardRecordPath(incrementalFile) {
|
|
2351
3148
|
return `${incrementalFile}.test-gates.json`;
|
|
2352
3149
|
}
|
|
2353
|
-
function
|
|
2354
|
-
const
|
|
3150
|
+
function coveringSpecs(report, gates) {
|
|
3151
|
+
const fileOf = /* @__PURE__ */ new Map();
|
|
3152
|
+
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
3153
|
+
for (const test of entry.tests ?? []) {
|
|
3154
|
+
fileOf.set(test.id, file);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
const covering = /* @__PURE__ */ Object.create(null);
|
|
3158
|
+
for (const gate of gates) {
|
|
3159
|
+
const specs = /* @__PURE__ */ new Set();
|
|
3160
|
+
let placed = true;
|
|
3161
|
+
const key = withoutDots(gate);
|
|
3162
|
+
for (const mutant of report.files?.[key]?.mutants ?? []) {
|
|
3163
|
+
for (const id of [...mutant.coveredBy ?? [], ...mutant.killedBy ?? []]) {
|
|
3164
|
+
const file = fileOf.get(id);
|
|
3165
|
+
if (file === void 0) {
|
|
3166
|
+
placed = false;
|
|
3167
|
+
} else {
|
|
3168
|
+
specs.add(file);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
covering[gate] = placed ? [...specs].sort() : null;
|
|
3173
|
+
}
|
|
3174
|
+
return covering;
|
|
3175
|
+
}
|
|
3176
|
+
function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
2355
3177
|
const loaded = loadStrykerConfig(manifest, config);
|
|
2356
3178
|
const options = incrementalOptions(loaded, strykerArgs);
|
|
3179
|
+
const base = fs8.realpathSync(manifest.dir);
|
|
3180
|
+
const configs = () => runnerConfigs(base, config, loaded ?? {});
|
|
3181
|
+
const decision = {
|
|
3182
|
+
incremental: false,
|
|
3183
|
+
incrementalFile: null,
|
|
3184
|
+
recordFile: null,
|
|
3185
|
+
incrementalHash: null,
|
|
3186
|
+
forced: options.force,
|
|
3187
|
+
snapshot: null,
|
|
3188
|
+
failure: null,
|
|
3189
|
+
root: base,
|
|
3190
|
+
widened: [],
|
|
3191
|
+
scope: null,
|
|
3192
|
+
force: null,
|
|
3193
|
+
plan: null,
|
|
3194
|
+
last: void 0,
|
|
3195
|
+
ci: affected ? affectedRefused(process.env) : null,
|
|
3196
|
+
unchanged: null,
|
|
3197
|
+
configs
|
|
3198
|
+
};
|
|
2357
3199
|
if (loaded !== void 0 && !options.incremental) {
|
|
2358
|
-
return
|
|
3200
|
+
return decision;
|
|
2359
3201
|
}
|
|
2360
|
-
|
|
2361
|
-
const incrementalFile =
|
|
3202
|
+
decision.incremental = true;
|
|
3203
|
+
const incrementalFile = path8.resolve(base, options.incrementalFile);
|
|
2362
3204
|
const recordFile = guardRecordPath(incrementalFile);
|
|
3205
|
+
decision.incrementalFile = incrementalFile;
|
|
3206
|
+
decision.recordFile = recordFile;
|
|
2363
3207
|
let snapshot = null;
|
|
3208
|
+
let root = base;
|
|
2364
3209
|
let failure = "the Stryker config could not be loaded to see where the incremental file is";
|
|
2365
3210
|
if (loaded !== void 0) {
|
|
3211
|
+
const scope = (listed2) => {
|
|
3212
|
+
try {
|
|
3213
|
+
const found = loadedScope(manifest, configs(), listed2);
|
|
3214
|
+
decision.scope = "files" in found ? { files: found.files.size } : found;
|
|
3215
|
+
return "files" in found ? found.files : null;
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
decision.scope = { every: error.message };
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
2366
3221
|
try {
|
|
2367
|
-
|
|
3222
|
+
let widened;
|
|
3223
|
+
({ snapshot, root, widened } = snapshotOf({
|
|
3224
|
+
scope,
|
|
2368
3225
|
dir: manifest.dir,
|
|
2369
3226
|
written: [
|
|
2370
3227
|
incrementalFile,
|
|
2371
3228
|
recordFile,
|
|
2372
|
-
|
|
2373
|
-
|
|
3229
|
+
path8.resolve(base, options.tempDirName),
|
|
3230
|
+
path8.resolve(base, manifest.settings.strykerReportFile)
|
|
2374
3231
|
],
|
|
2375
3232
|
args: comparableArgs(strykerArgs)
|
|
2376
|
-
});
|
|
3233
|
+
}));
|
|
3234
|
+
decision.widened = widened;
|
|
2377
3235
|
} catch (error) {
|
|
2378
3236
|
failure = `the files the run depends on could not be listed (${error.message})`;
|
|
2379
3237
|
}
|
|
2380
3238
|
}
|
|
2381
|
-
|
|
3239
|
+
decision.snapshot = snapshot;
|
|
3240
|
+
decision.failure = snapshot === null ? failure : null;
|
|
3241
|
+
decision.root = root;
|
|
2382
3242
|
const incremental = hashFile(incrementalFile);
|
|
3243
|
+
decision.incrementalHash = incremental;
|
|
2383
3244
|
if (part) {
|
|
2384
|
-
|
|
2385
|
-
return
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
3245
|
+
decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
|
|
3246
|
+
return decision;
|
|
3247
|
+
}
|
|
3248
|
+
let stored;
|
|
3249
|
+
try {
|
|
3250
|
+
stored = JSON.parse(fs8.readFileSync(recordFile, "utf8"));
|
|
3251
|
+
} catch {
|
|
3252
|
+
stored = void 0;
|
|
2389
3253
|
}
|
|
3254
|
+
const ci = decision.ci;
|
|
3255
|
+
let force = null;
|
|
3256
|
+
let plan = null;
|
|
3257
|
+
let last;
|
|
2390
3258
|
if (incremental !== null && !options.force) {
|
|
2391
3259
|
if (snapshot === null) {
|
|
2392
3260
|
force = failure;
|
|
3261
|
+
} else if (!affected) {
|
|
3262
|
+
force = refuseReuse({ record: stored, incremental, snapshot });
|
|
3263
|
+
} else if (ci !== null) {
|
|
3264
|
+
force = ci;
|
|
2393
3265
|
} else {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
}
|
|
2398
|
-
|
|
3266
|
+
const read2 = readRecord({ record: stored, incremental, snapshot });
|
|
3267
|
+
if ("refused" in read2) {
|
|
3268
|
+
force = read2.refused;
|
|
3269
|
+
} else {
|
|
3270
|
+
last = read2.record.affected;
|
|
3271
|
+
try {
|
|
3272
|
+
const inputs = affectedInputs(manifest, {
|
|
3273
|
+
root,
|
|
3274
|
+
files: snapshot.files,
|
|
3275
|
+
configs: configs(),
|
|
3276
|
+
tests: last?.tests ?? [],
|
|
3277
|
+
covering: Object.fromEntries(
|
|
3278
|
+
Object.entries(last?.gates ?? {}).map(([gate, entry]) => [gate, entry.specs])
|
|
3279
|
+
)
|
|
3280
|
+
});
|
|
3281
|
+
plan = planAffected({
|
|
3282
|
+
before: { files: read2.record.snapshot.files, affected: last },
|
|
3283
|
+
now: snapshot.files,
|
|
3284
|
+
subproject: inputs.subproject,
|
|
3285
|
+
config: inputs.config,
|
|
3286
|
+
gates: manifest.gates.map((gate) => ({
|
|
3287
|
+
path: gate.path,
|
|
3288
|
+
files: inputs.gates[gate.path]?.files ?? []
|
|
3289
|
+
}))
|
|
3290
|
+
});
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
if (!(error instanceof AffectedError)) {
|
|
3293
|
+
throw error;
|
|
3294
|
+
}
|
|
3295
|
+
plan = { full: `what a gate depends on cannot be told: ${error.message}` };
|
|
3296
|
+
}
|
|
3297
|
+
if ("full" in plan) {
|
|
3298
|
+
if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
3299
|
+
decision.unchanged = plan.full;
|
|
3300
|
+
plan = null;
|
|
3301
|
+
} else {
|
|
3302
|
+
force = plan.full;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
2399
3305
|
}
|
|
2400
|
-
force = refuseReuse({ record, incremental, snapshot });
|
|
2401
3306
|
}
|
|
3307
|
+
} else if (affected && incremental === null && !options.force) {
|
|
3308
|
+
force = "there is no incremental file of an earlier run";
|
|
3309
|
+
}
|
|
3310
|
+
decision.force = force;
|
|
3311
|
+
decision.plan = plan;
|
|
3312
|
+
decision.last = last;
|
|
3313
|
+
return decision;
|
|
3314
|
+
}
|
|
3315
|
+
function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
3316
|
+
const nothing = { force: null, mutate: null, said: [], record: () => void 0 };
|
|
3317
|
+
const decision = decideReuse(manifest, config, strykerArgs, part, affected);
|
|
3318
|
+
if (!decision.incremental) {
|
|
3319
|
+
return affected ? {
|
|
3320
|
+
...nothing,
|
|
3321
|
+
said: [
|
|
3322
|
+
"test-gates mutation --affected: Stryker does not run in incremental mode, so every mutant is run"
|
|
3323
|
+
]
|
|
3324
|
+
} : nothing;
|
|
3325
|
+
}
|
|
3326
|
+
const { snapshot, root, plan, last, ci, configs, force } = decision;
|
|
3327
|
+
const incrementalFile = decision.incrementalFile;
|
|
3328
|
+
const recordFile = decision.recordFile;
|
|
3329
|
+
const incremental = decision.incrementalHash;
|
|
3330
|
+
const options = { force: decision.forced };
|
|
3331
|
+
if (part) {
|
|
3332
|
+
fs8.rmSync(recordFile, { force: true });
|
|
3333
|
+
return { ...nothing, force };
|
|
2402
3334
|
}
|
|
2403
|
-
|
|
3335
|
+
fs8.rmSync(recordFile, { force: true });
|
|
2404
3336
|
const before = modifiedAt(incrementalFile);
|
|
3337
|
+
const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
|
|
3338
|
+
const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
|
|
3339
|
+
let ranges = [];
|
|
3340
|
+
if (everyGate === null && incremental !== null && !options.force) {
|
|
3341
|
+
const kept = rerun === null ? manifest.gates.map((gate) => gate.path) : manifest.gates.map((gate) => gate.path).filter((gate) => !rerun.includes(gate));
|
|
3342
|
+
try {
|
|
3343
|
+
ranges = timeoutRanges(
|
|
3344
|
+
JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
|
|
3345
|
+
kept
|
|
3346
|
+
);
|
|
3347
|
+
} catch {
|
|
3348
|
+
ranges = [];
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
|
|
3352
|
+
let said = [];
|
|
3353
|
+
if (affected && !options.force) {
|
|
3354
|
+
if (force !== null) {
|
|
3355
|
+
said = describePlan({ full: force });
|
|
3356
|
+
} else if (plan !== null) {
|
|
3357
|
+
said = describePlan(plan, 5, ranges.length);
|
|
3358
|
+
} else if (decision.unchanged !== null) {
|
|
3359
|
+
said = [
|
|
3360
|
+
`test-gates mutation --affected: nothing changed since the last run, every result is reused (gate by gate, ${decision.unchanged.replace(/^what a gate/, "what each gate")})`,
|
|
3361
|
+
...ranges.length > 0 ? [` and ${ranges.length} mutant(s) that timed out in the last run are run again`] : []
|
|
3362
|
+
];
|
|
3363
|
+
}
|
|
3364
|
+
} else if (ranges.length > 0) {
|
|
3365
|
+
said = [
|
|
3366
|
+
`test-gates mutation: ${ranges.length} mutant(s) that timed out in the last run are run again, the other results are reused`
|
|
3367
|
+
];
|
|
3368
|
+
}
|
|
2405
3369
|
return {
|
|
2406
|
-
|
|
2407
|
-
|
|
3370
|
+
// Every gate is run again: the same as a run of every mutant.
|
|
3371
|
+
force: everyGate,
|
|
3372
|
+
mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
|
|
3373
|
+
said,
|
|
3374
|
+
record: (judged) => {
|
|
2408
3375
|
const written = hashFile(incrementalFile);
|
|
2409
3376
|
if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
|
|
2410
3377
|
return;
|
|
2411
3378
|
}
|
|
2412
3379
|
const record = { format: RECORD_FORMAT, incremental: written, snapshot };
|
|
3380
|
+
if (affected && ci === null && judged !== null) {
|
|
3381
|
+
record.affected = affectedRecord(judged, rerun, last);
|
|
3382
|
+
}
|
|
2413
3383
|
try {
|
|
2414
|
-
|
|
3384
|
+
fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
|
|
2415
3385
|
`);
|
|
2416
3386
|
} catch {
|
|
2417
3387
|
}
|
|
2418
3388
|
}
|
|
2419
|
-
};
|
|
3389
|
+
};
|
|
3390
|
+
function affectedRecord(judged, ran, previous) {
|
|
3391
|
+
const gates = manifest.gates.map((gate) => gate.path);
|
|
3392
|
+
const dirty = new Set(
|
|
3393
|
+
[...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
|
|
3394
|
+
);
|
|
3395
|
+
const fresh = coveringSpecs(judged.report, gates);
|
|
3396
|
+
const covering = /* @__PURE__ */ Object.create(null);
|
|
3397
|
+
for (const gate of gates) {
|
|
3398
|
+
const specs = ran === null || ran.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
|
|
3399
|
+
if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
|
|
3400
|
+
covering[gate] = specs;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
try {
|
|
3404
|
+
const inputs = affectedInputs(manifest, {
|
|
3405
|
+
root,
|
|
3406
|
+
files: snapshot.files,
|
|
3407
|
+
configs: configs(),
|
|
3408
|
+
tests: Object.keys(judged.report.testFiles ?? {}),
|
|
3409
|
+
covering
|
|
3410
|
+
});
|
|
3411
|
+
const recorded = /* @__PURE__ */ Object.create(null);
|
|
3412
|
+
for (const gate of Object.keys(covering)) {
|
|
3413
|
+
const entry = inputs.gates[gate];
|
|
3414
|
+
if (entry !== void 0) {
|
|
3415
|
+
recorded[gate] = { specs: entry.specs, files: entry.files };
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
if (error instanceof AffectedError) {
|
|
3421
|
+
return void 0;
|
|
3422
|
+
}
|
|
3423
|
+
throw error;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
3428
|
+
const part = only.length > 0;
|
|
3429
|
+
if (affected && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
3430
|
+
throw new UsageError(
|
|
3431
|
+
"--affected and --mutate cannot be combined: --affected sets --mutate itself"
|
|
3432
|
+
);
|
|
3433
|
+
}
|
|
3434
|
+
if (part) {
|
|
3435
|
+
const refused = gateRunRefused(process.env);
|
|
3436
|
+
if (refused !== null) {
|
|
3437
|
+
throw new UsageError(refused);
|
|
3438
|
+
}
|
|
3439
|
+
if (strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
|
|
3440
|
+
throw new UsageError("--gate and --mutate cannot be combined: --gate sets --mutate itself");
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
const manifest = readManifest(dir);
|
|
3444
|
+
const chosen = selectControls({
|
|
3445
|
+
gates: manifest.gates.map((gate) => gate.path),
|
|
3446
|
+
mode: "all",
|
|
3447
|
+
only,
|
|
3448
|
+
shard: null
|
|
3449
|
+
});
|
|
3450
|
+
if ("error" in chosen) {
|
|
3451
|
+
throw new UsageError(chosen.error);
|
|
3452
|
+
}
|
|
3453
|
+
const config = findStrykerConfig(manifest);
|
|
3454
|
+
if (config === null) {
|
|
3455
|
+
throw new ManifestError(
|
|
3456
|
+
"no Stryker config found (stryker.gates.config.mjs). Create one, or set settings.stryker.configFile"
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
const stryker = findLocalBin(manifest.dir, "stryker");
|
|
3460
|
+
if (stryker === null) {
|
|
3461
|
+
throw new ManifestError(
|
|
3462
|
+
"stryker is not installed in this project (node_modules/.bin/stryker). Add @stryker-mutator/core as a devDependency"
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
const reportFile = reportPathOf(manifest, null);
|
|
3466
|
+
fs8.rmSync(reportFile, { force: true });
|
|
3467
|
+
const guard = guardIncremental(manifest, config, strykerArgs, part, affected);
|
|
3468
|
+
if (guard.said.length > 0) {
|
|
3469
|
+
guard.said.forEach((line) => io2.out(line));
|
|
3470
|
+
} else if (guard.force !== null) {
|
|
3471
|
+
io2.out(
|
|
3472
|
+
`test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
|
|
3476
|
+
const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
|
|
3477
|
+
const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
|
|
3478
|
+
cwd: manifest.dir,
|
|
3479
|
+
stdio: "inherit",
|
|
3480
|
+
shell: process.platform === "win32"
|
|
3481
|
+
});
|
|
3482
|
+
if (result.error) {
|
|
3483
|
+
io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
|
|
3484
|
+
return 1;
|
|
3485
|
+
}
|
|
3486
|
+
if (result.status !== 0) {
|
|
3487
|
+
io2.err(
|
|
3488
|
+
`test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
|
|
3489
|
+
);
|
|
3490
|
+
return 1;
|
|
3491
|
+
}
|
|
3492
|
+
let judged = null;
|
|
3493
|
+
const status = runMutationResult(dir, null, io2, part ? chosen.selected : [], (seen) => {
|
|
3494
|
+
judged = seen;
|
|
3495
|
+
});
|
|
3496
|
+
guard.record(judged);
|
|
3497
|
+
if (part) {
|
|
3498
|
+
fs8.rmSync(reportFile, { force: true });
|
|
3499
|
+
}
|
|
3500
|
+
return status;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// src/commands/doctor.ts
|
|
3504
|
+
function read(file) {
|
|
3505
|
+
try {
|
|
3506
|
+
return fs9.readFileSync(file, "utf8");
|
|
3507
|
+
} catch {
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
function git2(dir, args) {
|
|
3512
|
+
const result = spawnSync3("git", args, {
|
|
3513
|
+
cwd: dir,
|
|
3514
|
+
env: gitEnvironment(process.env),
|
|
3515
|
+
encoding: "utf8"
|
|
3516
|
+
});
|
|
3517
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
3518
|
+
}
|
|
3519
|
+
function hooksOf(dir) {
|
|
3520
|
+
const root = git2(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
|
|
3521
|
+
const hooksDir = git2(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
|
|
3522
|
+
const candidates = [
|
|
3523
|
+
...["pre-push", "pre-commit"].map((name) => path9.join(root, ".husky", name)),
|
|
3524
|
+
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path9.join(hooksDir, name)),
|
|
3525
|
+
...["lefthook.yml", ".lefthook.yml"].map((name) => path9.join(root, name))
|
|
3526
|
+
];
|
|
3527
|
+
let script = "";
|
|
3528
|
+
try {
|
|
3529
|
+
const manifest = JSON.parse(read(path9.join(dir, "package.json")) ?? "{}");
|
|
3530
|
+
script = manifest.scripts?.["test:gates:mutation"] ?? "";
|
|
3531
|
+
} catch {
|
|
3532
|
+
}
|
|
3533
|
+
const subproject = path9.relative(fs9.realpathSync(root), dir).split(path9.sep).join("/");
|
|
3534
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3535
|
+
const uses = [];
|
|
3536
|
+
for (const file of candidates) {
|
|
3537
|
+
const text = read(file);
|
|
3538
|
+
if (text === null) {
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
const real = fs9.realpathSync(file);
|
|
3542
|
+
if (seen.has(real)) {
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
seen.add(real);
|
|
3546
|
+
const shown = path9.relative(root, file).split(path9.sep).join("/");
|
|
3547
|
+
uses.push(...hookUses({ file: shown, text, subproject, script }));
|
|
3548
|
+
}
|
|
3549
|
+
return uses;
|
|
3550
|
+
}
|
|
3551
|
+
function runDoctor(dir, io2) {
|
|
3552
|
+
const manifest = readManifest(dir);
|
|
3553
|
+
const config = findStrykerConfig(manifest);
|
|
3554
|
+
if (config === null) {
|
|
3555
|
+
throw new ManifestError(
|
|
3556
|
+
"no Stryker config found (stryker.gates.config.mjs). Create one, or set settings.stryker.configFile"
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
const plain2 = decideReuse(manifest, config, [], false, false);
|
|
3560
|
+
const affected = decideReuse(manifest, config, [], false, true);
|
|
3561
|
+
io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
|
|
3562
|
+
if (plain2.snapshot !== null) {
|
|
3563
|
+
const subproject = path9.relative(plain2.root, fs9.realpathSync(manifest.dir));
|
|
3564
|
+
const prefix = subproject === "" ? "" : `${subproject.split(path9.sep).join("/")}/`;
|
|
3565
|
+
const keys = Object.keys(plain2.snapshot.files);
|
|
3566
|
+
const outside = keys.filter((key) => !key.startsWith(prefix)).length;
|
|
3567
|
+
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";
|
|
3568
|
+
io2.out(` compared: ${keys.length} files, ${outside} outside the subproject${followed}`);
|
|
3569
|
+
}
|
|
3570
|
+
if (plain2.incremental) {
|
|
3571
|
+
const reuse = plain2.forced ? "the Stryker config sets force" : plain2.force ?? (plain2.incrementalHash === null ? "there is no incremental file of an earlier run" : null);
|
|
3572
|
+
io2.out(
|
|
3573
|
+
reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
|
|
3574
|
+
);
|
|
3575
|
+
if (affected.ci !== null) {
|
|
3576
|
+
io2.out(` next run with --affected: every mutant, ${affected.ci}`);
|
|
3577
|
+
} else if (affected.plan !== null && "rerun" in affected.plan) {
|
|
3578
|
+
const [first = "", ...rest] = describePlan(affected.plan);
|
|
3579
|
+
io2.out(
|
|
3580
|
+
` next run with --affected: ${first.replace(/^test-gates mutation --affected: /, "")}`
|
|
3581
|
+
);
|
|
3582
|
+
rest.forEach((line) => io2.out(` ${line}`));
|
|
3583
|
+
} else if (affected.unchanged !== null) {
|
|
3584
|
+
io2.out(" next run with --affected: reuses the results of the last run (nothing changed)");
|
|
3585
|
+
} else {
|
|
3586
|
+
const why = affected.forced ? "the Stryker config sets force" : affected.force;
|
|
3587
|
+
io2.out(` next run with --affected: every mutant, ${why ?? "there is nothing to reuse"}`);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
let blind = null;
|
|
3591
|
+
if (plain2.snapshot !== null) {
|
|
3592
|
+
try {
|
|
3593
|
+
affectedInputs(manifest, {
|
|
3594
|
+
root: plain2.root,
|
|
3595
|
+
files: plain2.snapshot.files,
|
|
3596
|
+
configs: plain2.configs(),
|
|
3597
|
+
tests: [],
|
|
3598
|
+
covering: {}
|
|
3599
|
+
});
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
if (!(error instanceof AffectedError)) {
|
|
3602
|
+
throw error;
|
|
3603
|
+
}
|
|
3604
|
+
blind = error.message;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
const { lines, status } = formatFindings(
|
|
3608
|
+
diagnose({
|
|
3609
|
+
incremental: plain2.incremental,
|
|
3610
|
+
forced: plain2.forced,
|
|
3611
|
+
failure: plain2.failure,
|
|
3612
|
+
widened: plain2.widened,
|
|
3613
|
+
scope: plain2.scope,
|
|
3614
|
+
hooks: hooksOf(fs9.realpathSync(manifest.dir)),
|
|
3615
|
+
blind
|
|
3616
|
+
})
|
|
3617
|
+
);
|
|
3618
|
+
lines.forEach((line) => io2.out(line));
|
|
3619
|
+
return status;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// src/commands/lcov.ts
|
|
3623
|
+
import fs10 from "fs";
|
|
3624
|
+
import path10 from "path";
|
|
3625
|
+
|
|
3626
|
+
// src/core/lcov.ts
|
|
3627
|
+
function normalizePath2(file, rootDir) {
|
|
3628
|
+
let normalized = file.replace(/\\/g, "/");
|
|
3629
|
+
if (rootDir !== void 0) {
|
|
3630
|
+
const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3631
|
+
if (normalized.startsWith(`${root}/`)) {
|
|
3632
|
+
normalized = normalized.slice(root.length + 1);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return normalized.replace(/^\.\//, "");
|
|
3636
|
+
}
|
|
3637
|
+
function parseLcov(text, rootDir) {
|
|
3638
|
+
const files = /* @__PURE__ */ new Map();
|
|
3639
|
+
let record = null;
|
|
3640
|
+
const closeRecord = () => {
|
|
3641
|
+
if (record && !record.hasDa) {
|
|
3642
|
+
record.file.found += record.found;
|
|
3643
|
+
record.file.hit += record.hit;
|
|
3644
|
+
}
|
|
3645
|
+
record = null;
|
|
3646
|
+
};
|
|
3647
|
+
for (const rawLine of text.split("\n")) {
|
|
3648
|
+
const line = rawLine.trim();
|
|
3649
|
+
if (line.startsWith("SF:")) {
|
|
3650
|
+
closeRecord();
|
|
3651
|
+
const path13 = normalizePath2(line.slice(3), rootDir);
|
|
3652
|
+
const file = files.get(path13) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
3653
|
+
files.set(path13, file);
|
|
3654
|
+
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
3655
|
+
} else if (line === "end_of_record") {
|
|
3656
|
+
closeRecord();
|
|
3657
|
+
} else if (record) {
|
|
3658
|
+
const open = record;
|
|
3659
|
+
if (line.startsWith("DA:")) {
|
|
3660
|
+
const [lineNumber, hits] = line.slice(3).split(",");
|
|
3661
|
+
const number = Number(lineNumber);
|
|
3662
|
+
const count = Number(hits);
|
|
3663
|
+
if (Number.isInteger(number) && Number.isFinite(count)) {
|
|
3664
|
+
open.hasDa = true;
|
|
3665
|
+
open.file.lines.set(number, Math.max(open.file.lines.get(number) ?? 0, count));
|
|
3666
|
+
}
|
|
3667
|
+
} else if (line.startsWith("LF:")) {
|
|
3668
|
+
open.found = Number(line.slice(3)) || 0;
|
|
3669
|
+
} else if (line.startsWith("LH:")) {
|
|
3670
|
+
open.hit = Number(line.slice(3)) || 0;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
closeRecord();
|
|
3675
|
+
const result = /* @__PURE__ */ new Map();
|
|
3676
|
+
for (const [file, accumulator] of files) {
|
|
3677
|
+
const uncovered = [...accumulator.lines.entries()].filter(([, hits]) => hits <= 0).map(([lineNumber]) => lineNumber).sort((a, b) => a - b);
|
|
3678
|
+
result.set(file, {
|
|
3679
|
+
found: accumulator.lines.size + accumulator.found,
|
|
3680
|
+
hit: accumulator.lines.size - uncovered.length + accumulator.hit,
|
|
3681
|
+
uncovered
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
return result;
|
|
2420
3685
|
}
|
|
2421
|
-
function
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3686
|
+
function judgeLcov(gatePaths, coverage) {
|
|
3687
|
+
const violations = [];
|
|
3688
|
+
const results = [];
|
|
3689
|
+
for (const gatePath of gatePaths) {
|
|
3690
|
+
const file = coverage.get(normalizePath2(gatePath));
|
|
3691
|
+
if (!file) {
|
|
3692
|
+
violations.push({ file: gatePath, message: "no record in the lcov file" });
|
|
3693
|
+
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
3694
|
+
continue;
|
|
2427
3695
|
}
|
|
2428
|
-
if (
|
|
2429
|
-
|
|
3696
|
+
if (file.found === 0) {
|
|
3697
|
+
violations.push({ file: gatePath, message: "no instrumented lines in the lcov record" });
|
|
3698
|
+
results.push({ path: gatePath, found: 0, hit: 0, ok: false });
|
|
3699
|
+
continue;
|
|
2430
3700
|
}
|
|
3701
|
+
const ok = file.hit === file.found;
|
|
3702
|
+
if (!ok) {
|
|
3703
|
+
const shown = file.uncovered.slice(0, 20).join(", ");
|
|
3704
|
+
const more = file.uncovered.length > 20 ? ", \u2026" : "";
|
|
3705
|
+
violations.push({
|
|
3706
|
+
file: gatePath,
|
|
3707
|
+
...file.uncovered[0] === void 0 ? {} : { line: file.uncovered[0] },
|
|
3708
|
+
message: `${file.hit}/${file.found} lines covered` + (shown === "" ? "" : ` (uncovered: ${shown}${more})`)
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
results.push({ path: gatePath, found: file.found, hit: file.hit, ok });
|
|
2431
3712
|
}
|
|
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
|
-
);
|
|
3713
|
+
return { violations, results };
|
|
3714
|
+
}
|
|
3715
|
+
function summarizeLcov(coverage, exclude) {
|
|
3716
|
+
let found = 0;
|
|
3717
|
+
let hit = 0;
|
|
3718
|
+
for (const [file, lines] of coverage) {
|
|
3719
|
+
if (exclude.some((regex) => regex.test(file))) {
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
found += lines.found;
|
|
3723
|
+
hit += lines.hit;
|
|
2447
3724
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
3725
|
+
return { found, hit };
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/commands/lcov.ts
|
|
3729
|
+
function runLcov(dir, fileOption, io2) {
|
|
3730
|
+
const manifest = readManifest(dir);
|
|
3731
|
+
const file = fileOption ?? manifest.settings.lcovFile;
|
|
3732
|
+
if (file === null) {
|
|
2450
3733
|
throw new ManifestError(
|
|
2451
|
-
"
|
|
3734
|
+
"no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
|
|
2452
3735
|
);
|
|
2453
3736
|
}
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
3737
|
+
const lcovPath = path10.resolve(manifest.dir, file);
|
|
3738
|
+
let text;
|
|
3739
|
+
try {
|
|
3740
|
+
text = fs10.readFileSync(lcovPath, "utf8");
|
|
3741
|
+
} catch (error) {
|
|
3742
|
+
io2.err(
|
|
3743
|
+
`test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
|
|
2460
3744
|
);
|
|
3745
|
+
return 1;
|
|
2461
3746
|
}
|
|
2462
|
-
const
|
|
2463
|
-
const
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3747
|
+
const violations = [...manifest.violations];
|
|
3748
|
+
const listing = /* @__PURE__ */ new Map();
|
|
3749
|
+
const existing = manifest.gates.filter((gate) => {
|
|
3750
|
+
const exists = existsExact(manifest.dir, gate.path, "file", listing);
|
|
3751
|
+
if (!exists) {
|
|
3752
|
+
violations.push({
|
|
3753
|
+
file: gate.path,
|
|
3754
|
+
message: "file not found (the check is case-sensitive). Update test-gates.json if it moved"
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
return exists;
|
|
2468
3758
|
});
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3759
|
+
const coverage = parseLcov(text, manifest.dir);
|
|
3760
|
+
const total = summarizeLcov(coverage, manifest.settings.lcovSummaryExclude);
|
|
3761
|
+
io2.out(
|
|
3762
|
+
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)`
|
|
3763
|
+
);
|
|
3764
|
+
const verdict = judgeLcov(
|
|
3765
|
+
existing.map((gate) => gate.path),
|
|
3766
|
+
coverage
|
|
3767
|
+
);
|
|
3768
|
+
for (const result of verdict.results) {
|
|
3769
|
+
if (result.ok) {
|
|
3770
|
+
io2.out(` \u2713 ${result.path}: ${result.hit}/${result.found} lines`);
|
|
3771
|
+
}
|
|
2472
3772
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
);
|
|
3773
|
+
violations.push(...verdict.violations);
|
|
3774
|
+
if (violations.length > 0) {
|
|
3775
|
+
printViolations(io2, "lcov", violations);
|
|
2477
3776
|
return 1;
|
|
2478
3777
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
if (part) {
|
|
2482
|
-
fs7.rmSync(reportFile, { force: true });
|
|
2483
|
-
}
|
|
2484
|
-
return status;
|
|
3778
|
+
io2.out(`test-gates lcov: OK (${verdict.results.length} gate(s) at 100% line coverage)`);
|
|
3779
|
+
return 0;
|
|
2485
3780
|
}
|
|
2486
3781
|
|
|
2487
3782
|
// src/commands/selfcheck.ts
|
|
2488
|
-
import { spawn, spawnSync as
|
|
2489
|
-
import
|
|
3783
|
+
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
3784
|
+
import fs12 from "fs";
|
|
2490
3785
|
import os2 from "os";
|
|
2491
|
-
import
|
|
3786
|
+
import path12 from "path";
|
|
2492
3787
|
|
|
2493
3788
|
// src/core/jobs.ts
|
|
2494
3789
|
var SWITCHES = {
|
|
@@ -2557,7 +3852,8 @@ var REACHES = [
|
|
|
2557
3852
|
"eval(",
|
|
2558
3853
|
"Function("
|
|
2559
3854
|
];
|
|
2560
|
-
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])/;
|
|
3855
|
+
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
|
|
3856
|
+
var COMPUTED = "loads a module whose name is computed";
|
|
2561
3857
|
var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
|
|
2562
3858
|
function reachesOutside(file, trusted) {
|
|
2563
3859
|
const dense = file.source.replace(/\s/g, "");
|
|
@@ -2566,7 +3862,7 @@ function reachesOutside(file, trusted) {
|
|
|
2566
3862
|
return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
|
|
2567
3863
|
}
|
|
2568
3864
|
if (COMPUTED_LOAD.test(dense)) {
|
|
2569
|
-
return
|
|
3865
|
+
return COMPUTED;
|
|
2570
3866
|
}
|
|
2571
3867
|
for (const { specifier, builtin } of file.imports) {
|
|
2572
3868
|
if (builtin) {
|
|
@@ -2590,17 +3886,47 @@ function jobsRefused(input) {
|
|
|
2590
3886
|
if (input.configSource === null || !input.configSource.includes(helper)) {
|
|
2591
3887
|
return `the gate config does not use ${helper}`;
|
|
2592
3888
|
}
|
|
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
3889
|
return null;
|
|
2603
3890
|
}
|
|
3891
|
+
function jobsPlan(input) {
|
|
3892
|
+
const byPath = new Map(input.files.map((file) => [file.path, file]));
|
|
3893
|
+
const causes = input.roots.flatMap((root) => {
|
|
3894
|
+
const loaded = root.files.flatMap((name) => {
|
|
3895
|
+
const file = byPath.get(name);
|
|
3896
|
+
return file === void 0 ? [] : [file];
|
|
3897
|
+
});
|
|
3898
|
+
let computed = false;
|
|
3899
|
+
const reaches = loaded.flatMap((file) => {
|
|
3900
|
+
const reach = reachesOutside(file, input.trusted);
|
|
3901
|
+
const said = reach === null ? [] : [`${file.path} ${reach}`];
|
|
3902
|
+
if (!COMPUTED_LOAD.test(file.source.replace(/\s/g, ""))) {
|
|
3903
|
+
return said;
|
|
3904
|
+
}
|
|
3905
|
+
computed = true;
|
|
3906
|
+
return reach === COMPUTED ? said : [...said, `${file.path} ${COMPUTED}`];
|
|
3907
|
+
});
|
|
3908
|
+
const reasons = [
|
|
3909
|
+
...root.lost === null ? [] : [`${root.lost} cannot be followed`],
|
|
3910
|
+
...reaches
|
|
3911
|
+
];
|
|
3912
|
+
if (reasons.length === 0) {
|
|
3913
|
+
return [];
|
|
3914
|
+
}
|
|
3915
|
+
const any = root.spec === null || root.lost !== null || computed;
|
|
3916
|
+
const gates = input.gates.filter(
|
|
3917
|
+
(gate) => gate.spec !== root.spec && (any || root.files.includes(withoutDots(gate.path)))
|
|
3918
|
+
).map((gate) => gate.path);
|
|
3919
|
+
const by = root.spec === null ? "run with what the gate config names" : `${any ? "maybe loaded" : "loaded"} by ${root.spec}`;
|
|
3920
|
+
return gates.length === 0 ? [] : [{ gates, reason: `${by}, in which ${reasons.join("; ")}` }];
|
|
3921
|
+
});
|
|
3922
|
+
const kept = new Set(causes.flatMap((cause) => cause.gates));
|
|
3923
|
+
const paths = input.gates.map((gate) => gate.path);
|
|
3924
|
+
return {
|
|
3925
|
+
atOnce: paths.filter((gate) => !kept.has(gate)),
|
|
3926
|
+
alone: paths.filter((gate) => kept.has(gate)),
|
|
3927
|
+
causes
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
2604
3930
|
var TROUBLE = [
|
|
2605
3931
|
"fail",
|
|
2606
3932
|
"fails",
|
|
@@ -2674,8 +4000,8 @@ function cleanHold(input) {
|
|
|
2674
4000
|
|
|
2675
4001
|
// src/touch.ts
|
|
2676
4002
|
import { createHash as createHash2 } from "crypto";
|
|
2677
|
-
import
|
|
2678
|
-
import
|
|
4003
|
+
import fs11 from "fs";
|
|
4004
|
+
import path11 from "path";
|
|
2679
4005
|
import { fileURLToPath } from "url";
|
|
2680
4006
|
var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
|
|
2681
4007
|
function isObject(value) {
|
|
@@ -2707,18 +4033,18 @@ function isCounters(value) {
|
|
|
2707
4033
|
);
|
|
2708
4034
|
}
|
|
2709
4035
|
function readNotes(directory) {
|
|
2710
|
-
const
|
|
4036
|
+
const read2 = (file) => {
|
|
2711
4037
|
try {
|
|
2712
|
-
return JSON.parse(
|
|
4038
|
+
return JSON.parse(fs11.readFileSync(path11.join(directory, file), "utf8"));
|
|
2713
4039
|
} catch {
|
|
2714
4040
|
return null;
|
|
2715
4041
|
}
|
|
2716
4042
|
};
|
|
2717
|
-
const run =
|
|
4043
|
+
const run = read2("run.json");
|
|
2718
4044
|
let notes = [];
|
|
2719
4045
|
try {
|
|
2720
|
-
for (const name of
|
|
2721
|
-
const note =
|
|
4046
|
+
for (const name of fs11.readdirSync(path11.join(directory, "specs"))) {
|
|
4047
|
+
const note = read2(path11.join("specs", name));
|
|
2722
4048
|
if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
|
|
2723
4049
|
throw new Error("not a note");
|
|
2724
4050
|
}
|
|
@@ -2730,135 +4056,8 @@ function readNotes(directory) {
|
|
|
2730
4056
|
return {
|
|
2731
4057
|
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
4058
|
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
|
-
}
|
|
4059
|
+
totals: countersOfAll(read2(path11.join("coverage", "coverage-final.json")))
|
|
2831
4060
|
};
|
|
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
4061
|
}
|
|
2863
4062
|
|
|
2864
4063
|
// src/commands/selfcheck.ts
|
|
@@ -2872,7 +4071,7 @@ function resolveGateCommand(manifest) {
|
|
|
2872
4071
|
if (manifest.settings.gateCommand !== null) {
|
|
2873
4072
|
return manifest.settings.gateCommand;
|
|
2874
4073
|
}
|
|
2875
|
-
const exists = (name) =>
|
|
4074
|
+
const exists = (name) => fs12.existsSync(path12.join(manifest.dir, name));
|
|
2876
4075
|
const jest = JEST_CONFIGS.find(exists);
|
|
2877
4076
|
const vitest = VITEST_CONFIGS.find(exists);
|
|
2878
4077
|
if (jest && vitest) {
|
|
@@ -2893,7 +4092,7 @@ function resolveGateCommand(manifest) {
|
|
|
2893
4092
|
var TOUCH_PREFIX = ".test-gates-touch-";
|
|
2894
4093
|
function realPath(file) {
|
|
2895
4094
|
try {
|
|
2896
|
-
return
|
|
4095
|
+
return fs12.realpathSync(file);
|
|
2897
4096
|
} catch {
|
|
2898
4097
|
return file;
|
|
2899
4098
|
}
|
|
@@ -2901,13 +4100,13 @@ function realPath(file) {
|
|
|
2901
4100
|
function shareOneRun(manifest, targets, runGate) {
|
|
2902
4101
|
let touchDir;
|
|
2903
4102
|
try {
|
|
2904
|
-
for (const name of
|
|
2905
|
-
const left =
|
|
2906
|
-
if (name.startsWith(TOUCH_PREFIX) && Date.now() -
|
|
2907
|
-
|
|
4103
|
+
for (const name of fs12.readdirSync(manifest.dir)) {
|
|
4104
|
+
const left = path12.join(manifest.dir, name);
|
|
4105
|
+
if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
|
|
4106
|
+
fs12.rmSync(left, { recursive: true, force: true });
|
|
2908
4107
|
}
|
|
2909
4108
|
}
|
|
2910
|
-
touchDir =
|
|
4109
|
+
touchDir = fs12.mkdtempSync(path12.join(manifest.dir, TOUCH_PREFIX));
|
|
2911
4110
|
} catch {
|
|
2912
4111
|
return { reason: "the subproject cannot be written to" };
|
|
2913
4112
|
}
|
|
@@ -2918,14 +4117,14 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2918
4117
|
return { reason: "the gate run with every spec does not pass" };
|
|
2919
4118
|
}
|
|
2920
4119
|
const base = realPath(manifest.dir);
|
|
2921
|
-
const absolute = (file) => realPath(
|
|
4120
|
+
const absolute = (file) => realPath(path12.resolve(base, file));
|
|
2922
4121
|
const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
|
|
2923
4122
|
const left = readNotes(touchDir);
|
|
2924
4123
|
plan = planControls({
|
|
2925
4124
|
all: manifest.gates.map((gate) => ({
|
|
2926
4125
|
path: gate.path,
|
|
2927
|
-
file:
|
|
2928
|
-
specFile:
|
|
4126
|
+
file: path12.join(base, gate.path),
|
|
4127
|
+
specFile: path12.join(base, gate.spec ?? "")
|
|
2929
4128
|
})),
|
|
2930
4129
|
targets: targets.map((gate) => gate.path),
|
|
2931
4130
|
run: left.run,
|
|
@@ -2934,7 +4133,7 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2934
4133
|
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
2935
4134
|
});
|
|
2936
4135
|
} finally {
|
|
2937
|
-
|
|
4136
|
+
fs12.rmSync(touchDir, { recursive: true, force: true });
|
|
2938
4137
|
}
|
|
2939
4138
|
if (plan.reason !== null) {
|
|
2940
4139
|
return { reason: plan.reason };
|
|
@@ -2944,15 +4143,15 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
2944
4143
|
return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
|
|
2945
4144
|
}
|
|
2946
4145
|
function installedVersion2(start, name) {
|
|
2947
|
-
let current =
|
|
4146
|
+
let current = path12.resolve(start);
|
|
2948
4147
|
for (; ; ) {
|
|
2949
4148
|
try {
|
|
2950
4149
|
const manifest = JSON.parse(
|
|
2951
|
-
|
|
4150
|
+
fs12.readFileSync(path12.join(current, "node_modules", name, "package.json"), "utf8")
|
|
2952
4151
|
);
|
|
2953
4152
|
return typeof manifest.version === "string" ? manifest.version : null;
|
|
2954
4153
|
} catch {
|
|
2955
|
-
const parent =
|
|
4154
|
+
const parent = path12.dirname(current);
|
|
2956
4155
|
if (parent === current) {
|
|
2957
4156
|
return null;
|
|
2958
4157
|
}
|
|
@@ -2981,13 +4180,13 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
2981
4180
|
const results = /* @__PURE__ */ new Map();
|
|
2982
4181
|
let root;
|
|
2983
4182
|
try {
|
|
2984
|
-
for (const name of
|
|
2985
|
-
const left =
|
|
2986
|
-
if (name.startsWith(JOBS_PREFIX) && Date.now() -
|
|
2987
|
-
|
|
4183
|
+
for (const name of fs12.readdirSync(os2.tmpdir())) {
|
|
4184
|
+
const left = path12.join(os2.tmpdir(), name);
|
|
4185
|
+
if (name.startsWith(JOBS_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
|
|
4186
|
+
fs12.rmSync(left, { recursive: true, force: true });
|
|
2988
4187
|
}
|
|
2989
4188
|
}
|
|
2990
|
-
root =
|
|
4189
|
+
root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
2991
4190
|
} catch {
|
|
2992
4191
|
return results;
|
|
2993
4192
|
}
|
|
@@ -3019,11 +4218,11 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
3019
4218
|
await Promise.all(
|
|
3020
4219
|
Array.from(
|
|
3021
4220
|
{ length: Math.min(jobs, gates.length) },
|
|
3022
|
-
(_unused, index) => line(
|
|
4221
|
+
(_unused, index) => line(path12.join(root, `line-${index}`))
|
|
3023
4222
|
)
|
|
3024
4223
|
);
|
|
3025
4224
|
} finally {
|
|
3026
|
-
|
|
4225
|
+
fs12.rmSync(root, { recursive: true, force: true });
|
|
3027
4226
|
}
|
|
3028
4227
|
return results;
|
|
3029
4228
|
}
|
|
@@ -3052,7 +4251,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
3052
4251
|
const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
|
|
3053
4252
|
const [program, ...args] = command;
|
|
3054
4253
|
const executable = findLocalBin(manifest.dir, program) ?? program;
|
|
3055
|
-
const runGate = (env) =>
|
|
4254
|
+
const runGate = (env) => spawnSync4(executable, args, {
|
|
3056
4255
|
cwd: manifest.dir,
|
|
3057
4256
|
env: {
|
|
3058
4257
|
...process.env,
|
|
@@ -3098,48 +4297,60 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
3098
4297
|
const jobs = selection.jobs ?? 1;
|
|
3099
4298
|
if (jobs > 1 && waiting.length > 1) {
|
|
3100
4299
|
const known = knownCommand(program, args);
|
|
3101
|
-
const
|
|
3102
|
-
const ask = (loaded) => jobsRefused({
|
|
4300
|
+
const refusal = jobsRefused({
|
|
3103
4301
|
failurePattern: manifest.settings.selfcheckFailurePattern,
|
|
3104
4302
|
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
|
-
) : []
|
|
4303
|
+
configSource: gateConfigSource(manifest, args)
|
|
3110
4304
|
});
|
|
3111
|
-
const refusal = ask({ files: [], lost: null }) ?? ask(loadedFiles(manifest, configFile));
|
|
3112
4305
|
if (refusal !== null || !("runner" in known)) {
|
|
3113
4306
|
io2.out(`test-gates selfcheck: one negative control at a time (${refusal})`);
|
|
3114
4307
|
} 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) {
|
|
4308
|
+
const plan = jobsPlan({
|
|
4309
|
+
gates: waiting.map((gate) => ({ path: gate.path, spec: gate.spec })),
|
|
4310
|
+
...loadedFiles(manifest, gateConfigFile(args)),
|
|
4311
|
+
trusted: manifest.settings.importMode === "allowlist" ? manifest.settings.importAllow.flatMap(
|
|
4312
|
+
(rule) => rule.module === void 0 ? [] : [rule.module]
|
|
4313
|
+
) : []
|
|
4314
|
+
});
|
|
4315
|
+
if (plan.alone.length > 0) {
|
|
3140
4316
|
io2.out(
|
|
3141
|
-
`test-gates selfcheck: ${
|
|
4317
|
+
`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
4318
|
);
|
|
4319
|
+
for (const { gates, reason } of plan.causes) {
|
|
4320
|
+
io2.out(` ${gates.join(", ")}: ${reason}`);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
const atOnce = waiting.filter((gate) => plan.atOnce.includes(gate.path));
|
|
4324
|
+
if (atOnce.length > 1) {
|
|
4325
|
+
const specs = new Set(manifest.gates.map((gate) => gate.spec)).size - 1;
|
|
4326
|
+
const names = [
|
|
4327
|
+
manifest.dir,
|
|
4328
|
+
realPath(manifest.dir),
|
|
4329
|
+
...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
|
|
4330
|
+
].sort((left, right) => right.length - left.length);
|
|
4331
|
+
const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
|
|
4332
|
+
for (const [gate, result] of results) {
|
|
4333
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
4334
|
+
const clean = cleanHold({
|
|
4335
|
+
held: judgeSelfcheck({ status: result.status, output, gatePath: gate.path }) === null,
|
|
4336
|
+
signal: result.signal,
|
|
4337
|
+
output,
|
|
4338
|
+
runner: known.runner,
|
|
4339
|
+
specs,
|
|
4340
|
+
gatePath: gate.path,
|
|
4341
|
+
others: manifest.gates.map((other) => other.path).filter((other) => other !== gate.path),
|
|
4342
|
+
names
|
|
4343
|
+
});
|
|
4344
|
+
if (clean) {
|
|
4345
|
+
heldAtOnce.set(gate.path, result.status);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
const again = atOnce.length - heldAtOnce.size;
|
|
4349
|
+
if (again > 0) {
|
|
4350
|
+
io2.out(
|
|
4351
|
+
`test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
|
|
4352
|
+
);
|
|
4353
|
+
}
|
|
3143
4354
|
}
|
|
3144
4355
|
}
|
|
3145
4356
|
}
|
|
@@ -3210,6 +4421,8 @@ Commands:
|
|
|
3210
4421
|
mutation-result Judge an existing Stryker JSON report
|
|
3211
4422
|
selfcheck [-- <command>] Negative control: leave one spec out, the gate must fail
|
|
3212
4423
|
lcov [--file <lcov.info>] Per-file 100% line coverage from an lcov tracefile
|
|
4424
|
+
doctor Why a mutation run would not reuse the last results, and what
|
|
4425
|
+
to change. Runs nothing; exit 1 when runs are full ones needlessly
|
|
3213
4426
|
|
|
3214
4427
|
Options:
|
|
3215
4428
|
--dir <subproject> Directory that holds test-gates.json (default: .)
|
|
@@ -3218,6 +4431,9 @@ Options:
|
|
|
3218
4431
|
--gate <path> selfcheck: only this gate (may be given more than once).
|
|
3219
4432
|
mutation: only the mutants of this gate, while working on it.
|
|
3220
4433
|
Not the final check, and refused when CI is set
|
|
4434
|
+
--affected mutation: run again only the gates that a change since the last
|
|
4435
|
+
run can affect, and reuse the results of the others. Every
|
|
4436
|
+
mutant is run when CI is set
|
|
3221
4437
|
--shard <index>/<total> selfcheck: one of <total> parts of the gates, for parallel CI jobs
|
|
3222
4438
|
--jobs <n> selfcheck: run <n> negative controls at the same time. One that does
|
|
3223
4439
|
not hold cleanly there is run again on its own
|
|
@@ -3250,8 +4466,10 @@ function main(argv) {
|
|
|
3250
4466
|
switch (args.command) {
|
|
3251
4467
|
case "check":
|
|
3252
4468
|
return runCheck(args.dir, io);
|
|
4469
|
+
case "doctor":
|
|
4470
|
+
return runDoctor(args.dir, io);
|
|
3253
4471
|
case "mutation":
|
|
3254
|
-
return runMutation(args.dir, args.rest, io, args.gates);
|
|
4472
|
+
return runMutation(args.dir, args.rest, io, args.gates, args.affected);
|
|
3255
4473
|
case "mutation-result":
|
|
3256
4474
|
return runMutationResult(args.dir, args.report, io);
|
|
3257
4475
|
case "selfcheck":
|