@t09tanaka/test-gates 0.4.1 → 0.4.3
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 +69 -0
- package/README.md +66 -19
- package/dist/cli.js +2392 -786
- package/dist/index.cjs +61 -44
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +61 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -319,14 +319,17 @@ function parseImports(source) {
|
|
|
319
319
|
const clause = match[3];
|
|
320
320
|
const module = match[5];
|
|
321
321
|
const braces = /\{([^}]*)\}/.exec(clause);
|
|
322
|
-
const
|
|
322
|
+
const entries = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "") : [];
|
|
323
|
+
const names = entries.filter((part) => !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim());
|
|
323
324
|
const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
|
|
324
325
|
found.push({
|
|
325
326
|
module,
|
|
326
327
|
line: lineAt(source, match.index + match[0].lastIndexOf(module)),
|
|
327
328
|
typeOnly: match[2] !== void 0,
|
|
328
329
|
names,
|
|
329
|
-
whole: outsideBraces !== ""
|
|
330
|
+
whole: outsideBraces !== "",
|
|
331
|
+
...match[2] === void 0 && outsideBraces === "" && entries.length > 0 && // `type as kind` imports the value named `type`.
|
|
332
|
+
entries.every((part) => /^type\s+(?!as\s)[\w$]/.test(part)) ? { inlineTypes: true } : {}
|
|
330
333
|
});
|
|
331
334
|
}
|
|
332
335
|
for (const match of source.matchAll(SIDE_EFFECT_IMPORT)) {
|
|
@@ -341,6 +344,9 @@ function parseImports(source) {
|
|
|
341
344
|
}
|
|
342
345
|
for (const match of source.matchAll(CALL_IMPORT)) {
|
|
343
346
|
const module = match[2];
|
|
347
|
+
if (match[1] === "`" && module.includes("${")) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
344
350
|
found.push({
|
|
345
351
|
module,
|
|
346
352
|
line: lineAt(source, match.index + match[0].lastIndexOf(module)),
|
|
@@ -351,7 +357,7 @@ function parseImports(source) {
|
|
|
351
357
|
}
|
|
352
358
|
return found.sort((a, b) => a.line - b.line);
|
|
353
359
|
}
|
|
354
|
-
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(
|
|
360
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\((?!\s*(?:\)|(?:'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\$]|\\.|\$(?!\{))*`)\s*\)))/g;
|
|
355
361
|
function findUncheckableImports(source) {
|
|
356
362
|
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
357
363
|
}
|
|
@@ -378,7 +384,11 @@ function resolveSpecifier(specifier, fromFile, aliases) {
|
|
|
378
384
|
const segments = [];
|
|
379
385
|
for (const segment of joined.split("/")) {
|
|
380
386
|
if (segment === "..") {
|
|
381
|
-
segments.
|
|
387
|
+
if (segments.length === 0 || segments[segments.length - 1] === "..") {
|
|
388
|
+
segments.push(segment);
|
|
389
|
+
} else {
|
|
390
|
+
segments.pop();
|
|
391
|
+
}
|
|
382
392
|
} else if (segment !== "." && segment !== "") {
|
|
383
393
|
segments.push(segment);
|
|
384
394
|
}
|
|
@@ -423,8 +433,8 @@ function findImporters(gatePath, others, aliases) {
|
|
|
423
433
|
if (imported.typeOnly) {
|
|
424
434
|
return false;
|
|
425
435
|
}
|
|
426
|
-
const
|
|
427
|
-
return
|
|
436
|
+
const resolved2 = resolveSpecifier(imported.module, other.path, aliases);
|
|
437
|
+
return resolved2 === target || resolved2 === asDirectory;
|
|
428
438
|
})
|
|
429
439
|
).map((other) => other.path);
|
|
430
440
|
}
|
|
@@ -555,8 +565,8 @@ function scanSpecSource(gatePath, specPath, source, settings) {
|
|
|
555
565
|
const gateDirectory = gateModule.replace(/\/index$/, "");
|
|
556
566
|
for (const match of source.matchAll(MODULE_MOCK)) {
|
|
557
567
|
const specifier = match[2];
|
|
558
|
-
const
|
|
559
|
-
if (
|
|
568
|
+
const resolved2 = resolveSpecifier(specifier, specPath, settings.importAliases);
|
|
569
|
+
if (resolved2 === gateModule || resolved2 === gateDirectory) {
|
|
560
570
|
let line = 1;
|
|
561
571
|
for (let i = 0; i < match.index; i += 1) {
|
|
562
572
|
if (source[i] === "\n") {
|
|
@@ -1024,13 +1034,13 @@ function judgeSelfcheck(input) {
|
|
|
1024
1034
|
function selectControls(input) {
|
|
1025
1035
|
const { gates, mode, only, shard } = input;
|
|
1026
1036
|
if (only.length > 0) {
|
|
1027
|
-
const unknown = only.filter((
|
|
1037
|
+
const unknown = only.filter((path15) => !gates.includes(path15));
|
|
1028
1038
|
if (unknown.length > 0) {
|
|
1029
1039
|
return {
|
|
1030
1040
|
error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
|
|
1031
1041
|
};
|
|
1032
1042
|
}
|
|
1033
|
-
return { selected: gates.filter((
|
|
1043
|
+
return { selected: gates.filter((path15) => only.includes(path15)) };
|
|
1034
1044
|
}
|
|
1035
1045
|
if (shard !== null) {
|
|
1036
1046
|
if (shard.total > gates.length) {
|
|
@@ -1183,8 +1193,8 @@ function planControls(input) {
|
|
|
1183
1193
|
if (silent !== void 0) {
|
|
1184
1194
|
return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
|
|
1185
1195
|
}
|
|
1186
|
-
const
|
|
1187
|
-
const short = all.find((gate) => accountedFor(totals[gate.file],
|
|
1196
|
+
const partsOf3 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
|
|
1197
|
+
const short = all.find((gate) => accountedFor(totals[gate.file], partsOf3(gate)) === "no");
|
|
1188
1198
|
if (short !== void 0) {
|
|
1189
1199
|
return everyoneAlone(
|
|
1190
1200
|
`the notes of the specs do not add up to what the runner counted for ${short.path}`
|
|
@@ -1198,7 +1208,7 @@ function planControls(input) {
|
|
|
1198
1208
|
if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
|
|
1199
1209
|
return false;
|
|
1200
1210
|
}
|
|
1201
|
-
if (accountedFor(totals[gate.file],
|
|
1211
|
+
if (accountedFor(totals[gate.file], partsOf3(gate)) !== "yes") {
|
|
1202
1212
|
return false;
|
|
1203
1213
|
}
|
|
1204
1214
|
const touching = notes.filter((note) => {
|
|
@@ -1409,207 +1419,630 @@ function runCheck(dir, io2) {
|
|
|
1409
1419
|
}
|
|
1410
1420
|
|
|
1411
1421
|
// src/commands/doctor.ts
|
|
1412
|
-
import { spawnSync as
|
|
1413
|
-
import
|
|
1414
|
-
import
|
|
1422
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
1423
|
+
import fs11 from "fs";
|
|
1424
|
+
import path11 from "path";
|
|
1415
1425
|
|
|
1416
|
-
// src/core/
|
|
1417
|
-
var
|
|
1418
|
-
|
|
1419
|
-
|
|
1426
|
+
// src/core/mutation-result.ts
|
|
1427
|
+
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
1428
|
+
var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
|
|
1429
|
+
var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
|
|
1430
|
+
function normalizeCode(text) {
|
|
1431
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1420
1432
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const { tests, gates } = value ?? {};
|
|
1424
|
-
if (!isStringList(tests) || typeof gates !== "object" || gates === null || Array.isArray(gates)) {
|
|
1425
|
-
return null;
|
|
1426
|
-
}
|
|
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;
|
|
1432
|
-
}
|
|
1433
|
-
parsed[gate] = { specs, files };
|
|
1434
|
-
}
|
|
1435
|
-
return { tests, gates: parsed };
|
|
1433
|
+
function normalizePath(file) {
|
|
1434
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1436
1435
|
}
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (mode !== "git" && mode !== "walk") {
|
|
1447
|
-
return null;
|
|
1448
|
-
}
|
|
1449
|
-
if (!isStringList(args) || !isStringRecord(environment) || !isStringRecord(files)) {
|
|
1436
|
+
function normalizeNewlines(text) {
|
|
1437
|
+
return text.replace(/\r\n/g, "\n");
|
|
1438
|
+
}
|
|
1439
|
+
function sliceByLocation(source, location) {
|
|
1440
|
+
const lines = source.split("\n");
|
|
1441
|
+
const { start, end } = location;
|
|
1442
|
+
const first = lines[start.line - 1];
|
|
1443
|
+
const last = lines[end.line - 1];
|
|
1444
|
+
if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
|
|
1450
1445
|
return null;
|
|
1451
1446
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const parsed = parseAffected(affected);
|
|
1455
|
-
if (parsed === null) {
|
|
1456
|
-
return null;
|
|
1457
|
-
}
|
|
1458
|
-
record.affected = parsed;
|
|
1447
|
+
if (start.line === end.line) {
|
|
1448
|
+
return first.slice(start.column - 1, end.column - 1);
|
|
1459
1449
|
}
|
|
1460
|
-
return
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1450
|
+
return [
|
|
1451
|
+
first.slice(start.column - 1),
|
|
1452
|
+
...lines.slice(start.line, end.line - 1),
|
|
1453
|
+
last.slice(0, end.column - 1)
|
|
1454
|
+
].join("\n");
|
|
1465
1455
|
}
|
|
1466
|
-
function
|
|
1467
|
-
return [
|
|
1456
|
+
function keyOf(mutator, original, replacement) {
|
|
1457
|
+
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
1468
1458
|
}
|
|
1469
|
-
function
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
};
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const changes = environment.map(
|
|
1490
|
-
(key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
|
|
1491
|
-
);
|
|
1492
|
-
return { refused: `the tools changed since the last run: ${listed(changes)}` };
|
|
1459
|
+
function locateMutants(source, mutants) {
|
|
1460
|
+
const located = [];
|
|
1461
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1462
|
+
const outside = [];
|
|
1463
|
+
const sorted2 = [...mutants].sort(
|
|
1464
|
+
(a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
|
|
1465
|
+
);
|
|
1466
|
+
for (const mutant of sorted2) {
|
|
1467
|
+
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
1468
|
+
if (original === null) {
|
|
1469
|
+
outside.push(mutant);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const replacement = mutant.replacement ?? "";
|
|
1473
|
+
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
1474
|
+
const group = byKey.get(key) ?? [];
|
|
1475
|
+
const entry = { mutant, original, replacement, occurrence: group.length + 1, sameKeyCount: 0 };
|
|
1476
|
+
group.push(entry);
|
|
1477
|
+
byKey.set(key, group);
|
|
1478
|
+
located.push(entry);
|
|
1493
1479
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1480
|
+
for (const group of byKey.values()) {
|
|
1481
|
+
for (const entry of group) {
|
|
1482
|
+
entry.sameKeyCount = group.length;
|
|
1483
|
+
}
|
|
1496
1484
|
}
|
|
1497
|
-
return {
|
|
1485
|
+
return { located, byKey, outside };
|
|
1498
1486
|
}
|
|
1499
|
-
function
|
|
1500
|
-
const
|
|
1501
|
-
if (
|
|
1502
|
-
return
|
|
1503
|
-
}
|
|
1504
|
-
const files = differingKeys(read2.record.snapshot.files, input.snapshot.files);
|
|
1505
|
-
if (files.length > 0) {
|
|
1506
|
-
return `changed since the last run: ${listed(files)}`;
|
|
1487
|
+
function resolveEntry(entry, byKey) {
|
|
1488
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
1489
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
1490
|
+
return { ambiguous: group.length };
|
|
1507
1491
|
}
|
|
1508
|
-
return
|
|
1509
|
-
}
|
|
1510
|
-
function incrementalOptions(config, args) {
|
|
1511
|
-
const options = typeof config === "object" && config !== null ? config : {};
|
|
1512
|
-
const valueOf = (name) => {
|
|
1513
|
-
let value;
|
|
1514
|
-
args.forEach((arg, position) => {
|
|
1515
|
-
if (arg === name) {
|
|
1516
|
-
value = args[position + 1];
|
|
1517
|
-
} else if (arg.startsWith(`${name}=`)) {
|
|
1518
|
-
value = arg.slice(name.length + 1);
|
|
1519
|
-
}
|
|
1520
|
-
});
|
|
1521
|
-
return value;
|
|
1522
|
-
};
|
|
1523
|
-
const fromConfig = (name, fallback) => typeof options[name] === "string" ? options[name] : fallback;
|
|
1524
|
-
return {
|
|
1525
|
-
incremental: options.incremental === true || args.includes("--incremental"),
|
|
1526
|
-
force: options.force === true || args.includes("--force"),
|
|
1527
|
-
incrementalFile: valueOf("--incrementalFile") ?? fromConfig("incrementalFile", "reports/stryker-incremental.json"),
|
|
1528
|
-
tempDirName: valueOf("--tempDirName") ?? fromConfig("tempDirName", ".stryker-tmp")
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
function comparableArgs(args) {
|
|
1532
|
-
return args.filter((arg) => arg !== "--force");
|
|
1492
|
+
return { target: group[(entry.occurrence ?? 1) - 1] };
|
|
1533
1493
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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";
|
|
1494
|
+
function expectedTimeoutMutants(gate, source, mutants) {
|
|
1495
|
+
const pointed = /* @__PURE__ */ new Set();
|
|
1496
|
+
if (source === void 0 || !Array.isArray(gate.expectedTimeouts)) {
|
|
1497
|
+
return pointed;
|
|
1561
1498
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1499
|
+
const { byKey, outside } = locateMutants(source, mutants);
|
|
1500
|
+
if (outside.length > 0) {
|
|
1501
|
+
return pointed;
|
|
1564
1502
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
}
|
|
1503
|
+
for (const entry of gate.expectedTimeouts) {
|
|
1504
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
1505
|
+
continue;
|
|
1577
1506
|
}
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1507
|
+
const found = resolveEntry(entry, byKey);
|
|
1508
|
+
if ("target" in found && found.target !== void 0) {
|
|
1509
|
+
pointed.add(found.target.mutant);
|
|
1581
1510
|
}
|
|
1582
|
-
result[gate.key] = { specs: [...specs].sort(), files: [...files].sort() };
|
|
1583
1511
|
}
|
|
1584
|
-
return
|
|
1512
|
+
return pointed;
|
|
1585
1513
|
}
|
|
1586
|
-
function
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1514
|
+
function describeMutant(file, entry) {
|
|
1515
|
+
const { mutant } = entry;
|
|
1516
|
+
return {
|
|
1517
|
+
file,
|
|
1518
|
+
line: mutant.location.start.line,
|
|
1519
|
+
column: mutant.location.start.column,
|
|
1520
|
+
status: mutant.status,
|
|
1521
|
+
mutator: mutant.mutatorName,
|
|
1522
|
+
original: normalizeCode(entry.original),
|
|
1523
|
+
replacement: normalizeCode(entry.replacement),
|
|
1524
|
+
occurrence: entry.occurrence,
|
|
1525
|
+
sameKeyCount: entry.sameKeyCount,
|
|
1526
|
+
allowance: {
|
|
1527
|
+
mutator: mutant.mutatorName,
|
|
1528
|
+
original: normalizeCode(entry.original),
|
|
1529
|
+
replacement: normalizeCode(entry.replacement),
|
|
1530
|
+
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
1531
|
+
reason: ""
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function describeAllowance(entry) {
|
|
1536
|
+
const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
|
|
1537
|
+
return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
|
|
1538
|
+
}
|
|
1539
|
+
function judgeMutationReport(input) {
|
|
1540
|
+
const violations = [];
|
|
1541
|
+
const survivors = [];
|
|
1542
|
+
const unexpectedTimeouts = [];
|
|
1543
|
+
const summary = {
|
|
1544
|
+
total: 0,
|
|
1545
|
+
detected: 0,
|
|
1546
|
+
timeout: 0,
|
|
1547
|
+
allowed: 0,
|
|
1548
|
+
notEvaluable: 0,
|
|
1549
|
+
unallowed: 0,
|
|
1550
|
+
expectedTimeout: 0
|
|
1551
|
+
};
|
|
1552
|
+
const files = /* @__PURE__ */ new Map();
|
|
1553
|
+
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
1554
|
+
files.set(normalizePath(file), result);
|
|
1603
1555
|
}
|
|
1604
|
-
const rerun = [];
|
|
1605
|
-
const reused = [];
|
|
1606
1556
|
for (const gate of input.gates) {
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1557
|
+
const gatePath = normalizePath(gate.path);
|
|
1558
|
+
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
1559
|
+
violations.push(...shapeViolations);
|
|
1560
|
+
violations.push(
|
|
1561
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
1562
|
+
);
|
|
1563
|
+
const result = files.get(gatePath);
|
|
1564
|
+
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
1565
|
+
violations.push({
|
|
1566
|
+
file: gate.path,
|
|
1567
|
+
message: "no mutants in the report (the gate was not mutation tested)"
|
|
1568
|
+
});
|
|
1610
1569
|
continue;
|
|
1611
1570
|
}
|
|
1612
|
-
const
|
|
1571
|
+
const diskSource = input.readSource(gate.path);
|
|
1572
|
+
const source = result.source ?? diskSource;
|
|
1573
|
+
if (source === void 0) {
|
|
1574
|
+
violations.push({
|
|
1575
|
+
file: gate.path,
|
|
1576
|
+
message: "source is neither in the report nor readable on disk"
|
|
1577
|
+
});
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
|
|
1581
|
+
violations.push({
|
|
1582
|
+
file: gate.path,
|
|
1583
|
+
message: "the report was made from a different version of this file. Run Stryker again"
|
|
1584
|
+
});
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const { located, byKey, outside } = locateMutants(source, result.mutants);
|
|
1588
|
+
for (const mutant of outside) {
|
|
1589
|
+
violations.push({
|
|
1590
|
+
file: gate.path,
|
|
1591
|
+
line: mutant.location.start.line,
|
|
1592
|
+
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (outside.length > 0) {
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
const claim = (field, entries, noun, accept) => {
|
|
1599
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1600
|
+
if (!Array.isArray(entries)) {
|
|
1601
|
+
return claimed;
|
|
1602
|
+
}
|
|
1603
|
+
entries.forEach((entry, index) => {
|
|
1604
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
1608
|
+
const found = resolveEntry(entry, byKey);
|
|
1609
|
+
if ("ambiguous" in found) {
|
|
1610
|
+
violations.push({
|
|
1611
|
+
file: MANIFEST_FILE,
|
|
1612
|
+
message: `${at}: matches ${found.ambiguous} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
1613
|
+
});
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const target = found.target;
|
|
1617
|
+
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
1618
|
+
if (!target || refusal !== null) {
|
|
1619
|
+
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (claimed.has(target)) {
|
|
1623
|
+
violations.push({
|
|
1624
|
+
file: MANIFEST_FILE,
|
|
1625
|
+
message: `${at}: another ${noun} already covers this mutant`
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
claimed.add(target);
|
|
1630
|
+
});
|
|
1631
|
+
return claimed;
|
|
1632
|
+
};
|
|
1633
|
+
const allowed = claim(
|
|
1634
|
+
"equivalentMutants",
|
|
1635
|
+
gate.equivalentMutants,
|
|
1636
|
+
"allowance",
|
|
1637
|
+
(status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
|
|
1638
|
+
);
|
|
1639
|
+
const expected = claim(
|
|
1640
|
+
"expectedTimeouts",
|
|
1641
|
+
gate.expectedTimeouts,
|
|
1642
|
+
"entry",
|
|
1643
|
+
(status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
|
|
1644
|
+
);
|
|
1645
|
+
for (const entry of located) {
|
|
1646
|
+
const { mutant } = entry;
|
|
1647
|
+
summary.total += 1;
|
|
1648
|
+
if (DETECTED.has(mutant.status)) {
|
|
1649
|
+
summary.detected += 1;
|
|
1650
|
+
if (mutant.status === "Timeout") {
|
|
1651
|
+
summary.timeout += 1;
|
|
1652
|
+
if (expected.has(entry)) {
|
|
1653
|
+
summary.expectedTimeout += 1;
|
|
1654
|
+
} else {
|
|
1655
|
+
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
} else if (NOT_EVALUABLE.has(mutant.status)) {
|
|
1659
|
+
summary.notEvaluable += 1;
|
|
1660
|
+
} else if (SURVIVED.has(mutant.status)) {
|
|
1661
|
+
if (allowed.has(entry)) {
|
|
1662
|
+
summary.allowed += 1;
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
summary.unallowed += 1;
|
|
1666
|
+
survivors.push(describeMutant(gate.path, entry));
|
|
1667
|
+
} else {
|
|
1668
|
+
violations.push({
|
|
1669
|
+
file: gate.path,
|
|
1670
|
+
line: mutant.location.start.line,
|
|
1671
|
+
message: mutant.status === "Ignored" ? `a ${mutant.mutatorName} mutant was ignored. Taking mutants out with Stryker comments or mutator exclusions is not allowed` : `a ${mutant.mutatorName} mutant was not evaluated (status: ${mutant.status})`
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (summary.total === 0) {
|
|
1677
|
+
violations.push({
|
|
1678
|
+
file: MANIFEST_FILE,
|
|
1679
|
+
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
1683
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
1684
|
+
violations.push({
|
|
1685
|
+
file: MANIFEST_FILE,
|
|
1686
|
+
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return { violations, survivors, unexpectedTimeouts, summary };
|
|
1690
|
+
}
|
|
1691
|
+
var UNKNOWN_TEST = "not-in-the-first-run:";
|
|
1692
|
+
function sameMutant(a, b) {
|
|
1693
|
+
return a.mutatorName === b.mutatorName && (a.replacement ?? "") === (b.replacement ?? "") && a.location.start.line === b.location.start.line && a.location.start.column === b.location.start.column && a.location.end.line === b.location.end.line && a.location.end.column === b.location.end.column;
|
|
1694
|
+
}
|
|
1695
|
+
function mergeRetry(first, retry, gates) {
|
|
1696
|
+
const outcome = { retried: 0, statuses: {}, missing: 0 };
|
|
1697
|
+
const firstId = translator(first, retry);
|
|
1698
|
+
const files = { ...first.files };
|
|
1699
|
+
const again = retry.files ?? {};
|
|
1700
|
+
for (const named of gates) {
|
|
1701
|
+
const gate = named.path.split("/").filter((part) => part !== ".").join("/");
|
|
1702
|
+
const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
|
|
1703
|
+
if (result?.mutants === void 0) {
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const expected = expectedTimeoutMutants(named, result.source, result.mutants);
|
|
1707
|
+
const rerun = Object.hasOwn(again, gate) ? again[gate]?.mutants : void 0;
|
|
1708
|
+
files[gate] = {
|
|
1709
|
+
...result,
|
|
1710
|
+
mutants: result.mutants.map((mutant) => {
|
|
1711
|
+
if (mutant.status !== "Timeout" || expected.has(mutant)) {
|
|
1712
|
+
return mutant;
|
|
1713
|
+
}
|
|
1714
|
+
outcome.retried += 1;
|
|
1715
|
+
const found = rerun?.find((other) => sameMutant(other, mutant));
|
|
1716
|
+
if (found === void 0) {
|
|
1717
|
+
outcome.missing += 1;
|
|
1718
|
+
return mutant;
|
|
1719
|
+
}
|
|
1720
|
+
outcome.statuses[found.status] = (outcome.statuses[found.status] ?? 0) + 1;
|
|
1721
|
+
return {
|
|
1722
|
+
...found,
|
|
1723
|
+
...found.coveredBy === void 0 ? {} : { coveredBy: found.coveredBy.map(firstId) },
|
|
1724
|
+
...found.killedBy === void 0 ? {} : { killedBy: found.killedBy.map(firstId) }
|
|
1725
|
+
};
|
|
1726
|
+
})
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
return { report: { ...first, files }, outcome };
|
|
1730
|
+
}
|
|
1731
|
+
function testKey(file, test) {
|
|
1732
|
+
const start = test.location?.start;
|
|
1733
|
+
return JSON.stringify([file, test.name ?? "", start?.line, start?.column]);
|
|
1734
|
+
}
|
|
1735
|
+
function* keyedTests(report) {
|
|
1736
|
+
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
1737
|
+
if (entry.tests !== void 0) {
|
|
1738
|
+
for (const test of entry.tests) {
|
|
1739
|
+
yield { id: test.id, key: testKey(file, test) };
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function translator(first, retry) {
|
|
1745
|
+
const firstIds = /* @__PURE__ */ new Map();
|
|
1746
|
+
for (const { id, key } of keyedTests(first)) {
|
|
1747
|
+
firstIds.set(key, firstIds.has(key) ? null : id);
|
|
1748
|
+
}
|
|
1749
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1750
|
+
for (const { id, key } of keyedTests(retry)) {
|
|
1751
|
+
ids.set(id, firstIds.get(key));
|
|
1752
|
+
}
|
|
1753
|
+
return (id) => {
|
|
1754
|
+
const found = ids.get(id);
|
|
1755
|
+
return typeof found === "string" ? found : `${UNKNOWN_TEST}${id}`;
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function formatRetry(outcome) {
|
|
1759
|
+
const statuses = Object.entries(outcome.statuses).sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `${count} ${status}`);
|
|
1760
|
+
if (outcome.missing > 0) {
|
|
1761
|
+
statuses.push(`${outcome.missing} not in the report (still Timeout)`);
|
|
1762
|
+
}
|
|
1763
|
+
return `test-gates mutation: ${outcome.retried} mutant(s) that timed out were run again one at a time (--concurrency 1): ${statuses.join(", ")}`;
|
|
1764
|
+
}
|
|
1765
|
+
function formatSurvivor(survivor) {
|
|
1766
|
+
const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
|
|
1767
|
+
return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
|
|
1768
|
+
}
|
|
1769
|
+
function formatSummary(summary) {
|
|
1770
|
+
return `mutants ${summary.total} / detected ${summary.detected} (timeout ${summary.timeout}) / allowed equivalent ${summary.allowed} / not evaluable ${summary.notEvaluable} / unallowed survivors ${summary.unallowed}`;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// src/core/reuse-guard.ts
|
|
1774
|
+
var RECORD_FORMAT = 3;
|
|
1775
|
+
function isStringRecord(value) {
|
|
1776
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1777
|
+
}
|
|
1778
|
+
var isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1779
|
+
function parseAffected(value) {
|
|
1780
|
+
const { tests, gates } = value ?? {};
|
|
1781
|
+
if (!isStringList(tests) || typeof gates !== "object" || gates === null || Array.isArray(gates)) {
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
const parsed = /* @__PURE__ */ Object.create(null);
|
|
1785
|
+
for (const [gate, entry] of Object.entries(gates)) {
|
|
1786
|
+
const { specs, files, blind } = entry ?? {};
|
|
1787
|
+
if (!isStringList(specs) || !isStringList(files)) {
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
if (blind !== void 0 && typeof blind !== "string") {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
parsed[gate] = { specs, files, ...blind === void 0 ? {} : { blind } };
|
|
1794
|
+
}
|
|
1795
|
+
return { tests, gates: parsed };
|
|
1796
|
+
}
|
|
1797
|
+
function parseRecord(value) {
|
|
1798
|
+
const { format, incremental, snapshot, affected } = value ?? {};
|
|
1799
|
+
if (format !== RECORD_FORMAT || typeof incremental !== "string") {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
if (typeof snapshot !== "object" || snapshot === null) {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
const { mode, environment, args, files } = snapshot;
|
|
1806
|
+
if (mode !== "git" && mode !== "walk") {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
if (!isStringList(args) || !isStringRecord(environment) || !isStringRecord(files)) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
const record = { format, incremental, snapshot: { mode, environment, args, files } };
|
|
1813
|
+
if (affected !== void 0) {
|
|
1814
|
+
const parsed = parseAffected(affected);
|
|
1815
|
+
if (parsed === null) {
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
record.affected = parsed;
|
|
1819
|
+
}
|
|
1820
|
+
return record;
|
|
1821
|
+
}
|
|
1822
|
+
function listed(names) {
|
|
1823
|
+
const shown = names.slice(0, 3).join(", ");
|
|
1824
|
+
return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
|
|
1825
|
+
}
|
|
1826
|
+
function differingKeys(before, after) {
|
|
1827
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).sort();
|
|
1828
|
+
}
|
|
1829
|
+
function readRecord(input) {
|
|
1830
|
+
if (input.record === void 0) {
|
|
1831
|
+
return { refused: "there is no record of the run that wrote the incremental file" };
|
|
1832
|
+
}
|
|
1833
|
+
const record = parseRecord(input.record);
|
|
1834
|
+
if (record === null) {
|
|
1835
|
+
return { refused: "the record of the last run is not one this version can read" };
|
|
1836
|
+
}
|
|
1837
|
+
if (record.incremental !== input.incremental) {
|
|
1838
|
+
return { refused: "the incremental file is not the one the last recorded run wrote" };
|
|
1839
|
+
}
|
|
1840
|
+
const before = record.snapshot;
|
|
1841
|
+
const now = input.snapshot;
|
|
1842
|
+
if (before.mode !== now.mode) {
|
|
1843
|
+
return {
|
|
1844
|
+
refused: `the files are listed in another way than in the last run (${before.mode}, now ${now.mode})`
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const environment = differingKeys(before.environment, now.environment);
|
|
1848
|
+
if (environment.length > 0) {
|
|
1849
|
+
const changes = environment.map(
|
|
1850
|
+
(key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
|
|
1851
|
+
);
|
|
1852
|
+
return { refused: `the tools changed since the last run: ${listed(changes)}` };
|
|
1853
|
+
}
|
|
1854
|
+
if (JSON.stringify(before.args) !== JSON.stringify(now.args)) {
|
|
1855
|
+
return { refused: "the arguments for Stryker are not those of the last run" };
|
|
1856
|
+
}
|
|
1857
|
+
return { record };
|
|
1858
|
+
}
|
|
1859
|
+
function refuseReuse(input) {
|
|
1860
|
+
const read2 = readRecord(input);
|
|
1861
|
+
if ("refused" in read2) {
|
|
1862
|
+
return read2.refused;
|
|
1863
|
+
}
|
|
1864
|
+
const files = differingKeys(read2.record.snapshot.files, input.snapshot.files);
|
|
1865
|
+
if (files.length > 0) {
|
|
1866
|
+
return `changed since the last run: ${listed(files)}`;
|
|
1867
|
+
}
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
function incrementalOptions(config, args) {
|
|
1871
|
+
const options = typeof config === "object" && config !== null ? config : {};
|
|
1872
|
+
const valueOf = (name) => {
|
|
1873
|
+
let value;
|
|
1874
|
+
args.forEach((arg, position) => {
|
|
1875
|
+
if (arg === name) {
|
|
1876
|
+
value = args[position + 1];
|
|
1877
|
+
} else if (arg.startsWith(`${name}=`)) {
|
|
1878
|
+
value = arg.slice(name.length + 1);
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
return value;
|
|
1882
|
+
};
|
|
1883
|
+
const fromConfig = (name, fallback) => typeof options[name] === "string" ? options[name] : fallback;
|
|
1884
|
+
return {
|
|
1885
|
+
incremental: options.incremental === true || args.includes("--incremental"),
|
|
1886
|
+
force: options.force === true || args.includes("--force"),
|
|
1887
|
+
incrementalFile: valueOf("--incrementalFile") ?? fromConfig("incrementalFile", "reports/stryker-incremental.json"),
|
|
1888
|
+
tempDirName: valueOf("--tempDirName") ?? fromConfig("tempDirName", ".stryker-tmp")
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function comparableArgs(args) {
|
|
1892
|
+
return args.filter((arg) => arg !== "--force");
|
|
1893
|
+
}
|
|
1894
|
+
function objectOf(value) {
|
|
1895
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1896
|
+
}
|
|
1897
|
+
function reportFiles(config) {
|
|
1898
|
+
const options = objectOf(config);
|
|
1899
|
+
const fileName = (reporter, fallback) => {
|
|
1900
|
+
const named = objectOf(options[reporter]).fileName;
|
|
1901
|
+
return typeof named === "string" ? named : fallback;
|
|
1902
|
+
};
|
|
1903
|
+
return [
|
|
1904
|
+
fileName("htmlReporter", "reports/mutation/mutation.html"),
|
|
1905
|
+
fileName("jsonReporter", "reports/mutation/mutation.json")
|
|
1906
|
+
];
|
|
1907
|
+
}
|
|
1908
|
+
var WILDCARDS = /* @__PURE__ */ new Map([
|
|
1909
|
+
["**/", "(?:.*/)?"],
|
|
1910
|
+
["*", "[^/]*"],
|
|
1911
|
+
["?", "[^/]"]
|
|
1912
|
+
]);
|
|
1913
|
+
function globSource(glob) {
|
|
1914
|
+
return glob.split(/(\*\*\/|\*|\?)/).map((part) => WILDCARDS.get(part) ?? part.replace(/\W/g, "\\$&")).join("");
|
|
1915
|
+
}
|
|
1916
|
+
function strykerIgnores(config) {
|
|
1917
|
+
const patterns = [objectOf(config).ignorePatterns].flat().filter((pattern) => typeof pattern === "string");
|
|
1918
|
+
if (patterns.some((pattern) => pattern.startsWith("!"))) {
|
|
1919
|
+
return () => false;
|
|
1920
|
+
}
|
|
1921
|
+
const rules = patterns.map((pattern) => {
|
|
1922
|
+
const directoryOnly = pattern.endsWith("/");
|
|
1923
|
+
const body = pattern.replace(/\/+$/, "");
|
|
1924
|
+
const anchored = body.includes("/");
|
|
1925
|
+
const expression = new RegExp(`^${globSource(body.replace(/^\//, ""))}$`, "i");
|
|
1926
|
+
return { directoryOnly, anchored, expression };
|
|
1927
|
+
});
|
|
1928
|
+
return (relative) => {
|
|
1929
|
+
const parts = relative.split("/");
|
|
1930
|
+
return rules.some(
|
|
1931
|
+
(rule) => parts.some((part, index) => {
|
|
1932
|
+
if (rule.directoryOnly && index === parts.length - 1) {
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
const target = rule.anchored ? parts.slice(0, index + 1).join("/") : part;
|
|
1936
|
+
return rule.expression.test(target);
|
|
1937
|
+
})
|
|
1938
|
+
);
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/core/affected.ts
|
|
1943
|
+
var CONFIG_NAMES = [
|
|
1944
|
+
/^package\.json$/,
|
|
1945
|
+
/^(?:package-lock|npm-shrinkwrap)\.json$/,
|
|
1946
|
+
/^test-gates\.json$/,
|
|
1947
|
+
/^[jt]sconfig(?:\..*)?\.json$/,
|
|
1948
|
+
/\.config\.[^.]+$/,
|
|
1949
|
+
/\.config\.[^.]+\.[^.]+$/,
|
|
1950
|
+
/^(?:jest|vitest|vite|stryker|babel)[.-]/,
|
|
1951
|
+
/^\./,
|
|
1952
|
+
// Declarations need no import to take effect.
|
|
1953
|
+
/\.d\.[cm]?ts$/
|
|
1954
|
+
];
|
|
1955
|
+
var ROOT_MOCKS = "__mocks__";
|
|
1956
|
+
var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc|html?)$/i;
|
|
1957
|
+
var ENTRY_PAGE = /^index\.html?$/i;
|
|
1958
|
+
var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
|
|
1959
|
+
function kindOf(key, subproject) {
|
|
1960
|
+
const segments = localSegments(key, subproject);
|
|
1961
|
+
const name = segments[segments.length - 1];
|
|
1962
|
+
if (segments[0] === ROOT_MOCKS || segments.includes("node_modules") || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
|
|
1963
|
+
return "config";
|
|
1964
|
+
}
|
|
1965
|
+
if (isNestedMock(key, subproject)) {
|
|
1966
|
+
return "other";
|
|
1967
|
+
}
|
|
1968
|
+
if (DOCUMENT.test(name) && !ENTRY_PAGE.test(name)) {
|
|
1969
|
+
return "document";
|
|
1970
|
+
}
|
|
1971
|
+
const inside = subproject === "" || key.startsWith(`${subproject}/`);
|
|
1972
|
+
if (!inside || segments.length === 1 || !PASSIVE.test(name)) {
|
|
1973
|
+
return "other";
|
|
1974
|
+
}
|
|
1975
|
+
return "passive";
|
|
1976
|
+
}
|
|
1977
|
+
function localSegments(key, subproject) {
|
|
1978
|
+
return (key.startsWith(`${subproject}/`) ? key.slice(subproject.length + 1) : key).split("/");
|
|
1979
|
+
}
|
|
1980
|
+
function isNestedMock(key, subproject) {
|
|
1981
|
+
return localSegments(key, subproject).slice(1, -1).includes(ROOT_MOCKS);
|
|
1982
|
+
}
|
|
1983
|
+
function gateDependencies(input) {
|
|
1984
|
+
const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
|
|
1985
|
+
const blind = input.blind ?? {};
|
|
1986
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1987
|
+
for (const gate of input.gates) {
|
|
1988
|
+
const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
|
|
1989
|
+
const specs = /* @__PURE__ */ new Set([...gate.specs, ...covering]);
|
|
1990
|
+
for (const test of input.tests) {
|
|
1991
|
+
if (closureOf(test).includes(gate.key)) {
|
|
1992
|
+
specs.add(test);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
const files = new Set(closureOf(gate.key));
|
|
1996
|
+
for (const spec of specs) {
|
|
1997
|
+
closureOf(spec).forEach((file) => files.add(file));
|
|
1998
|
+
}
|
|
1999
|
+
const sorted2 = [...files].sort();
|
|
2000
|
+
const reason = sorted2.find((file) => Object.hasOwn(blind, file));
|
|
2001
|
+
result[gate.key] = {
|
|
2002
|
+
specs: [...specs].sort(),
|
|
2003
|
+
files: sorted2,
|
|
2004
|
+
...reason === void 0 ? {} : { blind: blind[reason] }
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
return result;
|
|
2008
|
+
}
|
|
2009
|
+
function planAffected(input) {
|
|
2010
|
+
const recorded = input.before.affected;
|
|
2011
|
+
if (recorded === void 0) {
|
|
2012
|
+
return {
|
|
2013
|
+
full: "the last run did not write down what each gate depends on (it ran in CI, with an older version, or could not tell)"
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
const changed = differingKeys(input.before.files, input.now);
|
|
2017
|
+
const reached = new Set(input.gates.flatMap((gate) => gate.files));
|
|
2018
|
+
Object.values(recorded.gates).forEach((gate) => gate.files.forEach((file) => reached.add(file)));
|
|
2019
|
+
const config = new Set(input.config);
|
|
2020
|
+
const everywhere = changed.filter((key) => {
|
|
2021
|
+
const kind = kindOf(key, input.subproject);
|
|
2022
|
+
return config.has(key) || kind === "config" || kind === "other" && !reached.has(key) || // A mock that is gone may have been one of a package, which is used by name for every spec.
|
|
2023
|
+
isNestedMock(key, input.subproject) && !Object.hasOwn(input.now, key);
|
|
2024
|
+
});
|
|
2025
|
+
if (everywhere.length > 0) {
|
|
2026
|
+
return { full: `changed since the last run, and may affect every gate: ${listed(everywhere)}` };
|
|
2027
|
+
}
|
|
2028
|
+
const rerun = [];
|
|
2029
|
+
const reused = [];
|
|
2030
|
+
for (const gate of input.gates) {
|
|
2031
|
+
const last = Object.hasOwn(recorded.gates, gate.path) ? recorded.gates[gate.path] : void 0;
|
|
2032
|
+
if (last === void 0) {
|
|
2033
|
+
rerun.push({ gate: gate.path, changed: [] });
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
const blind = gate.blind ?? last.blind;
|
|
2037
|
+
if (blind !== void 0) {
|
|
2038
|
+
if (changed.length > 0 || input.seeded === true) {
|
|
2039
|
+
rerun.push({ gate: gate.path, changed, blind });
|
|
2040
|
+
} else {
|
|
2041
|
+
reused.push(gate.path);
|
|
2042
|
+
}
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
|
|
1613
2046
|
const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
|
|
1614
2047
|
if (differing.length > 0) {
|
|
1615
2048
|
rerun.push({ gate: gate.path, changed: differing });
|
|
@@ -1624,23 +2057,33 @@ function withoutDots(file) {
|
|
|
1624
2057
|
}
|
|
1625
2058
|
function timeoutRanges(last, gates) {
|
|
1626
2059
|
const ranges = [];
|
|
2060
|
+
const rerun = /* @__PURE__ */ new Set();
|
|
2061
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1627
2062
|
const files = last.files ?? {};
|
|
1628
2063
|
for (const named of gates) {
|
|
1629
|
-
const gate = withoutDots(named);
|
|
1630
|
-
const
|
|
1631
|
-
if (mutants === void 0) {
|
|
2064
|
+
const gate = withoutDots(named.path);
|
|
2065
|
+
const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
|
|
2066
|
+
if (result?.mutants === void 0) {
|
|
1632
2067
|
continue;
|
|
1633
2068
|
}
|
|
2069
|
+
const { mutants } = result;
|
|
2070
|
+
const expected = expectedTimeoutMutants(named, result.source, mutants);
|
|
1634
2071
|
for (const mutant of mutants) {
|
|
1635
|
-
if (mutant.status
|
|
1636
|
-
|
|
1637
|
-
|
|
2072
|
+
if (mutant.status !== "Timeout") {
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
if (expected.has(mutant)) {
|
|
2076
|
+
taken.add(mutant);
|
|
2077
|
+
continue;
|
|
1638
2078
|
}
|
|
2079
|
+
rerun.add(mutant);
|
|
2080
|
+
const { start, end } = mutant.location;
|
|
2081
|
+
ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
|
|
1639
2082
|
}
|
|
1640
2083
|
}
|
|
1641
|
-
return [...new Set(ranges)];
|
|
2084
|
+
return { ranges: [...new Set(ranges)], rerun: rerun.size, expected: taken.size };
|
|
1642
2085
|
}
|
|
1643
|
-
function describePlan(plan, shown = 5, timeouts = 0) {
|
|
2086
|
+
function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
|
|
1644
2087
|
if ("full" in plan) {
|
|
1645
2088
|
return [
|
|
1646
2089
|
`test-gates mutation --affected: running every mutant again (--force), the results of the last run are not reused: ${plan.full}`
|
|
@@ -1650,19 +2093,35 @@ function describePlan(plan, shown = 5, timeouts = 0) {
|
|
|
1650
2093
|
const lines = [
|
|
1651
2094
|
`test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
|
|
1652
2095
|
];
|
|
1653
|
-
|
|
2096
|
+
const told = plan.rerun.filter((entry) => entry.blind === void 0);
|
|
2097
|
+
for (const { gate, changed } of told.slice(0, shown)) {
|
|
1654
2098
|
lines.push(
|
|
1655
2099
|
changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
|
|
1656
2100
|
);
|
|
1657
2101
|
}
|
|
1658
|
-
if (
|
|
1659
|
-
lines.push(` and ${
|
|
2102
|
+
if (told.length > shown) {
|
|
2103
|
+
lines.push(` and ${told.length - shown} more`);
|
|
2104
|
+
}
|
|
2105
|
+
const reasons = [
|
|
2106
|
+
...new Set(plan.rerun.flatMap((entry) => entry.blind === void 0 ? [] : [entry.blind]))
|
|
2107
|
+
];
|
|
2108
|
+
if (reasons.length > 0) {
|
|
2109
|
+
const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
|
|
2110
|
+
lines.push(
|
|
2111
|
+
` ${plan.rerun.length - told.length} gate(s) run again for any change, as what they depend on cannot be told: ${reasons[0]}${more}`
|
|
2112
|
+
);
|
|
1660
2113
|
}
|
|
1661
2114
|
if (timeouts > 0) {
|
|
1662
2115
|
lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
|
|
1663
2116
|
}
|
|
2117
|
+
if (expected > 0) {
|
|
2118
|
+
lines.push(` ${describeExpected(expected)}`);
|
|
2119
|
+
}
|
|
1664
2120
|
return lines;
|
|
1665
2121
|
}
|
|
2122
|
+
function describeExpected(expected) {
|
|
2123
|
+
return `${expected} mutant(s) that timed out in the last run are taken over: expectedTimeouts lists them`;
|
|
2124
|
+
}
|
|
1666
2125
|
|
|
1667
2126
|
// src/core/doctor.ts
|
|
1668
2127
|
var NOT_THE_HELPER = "the runner config is not made with";
|
|
@@ -1697,12 +2156,22 @@ function diagnose(facts) {
|
|
|
1697
2156
|
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
2157
|
});
|
|
1699
2158
|
}
|
|
2159
|
+
const everyBlind = facts.blindGates.length >= facts.gates;
|
|
1700
2160
|
if (facts.blind !== null) {
|
|
1701
2161
|
findings.push({
|
|
1702
2162
|
level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
1703
2163
|
text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
|
|
1704
2164
|
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
2165
|
});
|
|
2166
|
+
} else if (facts.blindGates.length > 0) {
|
|
2167
|
+
const gates = facts.blindGates.map((one) => one.gate);
|
|
2168
|
+
const reasons = [...new Set(facts.blindGates.map((one) => one.reason))];
|
|
2169
|
+
const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
|
|
2170
|
+
findings.push({
|
|
2171
|
+
level: everyBlind && facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
|
|
2172
|
+
text: `with --affected, a change of any file runs ${gates.length} of ${facts.gates} gate(s) again (${listed(gates)}): what they depend on cannot be told (${reasons[0]}${more})`,
|
|
2173
|
+
fix: "keep what reads files by a path no import names out of what those gates and their specs import"
|
|
2174
|
+
});
|
|
1706
2175
|
}
|
|
1707
2176
|
if (facts.scope !== null && "every" in facts.scope) {
|
|
1708
2177
|
findings.push({
|
|
@@ -1711,6 +2180,21 @@ function diagnose(facts) {
|
|
|
1711
2180
|
fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
|
|
1712
2181
|
});
|
|
1713
2182
|
}
|
|
2183
|
+
if (facts.sandboxes !== null) {
|
|
2184
|
+
const { tempDir, count, kept, running } = facts.sandboxes;
|
|
2185
|
+
findings.push(
|
|
2186
|
+
running === null ? {
|
|
2187
|
+
level: "note",
|
|
2188
|
+
text: `${count} sandbox(es) an interrupted Stryker run left in ${tempDir}: a tool that walks the subproject (a test runner, a script) finds a copy of every file in them`,
|
|
2189
|
+
fix: kept === null ? "the next test-gates mutation removes them" : `remove them by hand; the next test-gates mutation keeps them, because ${kept}`
|
|
2190
|
+
} : {
|
|
2191
|
+
// The sandboxes may be those of that run: removing them would break it.
|
|
2192
|
+
level: "note",
|
|
2193
|
+
text: `another test-gates mutation is running in this subproject (pid ${running}): the ${count} sandbox(es) in ${tempDir} are in use`,
|
|
2194
|
+
fix: "leave them; the first test-gates mutation after that run ends removes what is left"
|
|
2195
|
+
}
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
1714
2198
|
if (facts.hooks.length === 0) {
|
|
1715
2199
|
findings.push({
|
|
1716
2200
|
level: "note",
|
|
@@ -1718,7 +2202,8 @@ function diagnose(facts) {
|
|
|
1718
2202
|
fix: null
|
|
1719
2203
|
});
|
|
1720
2204
|
}
|
|
1721
|
-
|
|
2205
|
+
const toldApart = facts.blind === null && !everyBlind;
|
|
2206
|
+
for (const hook of facts.hooks.filter((one) => !one.affected && toldApart)) {
|
|
1722
2207
|
findings.push({
|
|
1723
2208
|
level: "problem",
|
|
1724
2209
|
text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
|
|
@@ -1744,86 +2229,399 @@ function formatFindings(findings) {
|
|
|
1744
2229
|
var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
|
|
1745
2230
|
var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
|
|
1746
2231
|
var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
|
|
2232
|
+
var NONE = [];
|
|
2233
|
+
var COMMAND = String.raw`(?<=(?:^|[\n;&|(){]|\b(?:then|else|do))\s*)`;
|
|
2234
|
+
var TOKEN = new RegExp(
|
|
2235
|
+
String.raw`\n|;;&?|;&|${COMMAND}(?:case\s+(\S+)\s+in|for\s+(\w+)(?:\s+in\s+([^;\n]*))?|(esac|do|done))(?![\w-])`,
|
|
2236
|
+
"g"
|
|
2237
|
+
);
|
|
2238
|
+
var PATTERN = /\s*(?:esac(?![\w-])|\(?([^()\n;]*)\))/y;
|
|
2239
|
+
var VARIABLE = /^("?)\$(?:(\w+)|\{(\w+)\})\1$/;
|
|
2240
|
+
var REFERENCE = /\$(?:(\w+)|\{(\w+)\})/g;
|
|
2241
|
+
function namesOf(words) {
|
|
2242
|
+
const names = words.map((word) => word.trim().replace(/^(["'])(.*)\1$/, "$2"));
|
|
2243
|
+
return names.some((name) => name === "" || /[$*?[{}~\\`"']/.test(name)) ? null : names;
|
|
2244
|
+
}
|
|
2245
|
+
function partsOf(text) {
|
|
2246
|
+
const parts = [];
|
|
2247
|
+
let scopes = NONE;
|
|
2248
|
+
let start = 0;
|
|
2249
|
+
let loop = null;
|
|
2250
|
+
const enter = (scope) => {
|
|
2251
|
+
scopes = [...scopes, scope];
|
|
2252
|
+
};
|
|
2253
|
+
const leave = (kind) => {
|
|
2254
|
+
const left = scopes.at(-1)?.kind === kind;
|
|
2255
|
+
scopes = scopes.slice(0, -1);
|
|
2256
|
+
return left;
|
|
2257
|
+
};
|
|
2258
|
+
TOKEN.lastIndex = 0;
|
|
2259
|
+
for (; ; ) {
|
|
2260
|
+
const token = TOKEN.exec(text);
|
|
2261
|
+
parts.push({ text: text.slice(start, token?.index), scopes });
|
|
2262
|
+
if (token === null) {
|
|
2263
|
+
return scopes.length === 0 ? parts : null;
|
|
2264
|
+
}
|
|
2265
|
+
const [word, subject, variable, list, keyword] = token;
|
|
2266
|
+
let awaiting = false;
|
|
2267
|
+
if (subject !== void 0) {
|
|
2268
|
+
const named = VARIABLE.exec(subject);
|
|
2269
|
+
enter({
|
|
2270
|
+
kind: "case",
|
|
2271
|
+
id: /* @__PURE__ */ Symbol(),
|
|
2272
|
+
variable: named?.[2] ?? named?.[3] ?? null,
|
|
2273
|
+
names: null
|
|
2274
|
+
});
|
|
2275
|
+
awaiting = true;
|
|
2276
|
+
} else if (variable !== void 0) {
|
|
2277
|
+
loop = {
|
|
2278
|
+
kind: "loop",
|
|
2279
|
+
id: /* @__PURE__ */ Symbol(),
|
|
2280
|
+
variable,
|
|
2281
|
+
names: list === void 0 ? null : namesOf(list.trim().split(/\s+/))
|
|
2282
|
+
};
|
|
2283
|
+
} else if (keyword === "do") {
|
|
2284
|
+
enter(loop ?? { kind: "loop", id: /* @__PURE__ */ Symbol(), variable: null, names: null });
|
|
2285
|
+
loop = null;
|
|
2286
|
+
} else if (keyword === "done" || keyword === "esac") {
|
|
2287
|
+
if (!leave(keyword === "done" ? "loop" : "case")) {
|
|
2288
|
+
return null;
|
|
2289
|
+
}
|
|
2290
|
+
} else if (word === ";&") {
|
|
2291
|
+
return null;
|
|
2292
|
+
} else if (word !== "\n") {
|
|
2293
|
+
if (scopes.at(-1)?.kind !== "case") {
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
awaiting = true;
|
|
2297
|
+
}
|
|
2298
|
+
start = TOKEN.lastIndex;
|
|
2299
|
+
if (awaiting) {
|
|
2300
|
+
PATTERN.lastIndex = start;
|
|
2301
|
+
const pattern = PATTERN.exec(text);
|
|
2302
|
+
if (pattern === null) {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2305
|
+
start = PATTERN.lastIndex;
|
|
2306
|
+
TOKEN.lastIndex = start;
|
|
2307
|
+
const top = scopes.at(-1);
|
|
2308
|
+
scopes = scopes.slice(0, -1);
|
|
2309
|
+
if (pattern[1] !== void 0) {
|
|
2310
|
+
enter({ ...top, names: namesOf(pattern[1].split("|")) });
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
function sibling(of, scopes) {
|
|
2316
|
+
const apart = of.findIndex((scope, at) => scope !== scopes[at]);
|
|
2317
|
+
return apart !== -1 && of[apart].id === scopes[apart]?.id;
|
|
2318
|
+
}
|
|
2319
|
+
function expansions(target, scopes) {
|
|
2320
|
+
return [...target.matchAll(REFERENCE)].reduce(
|
|
2321
|
+
(words, [reference, plain2, braced]) => {
|
|
2322
|
+
const names = [...scopes].reverse().find((scope) => scope.variable === (plain2 ?? braced))?.names ?? [reference];
|
|
2323
|
+
return words.flatMap((word) => names.map((name) => word.replace(reference, name)));
|
|
2324
|
+
},
|
|
2325
|
+
[target]
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
1747
2328
|
function hookUses(input) {
|
|
1748
2329
|
const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
|
|
2330
|
+
const parts = partsOf(lines.join("\n")) ?? lines.map((text) => ({ text, scopes: NONE }));
|
|
1749
2331
|
const uses = [];
|
|
1750
|
-
|
|
1751
|
-
|
|
2332
|
+
const cds = parts.map((part) => ({ cd: CD.exec(part.text)?.[2], scopes: part.scopes }));
|
|
2333
|
+
parts.forEach((part, index) => {
|
|
2334
|
+
if (!RUNS_MUTATION.test(part.text)) {
|
|
1752
2335
|
return;
|
|
1753
2336
|
}
|
|
1754
|
-
const named = TARGET.exec(
|
|
1755
|
-
const
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
2337
|
+
const named = TARGET.exec(part.text) ?? CD.exec(part.text);
|
|
2338
|
+
const before = cds.slice(0, index).reverse().find((one) => one.cd !== void 0 && !sibling(one.scopes, part.scopes));
|
|
2339
|
+
const target = named?.[2] ?? before?.cd;
|
|
2340
|
+
const scopes = named !== null || before?.scopes === part.scopes ? part.scopes : NONE;
|
|
2341
|
+
const ours = target === void 0 || expansions(target, scopes).some((word) => {
|
|
2342
|
+
const normalized = word.split("/").filter((one) => one !== "." && one !== "").join("/");
|
|
2343
|
+
return normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
|
|
2344
|
+
});
|
|
1758
2345
|
if (ours) {
|
|
1759
2346
|
uses.push({
|
|
1760
2347
|
file: input.file,
|
|
1761
|
-
affected: /--affected\b/.test(
|
|
2348
|
+
affected: /--affected\b/.test(part.text) || /test:gates:mutation\b/.test(part.text) && input.script.includes("--affected")
|
|
1762
2349
|
});
|
|
1763
2350
|
}
|
|
1764
2351
|
});
|
|
1765
2352
|
return uses;
|
|
1766
2353
|
}
|
|
1767
2354
|
|
|
1768
|
-
// src/
|
|
2355
|
+
// src/sandboxes.ts
|
|
1769
2356
|
import { spawnSync } from "child_process";
|
|
2357
|
+
import fs4 from "fs";
|
|
2358
|
+
import path4 from "path";
|
|
2359
|
+
|
|
2360
|
+
// src/core/sandboxes.ts
|
|
2361
|
+
var SANDBOX = /^sandbox-[A-Za-z0-9]{6}$/;
|
|
2362
|
+
var MARKER = /^test-gates-run-([1-9][0-9]*)$/;
|
|
2363
|
+
function markerName(pid) {
|
|
2364
|
+
return `test-gates-run-${pid}`;
|
|
2365
|
+
}
|
|
2366
|
+
function markerText(pid, started) {
|
|
2367
|
+
return `${pid}
|
|
2368
|
+
${started ?? ""}
|
|
2369
|
+
`;
|
|
2370
|
+
}
|
|
2371
|
+
function markerAlive(input) {
|
|
2372
|
+
const recorded = input.text?.split("\n")[1]?.trim() || null;
|
|
2373
|
+
return input.there && (recorded === null || input.started === null || sameStart(recorded, input.started));
|
|
2374
|
+
}
|
|
2375
|
+
var START_SLACK_SECONDS = 5;
|
|
2376
|
+
function sameStart(a, b) {
|
|
2377
|
+
const difference = Math.abs(Date.parse(a) - Date.parse(b));
|
|
2378
|
+
return Number.isNaN(difference) ? a === b : difference <= START_SLACK_SECONDS * 1e3;
|
|
2379
|
+
}
|
|
2380
|
+
function insideSubproject(tempDir) {
|
|
2381
|
+
const parts = tempDir.split("/");
|
|
2382
|
+
return !/^[A-Za-z]:/.test(tempDir) && parts.every((part) => part !== "" && part !== "." && part !== "..");
|
|
2383
|
+
}
|
|
2384
|
+
function planSandboxes(facts) {
|
|
2385
|
+
const sandboxes = facts.entries.filter((entry) => entry.directory && SANDBOX.test(entry.name)).map((entry) => entry.name).sort();
|
|
2386
|
+
if (facts.tempDir === null) {
|
|
2387
|
+
return {
|
|
2388
|
+
kept: sandboxes,
|
|
2389
|
+
why: "the Stryker config could not be loaded to see where they are",
|
|
2390
|
+
running: null
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
if (!insideSubproject(facts.tempDir)) {
|
|
2394
|
+
return {
|
|
2395
|
+
kept: sandboxes,
|
|
2396
|
+
why: `tempDirName (${facts.tempDir}) is not a directory inside the subproject`,
|
|
2397
|
+
running: null
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
if (facts.keeps) {
|
|
2401
|
+
return { kept: sandboxes, why: "the Stryker config sets cleanTempDir: false", running: null };
|
|
2402
|
+
}
|
|
2403
|
+
const markers = [];
|
|
2404
|
+
for (const entry of facts.entries) {
|
|
2405
|
+
const pid = MARKER.exec(entry.name)?.[1];
|
|
2406
|
+
if (pid === void 0 || entry.directory) {
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (facts.alive(Number(pid), entry.name)) {
|
|
2410
|
+
return {
|
|
2411
|
+
kept: sandboxes,
|
|
2412
|
+
why: `another test-gates mutation is running in this subproject (pid ${pid})`,
|
|
2413
|
+
running: Number(pid)
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
markers.push(entry.name);
|
|
2417
|
+
}
|
|
2418
|
+
return { remove: sandboxes, markers };
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// src/sandboxes.ts
|
|
2422
|
+
function there(pid) {
|
|
2423
|
+
try {
|
|
2424
|
+
process.kill(pid, 0);
|
|
2425
|
+
return true;
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
return error.code === "EPERM";
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function startedAt(pid) {
|
|
2431
|
+
const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
2432
|
+
encoding: "utf8",
|
|
2433
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
2434
|
+
timeout: 5e3
|
|
2435
|
+
});
|
|
2436
|
+
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
2437
|
+
return started === "" ? null : started;
|
|
2438
|
+
}
|
|
2439
|
+
function alive(dir) {
|
|
2440
|
+
return (pid, marker) => {
|
|
2441
|
+
if (!there(pid)) {
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
let text = null;
|
|
2445
|
+
try {
|
|
2446
|
+
text = fs4.readFileSync(path4.join(dir, marker), "utf8");
|
|
2447
|
+
} catch {
|
|
2448
|
+
}
|
|
2449
|
+
return markerAlive({ there: true, text, started: startedAt(pid) });
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
function relativeTempDir(subproject, dir) {
|
|
2453
|
+
let real = dir;
|
|
2454
|
+
try {
|
|
2455
|
+
real = fs4.realpathSync(dir);
|
|
2456
|
+
} catch {
|
|
2457
|
+
}
|
|
2458
|
+
return path4.relative(fs4.realpathSync(subproject), real).split(path4.sep).join("/");
|
|
2459
|
+
}
|
|
2460
|
+
function sandboxPlan(subproject, setting) {
|
|
2461
|
+
const tempDir = setting.dir === null ? null : relativeTempDir(subproject, setting.dir);
|
|
2462
|
+
let entries = [];
|
|
2463
|
+
if (setting.dir !== null) {
|
|
2464
|
+
try {
|
|
2465
|
+
entries = fs4.readdirSync(setting.dir, { withFileTypes: true });
|
|
2466
|
+
} catch {
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const plan = planSandboxes({
|
|
2470
|
+
tempDir,
|
|
2471
|
+
entries: entries.map((entry) => ({ name: entry.name, directory: entry.isDirectory() })),
|
|
2472
|
+
keeps: setting.keeps,
|
|
2473
|
+
alive: alive(setting.dir ?? subproject)
|
|
2474
|
+
});
|
|
2475
|
+
return { plan, shown: tempDir ?? ".stryker-tmp" };
|
|
2476
|
+
}
|
|
2477
|
+
function removeStaleSandboxes(subproject, setting) {
|
|
2478
|
+
const { plan, shown } = sandboxPlan(subproject, setting);
|
|
2479
|
+
if ("kept" in plan) {
|
|
2480
|
+
if (plan.kept.length === 0) {
|
|
2481
|
+
return [];
|
|
2482
|
+
}
|
|
2483
|
+
return plan.running === null ? [
|
|
2484
|
+
`test-gates mutation: ${plan.kept.length} sandbox(es) left in ${shown} by an earlier Stryker run are kept: ${plan.why}`
|
|
2485
|
+
] : [
|
|
2486
|
+
`test-gates mutation: ${plan.why}, so the ${plan.kept.length} sandbox(es) in ${shown} are kept: they are in use`
|
|
2487
|
+
];
|
|
2488
|
+
}
|
|
2489
|
+
const dir = setting.dir;
|
|
2490
|
+
for (const marker of plan.markers) {
|
|
2491
|
+
try {
|
|
2492
|
+
fs4.rmSync(path4.join(dir, marker), { force: true });
|
|
2493
|
+
} catch {
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const failed = [];
|
|
2497
|
+
for (const sandbox of plan.remove) {
|
|
2498
|
+
try {
|
|
2499
|
+
fs4.rmSync(path4.join(dir, sandbox), { recursive: true, force: true });
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
failed.push(`${sandbox} (${error.message})`);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
const removed = plan.remove.length - failed.length;
|
|
2505
|
+
return [
|
|
2506
|
+
...removed > 0 ? [
|
|
2507
|
+
`test-gates mutation: removed ${removed} sandbox(es) an earlier Stryker run left in ${shown}`
|
|
2508
|
+
] : [],
|
|
2509
|
+
...failed.length > 0 ? [`test-gates mutation: could not remove from ${shown}: ${failed.join(", ")}`] : []
|
|
2510
|
+
];
|
|
2511
|
+
}
|
|
2512
|
+
function markRun(subproject, setting) {
|
|
2513
|
+
const dir = setting.dir;
|
|
2514
|
+
if (dir === null || !insideSubproject(relativeTempDir(subproject, dir))) {
|
|
2515
|
+
return () => void 0;
|
|
2516
|
+
}
|
|
2517
|
+
const marker = path4.join(dir, markerName(process.pid));
|
|
2518
|
+
try {
|
|
2519
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2520
|
+
if (!insideSubproject(relativeTempDir(subproject, dir))) {
|
|
2521
|
+
return () => void 0;
|
|
2522
|
+
}
|
|
2523
|
+
fs4.writeFileSync(marker, markerText(process.pid, startedAt(process.pid)));
|
|
2524
|
+
} catch {
|
|
2525
|
+
return () => void 0;
|
|
2526
|
+
}
|
|
2527
|
+
return () => {
|
|
2528
|
+
try {
|
|
2529
|
+
fs4.rmSync(marker, { force: true });
|
|
2530
|
+
fs4.rmdirSync(dir);
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// src/snapshot.ts
|
|
2537
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1770
2538
|
import { createHash } from "crypto";
|
|
1771
|
-
import
|
|
2539
|
+
import fs6 from "fs";
|
|
1772
2540
|
import { createRequire } from "module";
|
|
1773
2541
|
import os from "os";
|
|
1774
|
-
import
|
|
2542
|
+
import path6 from "path";
|
|
1775
2543
|
|
|
1776
2544
|
// src/commands/loaded-files.ts
|
|
1777
|
-
import
|
|
2545
|
+
import fs5 from "fs";
|
|
1778
2546
|
import { builtinModules } from "module";
|
|
1779
|
-
import
|
|
2547
|
+
import path5 from "path";
|
|
1780
2548
|
var SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
1781
2549
|
var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
|
|
1782
2550
|
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1783
2551
|
var QUOTED = /(['"`])([^'"`\n]+)\1/g;
|
|
1784
|
-
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
2552
|
+
var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
|
|
1785
2553
|
function isFile(file) {
|
|
1786
2554
|
try {
|
|
1787
|
-
return
|
|
2555
|
+
return fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
|
|
1788
2556
|
} catch {
|
|
1789
2557
|
return false;
|
|
1790
2558
|
}
|
|
1791
2559
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2560
|
+
var resolved = null;
|
|
2561
|
+
function resolving(read2) {
|
|
2562
|
+
if (resolved !== null) {
|
|
2563
|
+
return read2();
|
|
2564
|
+
}
|
|
2565
|
+
resolved = /* @__PURE__ */ new Map();
|
|
2566
|
+
try {
|
|
2567
|
+
return read2();
|
|
2568
|
+
} finally {
|
|
2569
|
+
resolved = null;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
var DECLARATIONS = [".d.ts", ".d.mts", ".d.cts"];
|
|
2573
|
+
function resolveFiles(base, directory = true, declarations = true) {
|
|
2574
|
+
const key = `${Number(directory)}${Number(declarations)}${base}`;
|
|
2575
|
+
const known = resolved?.get(key);
|
|
2576
|
+
if (known !== void 0) {
|
|
2577
|
+
return [...known];
|
|
2578
|
+
}
|
|
2579
|
+
const found = filesOf(base, directory, declarations);
|
|
2580
|
+
resolved?.set(key, found);
|
|
2581
|
+
return [...found];
|
|
2582
|
+
}
|
|
2583
|
+
function filesOf(base, directory, declarations) {
|
|
2584
|
+
const candidates = (extensions) => [
|
|
2585
|
+
...extensions.map((extension) => base + extension),
|
|
1796
2586
|
// `./fee.js` written in a TypeScript file is `./fee.ts`
|
|
1797
|
-
...
|
|
1798
|
-
...directory ?
|
|
2587
|
+
...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
|
|
2588
|
+
...directory ? extensions.map((extension) => path5.join(base, `index${extension}`)) : []
|
|
1799
2589
|
];
|
|
1800
|
-
|
|
2590
|
+
const data = [`${base}.json`, ...directory ? [path5.join(base, "index.json")] : []];
|
|
2591
|
+
const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
|
|
2592
|
+
return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
|
|
2593
|
+
}
|
|
2594
|
+
function resolveFile(base, directory = true, declarations = true) {
|
|
2595
|
+
return resolveFiles(base, directory, declarations)[0] ?? null;
|
|
1801
2596
|
}
|
|
1802
2597
|
function installed(specifier, from) {
|
|
1803
2598
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
1804
|
-
for (let current =
|
|
1805
|
-
if (
|
|
2599
|
+
for (let current = path5.dirname(from); ; current = path5.dirname(current)) {
|
|
2600
|
+
if (fs5.existsSync(path5.join(current, "node_modules", name))) {
|
|
1806
2601
|
return true;
|
|
1807
2602
|
}
|
|
1808
|
-
if (current ===
|
|
2603
|
+
if (current === path5.dirname(current)) {
|
|
1809
2604
|
return false;
|
|
1810
2605
|
}
|
|
1811
2606
|
}
|
|
1812
2607
|
}
|
|
1813
2608
|
function loadedFiles(manifest, configFile) {
|
|
2609
|
+
return resolving(() => loadedFilesNow(manifest, configFile));
|
|
2610
|
+
}
|
|
2611
|
+
function loadedFilesNow(manifest, configFile) {
|
|
1814
2612
|
const aliases = manifest.settings.importAliases;
|
|
1815
|
-
const relative = (file) =>
|
|
2613
|
+
const relative = (file) => path5.relative(manifest.dir, file).split(path5.sep).join("/");
|
|
1816
2614
|
const target = (specifier, from) => {
|
|
1817
2615
|
if (specifier.startsWith(".")) {
|
|
1818
|
-
return
|
|
2616
|
+
return resolveFiles(path5.resolve(path5.dirname(from), specifier));
|
|
1819
2617
|
}
|
|
1820
2618
|
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
1821
|
-
return alias === void 0 ?
|
|
1822
|
-
|
|
2619
|
+
return alias === void 0 ? [] : resolveFiles(
|
|
2620
|
+
path5.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
|
|
1823
2621
|
);
|
|
1824
2622
|
};
|
|
1825
2623
|
const read2 = (file) => {
|
|
1826
|
-
const source =
|
|
2624
|
+
const source = fs5.readFileSync(file, "utf8");
|
|
1827
2625
|
const imports = parseImports(source).filter((ref) => !ref.typeOnly);
|
|
1828
2626
|
const specifiers = [
|
|
1829
2627
|
.../* @__PURE__ */ new Set([
|
|
@@ -1835,7 +2633,7 @@ function loadedFiles(manifest, configFile) {
|
|
|
1835
2633
|
return { source, specifiers, strings };
|
|
1836
2634
|
};
|
|
1837
2635
|
const named = /* @__PURE__ */ new Set();
|
|
1838
|
-
const configs = configFile === null ? [] : [
|
|
2636
|
+
const configs = configFile === null ? [] : [path5.resolve(manifest.dir, configFile)];
|
|
1839
2637
|
const seenConfigs = new Set(configs);
|
|
1840
2638
|
const entry = configs[0];
|
|
1841
2639
|
for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
|
|
@@ -1843,16 +2641,18 @@ function loadedFiles(manifest, configFile) {
|
|
|
1843
2641
|
const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
|
|
1844
2642
|
for (const text of paths) {
|
|
1845
2643
|
const stripped = text.replace(/^<rootDir>\//, "");
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
|
|
2644
|
+
const here = resolveFiles(path5.resolve(path5.dirname(file), stripped), false);
|
|
2645
|
+
const found = here.length > 0 ? here : resolveFiles(path5.resolve(manifest.dir, stripped), false);
|
|
2646
|
+
for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
|
|
2647
|
+
named.add(one);
|
|
1849
2648
|
}
|
|
1850
2649
|
}
|
|
1851
2650
|
for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
|
|
1852
|
-
const found
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2651
|
+
for (const found of target(specifier, file)) {
|
|
2652
|
+
if (SCRIPT.test(found) && !seenConfigs.has(found)) {
|
|
2653
|
+
seenConfigs.add(found);
|
|
2654
|
+
configs.push(found);
|
|
2655
|
+
}
|
|
1856
2656
|
}
|
|
1857
2657
|
}
|
|
1858
2658
|
}
|
|
@@ -1869,10 +2669,10 @@ function loadedFiles(manifest, configFile) {
|
|
|
1869
2669
|
for (const specifier of specifiers) {
|
|
1870
2670
|
if (isProjectImport(specifier, aliases)) {
|
|
1871
2671
|
const found = target(specifier, file);
|
|
1872
|
-
if (found ===
|
|
2672
|
+
if (found.length === 0) {
|
|
1873
2673
|
lost ??= `${specifier} (imported by ${relative(file)})`;
|
|
1874
2674
|
} else {
|
|
1875
|
-
next.push(found);
|
|
2675
|
+
next.push(...found);
|
|
1876
2676
|
}
|
|
1877
2677
|
} else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
|
|
1878
2678
|
imports.push({ specifier, builtin: true });
|
|
@@ -1883,10 +2683,7 @@ function loadedFiles(manifest, configFile) {
|
|
|
1883
2683
|
}
|
|
1884
2684
|
}
|
|
1885
2685
|
for (const text of strings.filter((one) => one.startsWith("."))) {
|
|
1886
|
-
|
|
1887
|
-
if (found !== null) {
|
|
1888
|
-
next.push(found);
|
|
1889
|
-
}
|
|
2686
|
+
next.push(...resolveFiles(path5.resolve(path5.dirname(file), text)));
|
|
1890
2687
|
}
|
|
1891
2688
|
const visited = {
|
|
1892
2689
|
file: { path: relative(file), source, imports },
|
|
@@ -1916,7 +2713,7 @@ function loadedFiles(manifest, configFile) {
|
|
|
1916
2713
|
const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
|
|
1917
2714
|
const roots = [
|
|
1918
2715
|
closure(null, [...named]),
|
|
1919
|
-
...specs.map((spec) => closure(spec, [
|
|
2716
|
+
...specs.map((spec) => closure(spec, [path5.join(manifest.dir, spec)]))
|
|
1920
2717
|
];
|
|
1921
2718
|
return { files: [...known.values()].map((visited) => visited.file), roots };
|
|
1922
2719
|
}
|
|
@@ -1957,27 +2754,28 @@ var NEVER = ["node_modules", ".git"];
|
|
|
1957
2754
|
var WIDENED_SHOWN = 5;
|
|
1958
2755
|
var SCAN_LIMIT = 8 * 1024 * 1024;
|
|
1959
2756
|
var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
|
|
1960
|
-
var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
|
|
2757
|
+
var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc", ".html", ".htm"];
|
|
1961
2758
|
var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
|
|
1962
2759
|
var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
|
|
1963
2760
|
var LITERAL = /(['"])([^'"`\n]*)\1/g;
|
|
2761
|
+
var DOWNWARD = /(['"`])(\.\/[^'"`\n]*)\1/g;
|
|
1964
2762
|
var isUpward = (segment) => segment.split("/").includes("..");
|
|
1965
2763
|
function hashOf(content) {
|
|
1966
2764
|
return createHash("sha256").update(content).digest("hex");
|
|
1967
2765
|
}
|
|
1968
2766
|
function hashFile(file) {
|
|
1969
2767
|
try {
|
|
1970
|
-
return hashOf(
|
|
2768
|
+
return hashOf(fs6.readFileSync(file));
|
|
1971
2769
|
} catch {
|
|
1972
2770
|
return null;
|
|
1973
2771
|
}
|
|
1974
2772
|
}
|
|
1975
2773
|
function isInside(parent, child) {
|
|
1976
|
-
const relative =
|
|
1977
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2774
|
+
const relative = path6.relative(parent, child);
|
|
2775
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
1978
2776
|
}
|
|
1979
2777
|
function toPosix(file) {
|
|
1980
|
-
return file.split(
|
|
2778
|
+
return file.split(path6.sep).join("/");
|
|
1981
2779
|
}
|
|
1982
2780
|
function gitEnvironment(env) {
|
|
1983
2781
|
if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
|
|
@@ -1988,7 +2786,7 @@ function gitEnvironment(env) {
|
|
|
1988
2786
|
return rest;
|
|
1989
2787
|
}
|
|
1990
2788
|
function git(cwd, args) {
|
|
1991
|
-
const result =
|
|
2789
|
+
const result = spawnSync2("git", args, {
|
|
1992
2790
|
cwd,
|
|
1993
2791
|
env: gitEnvironment(process.env),
|
|
1994
2792
|
encoding: "utf8",
|
|
@@ -1996,9 +2794,12 @@ function git(cwd, args) {
|
|
|
1996
2794
|
});
|
|
1997
2795
|
return result.error || result.status !== 0 ? null : result.stdout;
|
|
1998
2796
|
}
|
|
2797
|
+
function globLiteral(relative) {
|
|
2798
|
+
return relative.replace(/[*?[\]\\]/g, "\\$&");
|
|
2799
|
+
}
|
|
1999
2800
|
function gitLister(root) {
|
|
2000
2801
|
return (directory, recursive) => {
|
|
2001
|
-
const relative = toPosix(
|
|
2802
|
+
const relative = globLiteral(toPosix(path6.relative(root, directory)));
|
|
2002
2803
|
const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
|
|
2003
2804
|
const listed2 = git(root, [
|
|
2004
2805
|
"ls-files",
|
|
@@ -2017,8 +2818,8 @@ function gitLister(root) {
|
|
|
2017
2818
|
}
|
|
2018
2819
|
function walkLister(root) {
|
|
2019
2820
|
const walk = (directory, recursive, found) => {
|
|
2020
|
-
for (const entry of
|
|
2021
|
-
const file =
|
|
2821
|
+
for (const entry of fs6.readdirSync(directory, { withFileTypes: true })) {
|
|
2822
|
+
const file = path6.join(directory, entry.name);
|
|
2022
2823
|
if (NEVER.includes(entry.name)) {
|
|
2023
2824
|
continue;
|
|
2024
2825
|
}
|
|
@@ -2027,7 +2828,7 @@ function walkLister(root) {
|
|
|
2027
2828
|
walk(file, true, found);
|
|
2028
2829
|
}
|
|
2029
2830
|
} else {
|
|
2030
|
-
found.push(toPosix(
|
|
2831
|
+
found.push(toPosix(path6.relative(root, file)));
|
|
2031
2832
|
}
|
|
2032
2833
|
}
|
|
2033
2834
|
return found;
|
|
@@ -2037,16 +2838,16 @@ function walkLister(root) {
|
|
|
2037
2838
|
function upTo(from, to) {
|
|
2038
2839
|
const directories = [from];
|
|
2039
2840
|
while (directories[directories.length - 1] !== to) {
|
|
2040
|
-
directories.push(
|
|
2841
|
+
directories.push(path6.dirname(directories[directories.length - 1]));
|
|
2041
2842
|
}
|
|
2042
2843
|
return directories;
|
|
2043
2844
|
}
|
|
2044
2845
|
function declaresWorkspaces(directory) {
|
|
2045
|
-
if (
|
|
2846
|
+
if (fs6.existsSync(path6.join(directory, "pnpm-workspace.yaml"))) {
|
|
2046
2847
|
return true;
|
|
2047
2848
|
}
|
|
2048
2849
|
try {
|
|
2049
|
-
const manifest = JSON.parse(
|
|
2850
|
+
const manifest = JSON.parse(fs6.readFileSync(path6.join(directory, "package.json"), "utf8"));
|
|
2050
2851
|
return manifest.workspaces !== void 0;
|
|
2051
2852
|
} catch {
|
|
2052
2853
|
return false;
|
|
@@ -2054,7 +2855,7 @@ function declaresWorkspaces(directory) {
|
|
|
2054
2855
|
}
|
|
2055
2856
|
function walkRoot(dir) {
|
|
2056
2857
|
const candidates = withinBounds(dir);
|
|
2057
|
-
const repository = candidates.find((directory) =>
|
|
2858
|
+
const repository = candidates.find((directory) => fs6.existsSync(path6.join(directory, ".git")));
|
|
2058
2859
|
if (repository !== void 0) {
|
|
2059
2860
|
return repository;
|
|
2060
2861
|
}
|
|
@@ -2063,11 +2864,11 @@ function walkRoot(dir) {
|
|
|
2063
2864
|
function withinBounds(dir) {
|
|
2064
2865
|
let home = null;
|
|
2065
2866
|
try {
|
|
2066
|
-
home =
|
|
2867
|
+
home = fs6.realpathSync(os.homedir());
|
|
2067
2868
|
} catch {
|
|
2068
2869
|
}
|
|
2069
|
-
return upTo(dir,
|
|
2070
|
-
(directory) => directory !==
|
|
2870
|
+
return upTo(dir, path6.parse(dir).root).filter(
|
|
2871
|
+
(directory) => directory !== path6.parse(dir).root && (home === null || !isInside(directory, home))
|
|
2071
2872
|
);
|
|
2072
2873
|
}
|
|
2073
2874
|
function linkedPackages(nodeModules, root) {
|
|
@@ -2075,20 +2876,20 @@ function linkedPackages(nodeModules, root) {
|
|
|
2075
2876
|
const visit = (directory, depth) => {
|
|
2076
2877
|
let entries;
|
|
2077
2878
|
try {
|
|
2078
|
-
entries =
|
|
2879
|
+
entries = fs6.readdirSync(directory, { withFileTypes: true });
|
|
2079
2880
|
} catch {
|
|
2080
2881
|
return;
|
|
2081
2882
|
}
|
|
2082
2883
|
for (const entry of entries) {
|
|
2083
|
-
const file =
|
|
2884
|
+
const file = path6.join(directory, entry.name);
|
|
2084
2885
|
if (entry.isSymbolicLink()) {
|
|
2085
2886
|
let target;
|
|
2086
2887
|
try {
|
|
2087
|
-
target =
|
|
2888
|
+
target = fs6.realpathSync(file);
|
|
2088
2889
|
} catch {
|
|
2089
2890
|
continue;
|
|
2090
2891
|
}
|
|
2091
|
-
if (target.split(
|
|
2892
|
+
if (target.split(path6.sep).includes("node_modules") || !fs6.statSync(target).isDirectory()) {
|
|
2092
2893
|
continue;
|
|
2093
2894
|
}
|
|
2094
2895
|
if (!isInside(root, target)) {
|
|
@@ -2110,11 +2911,11 @@ function installedVersion(start, name) {
|
|
|
2110
2911
|
for (; ; ) {
|
|
2111
2912
|
try {
|
|
2112
2913
|
const manifest = JSON.parse(
|
|
2113
|
-
|
|
2914
|
+
fs6.readFileSync(path6.join(current, "node_modules", name, "package.json"), "utf8")
|
|
2114
2915
|
);
|
|
2115
2916
|
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
2116
2917
|
} catch {
|
|
2117
|
-
const parent =
|
|
2918
|
+
const parent = path6.dirname(current);
|
|
2118
2919
|
if (parent === current) {
|
|
2119
2920
|
return null;
|
|
2120
2921
|
}
|
|
@@ -2122,17 +2923,30 @@ function installedVersion(start, name) {
|
|
|
2122
2923
|
}
|
|
2123
2924
|
}
|
|
2124
2925
|
}
|
|
2926
|
+
function nodeOnPath(cwd) {
|
|
2927
|
+
const result = spawnSync2("node", ["--version"], {
|
|
2928
|
+
cwd,
|
|
2929
|
+
encoding: "utf8",
|
|
2930
|
+
shell: process.platform === "win32",
|
|
2931
|
+
timeout: 3e4
|
|
2932
|
+
});
|
|
2933
|
+
const version = result.status === 0 ? result.stdout.trim() : "";
|
|
2934
|
+
return version === "" ? {} : { "Node.js on PATH": version };
|
|
2935
|
+
}
|
|
2125
2936
|
function ownVersion() {
|
|
2126
2937
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
2127
2938
|
return version;
|
|
2128
2939
|
}
|
|
2129
2940
|
function snapshotOf(input) {
|
|
2130
|
-
|
|
2941
|
+
return resolving(() => snapshotNow(input));
|
|
2942
|
+
}
|
|
2943
|
+
function snapshotNow(input) {
|
|
2944
|
+
const dir = fs6.realpathSync(input.dir);
|
|
2131
2945
|
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
2132
|
-
let root = top === null ? dir :
|
|
2946
|
+
let root = top === null ? dir : fs6.realpathSync(top.trim());
|
|
2133
2947
|
let lister = gitLister(root);
|
|
2134
2948
|
let mode = "git";
|
|
2135
|
-
const manifest = toPosix(
|
|
2949
|
+
const manifest = toPosix(path6.relative(root, path6.join(dir, "test-gates.json")));
|
|
2136
2950
|
if (top === null || !lister(dir, false).includes(manifest)) {
|
|
2137
2951
|
mode = "walk";
|
|
2138
2952
|
root = walkRoot(dir);
|
|
@@ -2140,17 +2954,17 @@ function snapshotOf(input) {
|
|
|
2140
2954
|
}
|
|
2141
2955
|
const listRoot = root;
|
|
2142
2956
|
const list = lister;
|
|
2143
|
-
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) =>
|
|
2957
|
+
const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path6.join(listRoot, file)));
|
|
2144
2958
|
const walk = walkLister(listRoot);
|
|
2145
2959
|
const written = input.written.map((file) => {
|
|
2146
|
-
const target =
|
|
2960
|
+
const target = path6.resolve(dir, file);
|
|
2147
2961
|
try {
|
|
2148
|
-
return
|
|
2962
|
+
return fs6.realpathSync(target);
|
|
2149
2963
|
} catch {
|
|
2150
2964
|
return target;
|
|
2151
2965
|
}
|
|
2152
2966
|
});
|
|
2153
|
-
const swallowing = written.find((target) => isInside(target,
|
|
2967
|
+
const swallowing = written.find((target) => isInside(target, path6.join(dir, "test-gates.json")));
|
|
2154
2968
|
if (swallowing !== void 0) {
|
|
2155
2969
|
throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
|
|
2156
2970
|
}
|
|
@@ -2167,12 +2981,12 @@ function snapshotOf(input) {
|
|
|
2167
2981
|
if (!isInside(listRoot, directory)) {
|
|
2168
2982
|
const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
|
|
2169
2983
|
throw new SnapshotError(
|
|
2170
|
-
`${toPosix(
|
|
2984
|
+
`${toPosix(path6.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
|
|
2171
2985
|
);
|
|
2172
2986
|
}
|
|
2173
2987
|
if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
|
|
2174
2988
|
const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
|
|
2175
|
-
widened.push(`${toPosix(
|
|
2989
|
+
widened.push(`${toPosix(path6.relative(listRoot, from))}${written2}`);
|
|
2176
2990
|
}
|
|
2177
2991
|
if (![...queued].some((region) => isInside(region, directory))) {
|
|
2178
2992
|
queued.add(directory);
|
|
@@ -2180,19 +2994,74 @@ function snapshotOf(input) {
|
|
|
2180
2994
|
}
|
|
2181
2995
|
};
|
|
2182
2996
|
const follow = (fixed, open, anchored, file, reference) => {
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2997
|
+
const from = (start) => {
|
|
2998
|
+
const target = path6.resolve(start, fixed);
|
|
2999
|
+
let isDirectory2 = false;
|
|
3000
|
+
try {
|
|
3001
|
+
isDirectory2 = fs6.statSync(target).isDirectory();
|
|
3002
|
+
} catch {
|
|
3003
|
+
}
|
|
3004
|
+
return {
|
|
3005
|
+
target,
|
|
3006
|
+
isDirectory: isDirectory2,
|
|
3007
|
+
found: isDirectory2 || resolveFile(target, true, false) !== null
|
|
3008
|
+
};
|
|
3009
|
+
};
|
|
3010
|
+
const fromFile = from(path6.dirname(file));
|
|
3011
|
+
const targets = [fromFile];
|
|
3012
|
+
if (!anchored && path6.dirname(file) !== dir) {
|
|
3013
|
+
const fromRun = from(dir);
|
|
3014
|
+
if (fromRun.found && !fromFile.found) {
|
|
3015
|
+
targets[0] = fromRun;
|
|
3016
|
+
} else if (fromRun.found && isInside(fromRun.target, listRoot)) {
|
|
3017
|
+
enqueue(listRoot, file, reference);
|
|
3018
|
+
} else if (fromRun.found) {
|
|
3019
|
+
targets.push(fromRun);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
for (const { target, isDirectory: isDirectory2 } of targets) {
|
|
3023
|
+
const beside = (isDirectory2 || open ? [] : resolveFiles(target, false, false)).filter(
|
|
3024
|
+
(one) => besideAncestors.has(one)
|
|
3025
|
+
);
|
|
3026
|
+
if (beside.length > 0) {
|
|
3027
|
+
beside.forEach(reach);
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
|
|
3031
|
+
files[toPosix(path6.relative(listRoot, target))] = "missing";
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
const directory = isDirectory2 ? target : path6.dirname(target);
|
|
3035
|
+
if (fs6.existsSync(directory)) {
|
|
3036
|
+
enqueue(directory, file, reference, anchored && !open);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
3040
|
+
const reached = /* @__PURE__ */ new Set();
|
|
3041
|
+
const reach = (file) => {
|
|
3042
|
+
if (reached.has(file) || DOCUMENTS.includes(path6.extname(file).toLowerCase())) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
reached.add(file);
|
|
3046
|
+
let text;
|
|
2185
3047
|
try {
|
|
2186
|
-
|
|
3048
|
+
const content = fs6.readFileSync(file);
|
|
3049
|
+
if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
text = content.toString("utf8").replace(/\\[\\/]/g, "/");
|
|
2187
3053
|
} catch {
|
|
2188
|
-
}
|
|
2189
|
-
if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
|
|
2190
|
-
files[toPosix(path5.relative(listRoot, target))] = "missing";
|
|
2191
3054
|
return;
|
|
2192
3055
|
}
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
3056
|
+
for (const match of text.matchAll(DOWNWARD)) {
|
|
3057
|
+
const literal = match[2];
|
|
3058
|
+
const open = literal.includes("${");
|
|
3059
|
+
const fixed = open ? literal.slice(0, literal.lastIndexOf("/", literal.indexOf("${")) + 1) : literal;
|
|
3060
|
+
if (open && isUpward(literal.slice(literal.indexOf("${")))) {
|
|
3061
|
+
enqueue(listRoot, file, literal);
|
|
3062
|
+
} else {
|
|
3063
|
+
follow(fixed, open, false, file, literal);
|
|
3064
|
+
}
|
|
2196
3065
|
}
|
|
2197
3066
|
};
|
|
2198
3067
|
const scan = (text, file) => {
|
|
@@ -2208,7 +3077,7 @@ function snapshotOf(input) {
|
|
|
2208
3077
|
const end = (match.index ?? 0) + match[0].length;
|
|
2209
3078
|
const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
|
|
2210
3079
|
const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
|
|
2211
|
-
follow(
|
|
3080
|
+
follow(path6.posix.join(...segments), open, true, file, reference);
|
|
2212
3081
|
}
|
|
2213
3082
|
for (const match of text.matchAll(UPWARD)) {
|
|
2214
3083
|
const at = match.index ?? 0;
|
|
@@ -2228,70 +3097,91 @@ function snapshotOf(input) {
|
|
|
2228
3097
|
}
|
|
2229
3098
|
};
|
|
2230
3099
|
const add = (relative) => {
|
|
2231
|
-
const file =
|
|
3100
|
+
const file = path6.join(listRoot, relative);
|
|
2232
3101
|
if (relative in files || written.some((target) => isInside(target, file))) {
|
|
2233
3102
|
return;
|
|
2234
3103
|
}
|
|
2235
3104
|
let stat;
|
|
2236
3105
|
try {
|
|
2237
|
-
stat =
|
|
3106
|
+
stat = fs6.lstatSync(file);
|
|
2238
3107
|
} catch {
|
|
2239
3108
|
files[relative] = "missing";
|
|
2240
3109
|
return;
|
|
2241
3110
|
}
|
|
2242
3111
|
if (stat.isSymbolicLink()) {
|
|
2243
|
-
files[relative] = `link ${
|
|
3112
|
+
files[relative] = `link ${fs6.readlinkSync(file)}`;
|
|
2244
3113
|
let target;
|
|
2245
3114
|
try {
|
|
2246
|
-
target =
|
|
3115
|
+
target = fs6.realpathSync(file);
|
|
2247
3116
|
} catch {
|
|
2248
3117
|
return;
|
|
2249
3118
|
}
|
|
2250
|
-
enqueue(
|
|
3119
|
+
enqueue(fs6.statSync(target).isDirectory() ? target : path6.dirname(target), file);
|
|
2251
3120
|
} else if (stat.isDirectory()) {
|
|
2252
3121
|
files[relative] = "directory";
|
|
2253
3122
|
walk(file, true).forEach(add);
|
|
2254
3123
|
} else if (!stat.isFile()) {
|
|
2255
3124
|
files[relative] = "special";
|
|
2256
3125
|
} else {
|
|
2257
|
-
const content =
|
|
3126
|
+
const content = fs6.readFileSync(file);
|
|
2258
3127
|
files[relative] = hashOf(content);
|
|
2259
|
-
const document = DOCUMENTS.includes(
|
|
3128
|
+
const document = DOCUMENTS.includes(path6.extname(file).toLowerCase());
|
|
2260
3129
|
const loaded = followed === null || !isInside(dir, file) || followed.has(file);
|
|
2261
3130
|
if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
|
|
2262
3131
|
scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
|
|
2263
3132
|
}
|
|
2264
3133
|
}
|
|
2265
|
-
};
|
|
2266
|
-
const ancestors = upTo(dir, listRoot).slice(1);
|
|
2267
|
-
|
|
2268
|
-
|
|
3134
|
+
};
|
|
3135
|
+
const ancestors = upTo(dir, listRoot).slice(1);
|
|
3136
|
+
const besideAncestors = /* @__PURE__ */ new Set();
|
|
3137
|
+
const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
|
|
3138
|
+
for (const relative of ancestorFiles) {
|
|
3139
|
+
besideAncestors.add(path6.join(listRoot, relative));
|
|
3140
|
+
}
|
|
3141
|
+
ancestorFiles.forEach(add);
|
|
3142
|
+
for (const directory of [dir, ...ancestors]) {
|
|
3143
|
+
const patches = path6.join(directory, "patches");
|
|
3144
|
+
if (fs6.existsSync(patches) && fs6.statSync(patches).isDirectory()) {
|
|
3145
|
+
enqueue(patches, directory);
|
|
3146
|
+
}
|
|
2269
3147
|
}
|
|
2270
3148
|
for (const directory of [dir, ...ancestors]) {
|
|
2271
|
-
for (const linked of linkedPackages(
|
|
3149
|
+
for (const linked of linkedPackages(path6.join(directory, "node_modules"), listRoot)) {
|
|
2272
3150
|
if (!isInside(linked, dir)) {
|
|
2273
|
-
enqueue(linked,
|
|
3151
|
+
enqueue(linked, path6.join(directory, "node_modules"));
|
|
2274
3152
|
}
|
|
2275
3153
|
}
|
|
2276
3154
|
}
|
|
2277
3155
|
const above = withinBounds(listRoot).slice(1);
|
|
2278
3156
|
for (const directory of [dir, ...ancestors, ...above]) {
|
|
2279
3157
|
const candidates = [
|
|
2280
|
-
...INSTALL_MARKERS.map((marker) =>
|
|
2281
|
-
...above.includes(directory) ? LOCK_FILES.map((name) =>
|
|
3158
|
+
...INSTALL_MARKERS.map((marker) => path6.join(directory, "node_modules", marker)),
|
|
3159
|
+
...above.includes(directory) ? LOCK_FILES.map((name) => path6.join(directory, name)) : []
|
|
2282
3160
|
];
|
|
2283
3161
|
for (const file of candidates) {
|
|
2284
3162
|
const hash = hashFile(file);
|
|
2285
3163
|
if (hash !== null) {
|
|
2286
|
-
files[toPosix(
|
|
3164
|
+
files[toPosix(path6.relative(listRoot, file))] = hash;
|
|
2287
3165
|
}
|
|
2288
3166
|
}
|
|
2289
3167
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
3168
|
+
let position = 0;
|
|
3169
|
+
const drain = () => {
|
|
3170
|
+
for (; position < regions.length; position += 1) {
|
|
3171
|
+
list(regions[position], true).forEach(add);
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
drain();
|
|
3175
|
+
for (const file of followed ?? []) {
|
|
3176
|
+
const parts = path6.relative(listRoot, file).split(path6.sep);
|
|
3177
|
+
if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs6.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
|
|
3178
|
+
add(toPosix(path6.relative(listRoot, file)));
|
|
3179
|
+
}
|
|
2292
3180
|
}
|
|
3181
|
+
drain();
|
|
2293
3182
|
const environment = {
|
|
2294
3183
|
"Node.js": process.version,
|
|
3184
|
+
...nodeOnPath(dir),
|
|
2295
3185
|
platform: `${process.platform} ${process.arch}`,
|
|
2296
3186
|
"test-gates": ownVersion()
|
|
2297
3187
|
};
|
|
@@ -2305,12 +3195,26 @@ function snapshotOf(input) {
|
|
|
2305
3195
|
}
|
|
2306
3196
|
|
|
2307
3197
|
// src/affected-files.ts
|
|
2308
|
-
import
|
|
3198
|
+
import fs7 from "fs";
|
|
2309
3199
|
import { builtinModules as builtinModules2 } from "module";
|
|
2310
|
-
import
|
|
3200
|
+
import path7 from "path";
|
|
2311
3201
|
var AffectedError = class extends Error {
|
|
2312
3202
|
};
|
|
2313
|
-
var RUNTIME_PATHS = /\b(?:
|
|
3203
|
+
var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readFile|readdirSync|fromFile|fromURL)\b|\bimport\.meta\.(?:glob|resolve)\b/;
|
|
3204
|
+
var PATH_FROM_TOP = /^[^\s./?#*:][^\s?#*:]*\.(?:html?|md|json)$/i;
|
|
3205
|
+
var withoutQuery = (specifier) => specifier.replace(/\?[^/]*$/, "");
|
|
3206
|
+
var RUNTIME_LOADS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire)\b|\bimport\.meta\.(?:glob|resolve)\b/;
|
|
3207
|
+
var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
|
|
3208
|
+
var LOCATION_READS = /\b(?:readFile|readdir|readlink|existsSync|stat|statSync|lstat|lstatSync|access|accessSync|realpath|realpathSync|open|openSync|opendir|opendirSync|createReadStream|watch|watchFile|glob|globSync|promises)\b|\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
|
|
3209
|
+
var LOCATION_LOADS = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
|
|
3210
|
+
var LOCATION_SAFE = /* @__PURE__ */ new Set([
|
|
3211
|
+
"path",
|
|
3212
|
+
"path/posix",
|
|
3213
|
+
"path/win32",
|
|
3214
|
+
"url",
|
|
3215
|
+
"vitest",
|
|
3216
|
+
"@jest/globals"
|
|
3217
|
+
]);
|
|
2314
3218
|
var FILE_MODULES = /* @__PURE__ */ new Set([
|
|
2315
3219
|
"fs",
|
|
2316
3220
|
"fs/promises",
|
|
@@ -2323,30 +3227,31 @@ var FILE_MODULES = /* @__PURE__ */ new Set([
|
|
|
2323
3227
|
"fast-glob",
|
|
2324
3228
|
"globby"
|
|
2325
3229
|
]);
|
|
3230
|
+
var LOADING_MODULES = /* @__PURE__ */ new Set(["child_process", "module", "worker_threads"]);
|
|
2326
3231
|
var bare = (specifier) => specifier.replace(/^node:/, "");
|
|
2327
3232
|
function linkedPackage(specifier, from) {
|
|
2328
3233
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
2329
|
-
for (let current =
|
|
2330
|
-
const candidate =
|
|
2331
|
-
if (
|
|
3234
|
+
for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
|
|
3235
|
+
const candidate = path7.join(current, "node_modules", name);
|
|
3236
|
+
if (fs7.existsSync(candidate)) {
|
|
2332
3237
|
const target = realpath(candidate);
|
|
2333
|
-
return target.split(
|
|
3238
|
+
return target.split(path7.sep).includes("node_modules") ? null : target;
|
|
2334
3239
|
}
|
|
2335
|
-
if (current ===
|
|
3240
|
+
if (current === path7.dirname(current)) {
|
|
2336
3241
|
return null;
|
|
2337
3242
|
}
|
|
2338
3243
|
}
|
|
2339
3244
|
}
|
|
2340
|
-
var toPosix2 = (file) => file.split(
|
|
3245
|
+
var toPosix2 = (file) => file.split(path7.sep).join("/");
|
|
2341
3246
|
function realpath(file) {
|
|
2342
3247
|
try {
|
|
2343
|
-
return
|
|
3248
|
+
return fs7.realpathSync.native(file);
|
|
2344
3249
|
} catch {
|
|
2345
3250
|
return file;
|
|
2346
3251
|
}
|
|
2347
3252
|
}
|
|
2348
3253
|
function runnerConfigs(dir, stryker, options) {
|
|
2349
|
-
const files = [
|
|
3254
|
+
const files = [path7.resolve(dir, stryker)];
|
|
2350
3255
|
const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
|
|
2351
3256
|
if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
|
|
2352
3257
|
throw new AffectedError(
|
|
@@ -2354,129 +3259,325 @@ function runnerConfigs(dir, stryker, options) {
|
|
|
2354
3259
|
);
|
|
2355
3260
|
}
|
|
2356
3261
|
if (typeof named === "string") {
|
|
2357
|
-
files.push(
|
|
3262
|
+
files.push(path7.resolve(dir, named));
|
|
2358
3263
|
} else {
|
|
2359
3264
|
const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
|
|
2360
3265
|
for (const name of names) {
|
|
2361
3266
|
for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
|
|
2362
|
-
const file =
|
|
2363
|
-
if (
|
|
3267
|
+
const file = path7.join(dir, name + extension);
|
|
3268
|
+
if (fs7.existsSync(file)) {
|
|
2364
3269
|
files.push(file);
|
|
2365
3270
|
}
|
|
2366
3271
|
}
|
|
2367
3272
|
}
|
|
2368
3273
|
}
|
|
2369
|
-
return {
|
|
3274
|
+
return {
|
|
3275
|
+
files: files.filter((file) => fs7.existsSync(file)).map(realpath),
|
|
3276
|
+
runner: options.testRunner,
|
|
3277
|
+
checkers: Array.isArray(options.checkers) && options.checkers.length > 0
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
var CHECKS_TYPES = /\bcheckers\b|typescript-checker|\btypecheck\b/;
|
|
3281
|
+
var TS_JEST_STRIPS = /["']?(?:isolatedModules["']?\s*:\s*true|diagnostics["']?\s*:\s*false)/;
|
|
3282
|
+
var TS_JEST_UNSURE = /\b(?:projects|compilerOptions)\b|\btsconfig["']?\s*:\s*\{/;
|
|
3283
|
+
var STRIPPING_TRANSFORMS = /@swc\/jest|babel-jest|esbuild-jest|@sucrase\/jest/;
|
|
3284
|
+
var KEEPS_IMPORTS = /\b(?:verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|onlyRemoveTypeImports)\b/;
|
|
3285
|
+
var COMPILER_CONFIG = /^(?:[jt]sconfig(?:\..+)?\.json|\.swcrc|\.babelrc(?:\..+)?|babel\.config\..+)$/;
|
|
3286
|
+
function typesErased(configs, texts, compiler) {
|
|
3287
|
+
const all = texts.map(withoutComments).join("\n");
|
|
3288
|
+
let erased = false;
|
|
3289
|
+
if (configs.checkers !== true && !CHECKS_TYPES.test(all)) {
|
|
3290
|
+
if (configs.runner === "vitest") {
|
|
3291
|
+
erased = true;
|
|
3292
|
+
} else if (configs.runner === "jest") {
|
|
3293
|
+
erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path7.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
|
|
3297
|
+
return { erased, inline };
|
|
3298
|
+
}
|
|
3299
|
+
function compilerConfigs(dir, root, keys) {
|
|
3300
|
+
const above = /* @__PURE__ */ new Set();
|
|
3301
|
+
for (let current = path7.dirname(dir); ; current = path7.dirname(current)) {
|
|
3302
|
+
const relative = path7.relative(root, current);
|
|
3303
|
+
if (relative.startsWith("..") || path7.isAbsolute(relative)) {
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
above.add(current);
|
|
3307
|
+
if (current === path7.dirname(current)) {
|
|
3308
|
+
break;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
const compared = new Set(keys.map((key) => path7.join(root, key)));
|
|
3312
|
+
const starts = [...compared].filter(
|
|
3313
|
+
(file) => (COMPILER_CONFIG.test(path7.basename(file)) || path7.basename(file) === "package.json") && (file.startsWith(`${dir}${path7.sep}`) || above.has(path7.dirname(file)))
|
|
3314
|
+
);
|
|
3315
|
+
const found = [];
|
|
3316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3317
|
+
const queue = [...starts];
|
|
3318
|
+
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
3319
|
+
if (seen.has(file)) {
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
seen.add(file);
|
|
3323
|
+
let text = null;
|
|
3324
|
+
if (compared.has(file) || file.split(path7.sep).includes("node_modules")) {
|
|
3325
|
+
try {
|
|
3326
|
+
text = fs7.readFileSync(file, "utf8");
|
|
3327
|
+
} catch {
|
|
3328
|
+
text = null;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
found.push({ file, text });
|
|
3332
|
+
for (const base of text === null ? [] : valuesOf(text, "extends")) {
|
|
3333
|
+
queue.push(extended(base, file) ?? `${file} extends ${base}`);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return found;
|
|
3337
|
+
}
|
|
3338
|
+
function extended(base, from) {
|
|
3339
|
+
const candidates = (start) => [
|
|
3340
|
+
start,
|
|
3341
|
+
`${start}.json`,
|
|
3342
|
+
path7.join(start, "tsconfig.json")
|
|
3343
|
+
];
|
|
3344
|
+
if (base.startsWith(".") || path7.isAbsolute(base)) {
|
|
3345
|
+
return candidates(path7.resolve(path7.dirname(from), base)).find(isFile2) ?? null;
|
|
3346
|
+
}
|
|
3347
|
+
for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
|
|
3348
|
+
const file = candidates(path7.join(current, "node_modules", base)).find(isFile2);
|
|
3349
|
+
if (file !== void 0) {
|
|
3350
|
+
return file;
|
|
3351
|
+
}
|
|
3352
|
+
if (current === path7.dirname(current)) {
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function isFile2(file) {
|
|
3358
|
+
try {
|
|
3359
|
+
return fs7.statSync(file).isFile();
|
|
3360
|
+
} catch {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
2370
3363
|
}
|
|
2371
3364
|
function fileEdges(file, context) {
|
|
2372
|
-
const { dir, aliases, cache } = context;
|
|
3365
|
+
const { dir, aliases, cache, types } = context;
|
|
2373
3366
|
const known = cache.get(file);
|
|
2374
3367
|
if (known !== void 0) {
|
|
2375
3368
|
return known;
|
|
2376
3369
|
}
|
|
2377
|
-
const shown = (one) => toPosix2(
|
|
3370
|
+
const shown = (one) => toPosix2(path7.relative(dir, one));
|
|
2378
3371
|
const target = (specifier, from) => {
|
|
2379
3372
|
if (specifier.startsWith(".")) {
|
|
2380
|
-
return
|
|
3373
|
+
return resolveFiles(path7.resolve(path7.dirname(from), specifier));
|
|
2381
3374
|
}
|
|
2382
3375
|
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
2383
|
-
return alias === void 0 ?
|
|
3376
|
+
return alias === void 0 ? [] : resolveFiles(path7.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
|
|
3377
|
+
};
|
|
3378
|
+
const found = { loads: [], unknown: null, loading: null, computed: false, unnamed: [] };
|
|
3379
|
+
const loading = (reason) => {
|
|
3380
|
+
found.unknown ??= reason;
|
|
3381
|
+
found.loading ??= reason;
|
|
2384
3382
|
};
|
|
2385
|
-
const found = { loads: [], unknown: null, computed: false, unnamed: [] };
|
|
2386
3383
|
cache.set(file, found);
|
|
2387
|
-
const snapshotFile =
|
|
2388
|
-
|
|
3384
|
+
const snapshotFile = path7.join(
|
|
3385
|
+
path7.dirname(file),
|
|
2389
3386
|
"__snapshots__",
|
|
2390
|
-
`${
|
|
3387
|
+
`${path7.basename(file)}.snap`
|
|
2391
3388
|
);
|
|
2392
|
-
if (
|
|
3389
|
+
if (fs7.existsSync(snapshotFile)) {
|
|
2393
3390
|
found.loads.push(realpath(snapshotFile));
|
|
2394
3391
|
}
|
|
3392
|
+
const mocks = path7.join(path7.dirname(file), "__mocks__");
|
|
3393
|
+
const name = path7.basename(file);
|
|
3394
|
+
const candidates = isDirectory(mocks) ? [
|
|
3395
|
+
path7.join(mocks, name),
|
|
3396
|
+
...resolveFiles(path7.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
|
|
3397
|
+
] : [];
|
|
3398
|
+
for (const mock of new Set(candidates)) {
|
|
3399
|
+
if (isFile2(mock)) {
|
|
3400
|
+
found.loads.push(realpath(mock));
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
2395
3403
|
if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
|
|
2396
3404
|
return found;
|
|
2397
3405
|
}
|
|
2398
|
-
const source =
|
|
3406
|
+
const source = fs7.readFileSync(file, "utf8");
|
|
3407
|
+
const code = withoutComments(source);
|
|
2399
3408
|
if (COMPONENT.test(file)) {
|
|
2400
|
-
|
|
3409
|
+
loading(`${shown(file)} is a component, which can use others that it does not import`);
|
|
2401
3410
|
}
|
|
2402
|
-
const uncheckable = findUncheckableImports(
|
|
2403
|
-
const runtime = RUNTIME_PATHS.exec(
|
|
3411
|
+
const uncheckable = findUncheckableImports(code)[0];
|
|
3412
|
+
const runtime = RUNTIME_PATHS.exec(code);
|
|
3413
|
+
const runtimeLoads = RUNTIME_LOADS.exec(code);
|
|
2404
3414
|
if (uncheckable !== void 0) {
|
|
2405
3415
|
found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
|
|
3416
|
+
found.loading = found.unknown;
|
|
2406
3417
|
found.computed = true;
|
|
2407
3418
|
} else if (runtime !== null) {
|
|
2408
3419
|
found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
|
|
3420
|
+
if (runtimeLoads !== null) {
|
|
3421
|
+
found.loading ??= `${shown(file)} uses ${runtimeLoads[0]}, which can reach files that no import names`;
|
|
3422
|
+
}
|
|
2409
3423
|
}
|
|
3424
|
+
const location = LOCATION.exec(code);
|
|
3425
|
+
const refs = parseImports(code);
|
|
3426
|
+
const dropped = (ref) => types !== void 0 && types.erased && (ref.typeOnly || types.inline && ref.inlineTypes === true);
|
|
2410
3427
|
const specifiers = /* @__PURE__ */ new Set([
|
|
2411
|
-
...
|
|
2412
|
-
...[...
|
|
3428
|
+
...refs.filter((ref) => !dropped(ref)).map((ref) => ref.module),
|
|
3429
|
+
...[...code.matchAll(RUNNER_LOAD)].map((match) => match[2])
|
|
2413
3430
|
]);
|
|
3431
|
+
const typeLines = /* @__PURE__ */ new Map();
|
|
3432
|
+
for (const ref of refs.filter(dropped)) {
|
|
3433
|
+
const key = `${ref.line}:${ref.module}`;
|
|
3434
|
+
typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
|
|
3435
|
+
}
|
|
2414
3436
|
for (const specifier of specifiers) {
|
|
2415
|
-
if (
|
|
2416
|
-
const
|
|
2417
|
-
specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
|
|
3437
|
+
if (path7.isAbsolute(specifier) || specifier.startsWith("file:")) {
|
|
3438
|
+
const resolved2 = resolveFiles(
|
|
3439
|
+
withoutQuery(specifier.startsWith("file:") ? new URL(specifier).pathname : specifier)
|
|
2418
3440
|
);
|
|
2419
|
-
if (
|
|
2420
|
-
|
|
3441
|
+
if (resolved2.length === 0) {
|
|
3442
|
+
loading(`${shown(file)} imports ${specifier}, which leads to no file`);
|
|
2421
3443
|
} else {
|
|
2422
|
-
found.loads.push(realpath
|
|
3444
|
+
found.loads.push(...resolved2.map(realpath));
|
|
2423
3445
|
}
|
|
2424
3446
|
} else if (isProjectImport(specifier, aliases)) {
|
|
2425
|
-
const
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
3447
|
+
const resolved2 = target(withoutQuery(specifier), file);
|
|
3448
|
+
if (resolved2.length === 0) {
|
|
3449
|
+
loading(`${shown(file)} imports ${specifier}, which leads to no file`);
|
|
2428
3450
|
} else {
|
|
2429
|
-
found.loads.push(realpath
|
|
3451
|
+
found.loads.push(...resolved2.map(realpath));
|
|
2430
3452
|
}
|
|
2431
3453
|
} else if (FILE_MODULES.has(bare(specifier))) {
|
|
2432
|
-
|
|
3454
|
+
const reason = `${shown(file)} imports ${specifier}, which can reach files that no import names`;
|
|
3455
|
+
if (LOADING_MODULES.has(bare(specifier))) {
|
|
3456
|
+
loading(reason);
|
|
3457
|
+
} else {
|
|
3458
|
+
found.unknown ??= reason;
|
|
3459
|
+
}
|
|
2433
3460
|
} else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
|
|
2434
3461
|
if (!installed(specifier, file)) {
|
|
2435
|
-
|
|
3462
|
+
loading(
|
|
3463
|
+
`${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`
|
|
3464
|
+
);
|
|
2436
3465
|
found.unnamed.push(specifier);
|
|
2437
3466
|
} else if (linkedPackage(specifier, file) !== null) {
|
|
2438
|
-
|
|
3467
|
+
loading(
|
|
3468
|
+
`${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`
|
|
3469
|
+
);
|
|
2439
3470
|
found.computed = true;
|
|
2440
3471
|
}
|
|
2441
3472
|
}
|
|
2442
3473
|
}
|
|
3474
|
+
if (location !== null) {
|
|
3475
|
+
const reads = LOCATION_READS.exec(code);
|
|
3476
|
+
const reader = [...specifiers].find(
|
|
3477
|
+
(specifier) => !specifier.startsWith(".") && !path7.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
|
|
3478
|
+
);
|
|
3479
|
+
if (reads !== null) {
|
|
3480
|
+
found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
|
|
3481
|
+
const loads = LOCATION_LOADS.exec(code);
|
|
3482
|
+
if (loads !== null) {
|
|
3483
|
+
found.loading ??= `${shown(file)} uses ${location[0]} and ${loads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
|
|
3484
|
+
}
|
|
3485
|
+
} else if (reader !== void 0) {
|
|
3486
|
+
found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
let line = 1;
|
|
3490
|
+
let counted = 0;
|
|
2443
3491
|
for (const match of source.matchAll(QUOTED)) {
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
if (resolved !== null) {
|
|
2448
|
-
found.loads.push(realpath(resolved));
|
|
3492
|
+
for (; counted < match.index; counted += 1) {
|
|
3493
|
+
if (source[counted] === "\n") {
|
|
3494
|
+
line += 1;
|
|
2449
3495
|
}
|
|
2450
3496
|
}
|
|
3497
|
+
const text = match[2];
|
|
3498
|
+
const key = `${line}:${text}`;
|
|
3499
|
+
const left = typeLines.get(key) ?? 0;
|
|
3500
|
+
if (left > 0) {
|
|
3501
|
+
typeLines.set(key, left - 1);
|
|
3502
|
+
} else if (text.startsWith(".") && !specifiers.has(text)) {
|
|
3503
|
+
found.loads.push(...resolveFiles(path7.resolve(path7.dirname(file), text)).map(realpath));
|
|
3504
|
+
} else if (PATH_FROM_TOP.test(text) && isFile2(path7.resolve(dir, text))) {
|
|
3505
|
+
found.loads.push(realpath(path7.resolve(dir, text)));
|
|
3506
|
+
}
|
|
2451
3507
|
}
|
|
2452
3508
|
return found;
|
|
2453
3509
|
}
|
|
2454
3510
|
function affectedInputs(manifest, input) {
|
|
3511
|
+
return resolving(() => inputsOf(manifest, input));
|
|
3512
|
+
}
|
|
3513
|
+
function inputsOf(manifest, input) {
|
|
2455
3514
|
const dir = realpath(manifest.dir);
|
|
2456
3515
|
const aliases = manifest.settings.importAliases;
|
|
2457
3516
|
const placed = [...input.tests, ...Object.values(input.covering).flat()];
|
|
2458
|
-
const absolute = placed.find((file) =>
|
|
3517
|
+
const absolute = placed.find((file) => path7.isAbsolute(file) || file.startsWith(".."));
|
|
2459
3518
|
if (absolute !== void 0) {
|
|
2460
3519
|
throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
|
|
2461
3520
|
}
|
|
2462
3521
|
const keyOf2 = (file) => {
|
|
2463
|
-
const relative =
|
|
2464
|
-
if (relative.startsWith("..") ||
|
|
3522
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3523
|
+
if (relative.startsWith("..") || path7.isAbsolute(relative)) {
|
|
2465
3524
|
throw new AffectedError(`${file} is outside of the files that are compared`);
|
|
2466
3525
|
}
|
|
2467
3526
|
return toPosix2(relative);
|
|
2468
3527
|
};
|
|
2469
|
-
const shown = (file) => toPosix2(
|
|
3528
|
+
const shown = (file) => toPosix2(path7.relative(dir, file));
|
|
3529
|
+
const plain2 = /* @__PURE__ */ new Map();
|
|
3530
|
+
const configTexts = [];
|
|
3531
|
+
const runnerSources = [];
|
|
3532
|
+
const reached = new Set(input.configs.files);
|
|
3533
|
+
const pending = [...input.configs.files];
|
|
3534
|
+
for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
|
|
3535
|
+
const text = fs7.readFileSync(file, "utf8");
|
|
3536
|
+
configTexts.push(text);
|
|
3537
|
+
runnerSources.push({ file, text });
|
|
3538
|
+
for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
|
|
3539
|
+
if (!reached.has(next) && SCRIPT.test(next)) {
|
|
3540
|
+
reached.add(next);
|
|
3541
|
+
pending.push(next);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
const compiler = compilerConfigs(dir, realpath(input.root), Object.keys(input.files));
|
|
3546
|
+
const types = typesErased(input.configs, configTexts, compiler);
|
|
3547
|
+
const maps = unexplainedMaps({
|
|
3548
|
+
dir,
|
|
3549
|
+
aliases,
|
|
3550
|
+
// Not what lies outside of the compared files: this package, imported by path, is installed.
|
|
3551
|
+
runner: [
|
|
3552
|
+
...runnerSources.filter(({ file }) => {
|
|
3553
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3554
|
+
return !relative.startsWith("..") && toPosix2(relative) in input.files;
|
|
3555
|
+
}),
|
|
3556
|
+
...jestInPackage(dir, input.configs)
|
|
3557
|
+
],
|
|
3558
|
+
compiler,
|
|
3559
|
+
...input.configs.runner === void 0 ? {} : { testRunner: input.configs.runner }
|
|
3560
|
+
});
|
|
3561
|
+
if (maps !== null) {
|
|
3562
|
+
throw new AffectedError(maps);
|
|
3563
|
+
}
|
|
2470
3564
|
const edges = /* @__PURE__ */ new Map();
|
|
2471
|
-
const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
|
|
3565
|
+
const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
|
|
3566
|
+
const blind = /* @__PURE__ */ Object.create(null);
|
|
2472
3567
|
const closure = (start, strict) => {
|
|
2473
3568
|
const seen = /* @__PURE__ */ new Set([start]);
|
|
2474
3569
|
const queue = [start];
|
|
2475
3570
|
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2476
3571
|
const found = edgesOf(file);
|
|
2477
|
-
if (strict && found.unknown !== null) {
|
|
3572
|
+
if (strict === "every" && found.unknown !== null) {
|
|
2478
3573
|
throw new AffectedError(found.unknown);
|
|
2479
3574
|
}
|
|
3575
|
+
if (strict === "gate" && found.unknown !== null) {
|
|
3576
|
+
if (found.loading !== null) {
|
|
3577
|
+
throw new AffectedError(found.loading);
|
|
3578
|
+
}
|
|
3579
|
+
blind[keyOf2(file)] = found.unknown;
|
|
3580
|
+
}
|
|
2480
3581
|
for (const next of found.loads) {
|
|
2481
3582
|
if (!seen.has(next)) {
|
|
2482
3583
|
seen.add(next);
|
|
@@ -2487,8 +3588,8 @@ function affectedInputs(manifest, input) {
|
|
|
2487
3588
|
return [...seen];
|
|
2488
3589
|
};
|
|
2489
3590
|
const isCompared = (file) => {
|
|
2490
|
-
const relative =
|
|
2491
|
-
return !relative.startsWith("..") && !
|
|
3591
|
+
const relative = path7.relative(input.root, realpath(file));
|
|
3592
|
+
return !relative.startsWith("..") && !path7.isAbsolute(relative) && toPosix2(relative) in input.files;
|
|
2492
3593
|
};
|
|
2493
3594
|
const keysOf = (files) => files.map((file) => {
|
|
2494
3595
|
const key = keyOf2(file);
|
|
@@ -2507,17 +3608,25 @@ function affectedInputs(manifest, input) {
|
|
|
2507
3608
|
if (!SCRIPT.test(loaded)) {
|
|
2508
3609
|
continue;
|
|
2509
3610
|
}
|
|
2510
|
-
for (const match of
|
|
3611
|
+
for (const match of fs7.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
|
|
2511
3612
|
const text = match[2].replace(/^<rootDir>\//, "");
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
3613
|
+
const here = resolveFiles(path7.resolve(path7.dirname(loaded), text), false, false);
|
|
3614
|
+
const candidates = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false, false);
|
|
3615
|
+
for (const found of candidates) {
|
|
3616
|
+
if (text.includes("*") || !isCompared(found)) {
|
|
3617
|
+
continue;
|
|
3618
|
+
}
|
|
3619
|
+
if (SCRIPT.test(found)) {
|
|
3620
|
+
named.push(realpath(found));
|
|
3621
|
+
} else {
|
|
3622
|
+
config.add(realpath(found));
|
|
3623
|
+
}
|
|
2515
3624
|
}
|
|
2516
3625
|
}
|
|
2517
3626
|
}
|
|
2518
3627
|
}
|
|
2519
3628
|
const gatePaths = new Map(
|
|
2520
|
-
manifest.gates.map((gate) => [realpath(
|
|
3629
|
+
manifest.gates.map((gate) => [realpath(path7.join(dir, gate.path)), gate.path])
|
|
2521
3630
|
);
|
|
2522
3631
|
const tests = [
|
|
2523
3632
|
.../* @__PURE__ */ new Set([
|
|
@@ -2528,13 +3637,13 @@ function affectedInputs(manifest, input) {
|
|
|
2528
3637
|
];
|
|
2529
3638
|
const runAsSuch = new Set(
|
|
2530
3639
|
[...tests, ...manifest.gates.map((gate) => gate.path)].map(
|
|
2531
|
-
(file) => realpath(
|
|
3640
|
+
(file) => realpath(path7.join(dir, file))
|
|
2532
3641
|
)
|
|
2533
3642
|
);
|
|
2534
|
-
const subprojectKey = toPosix2(
|
|
3643
|
+
const subprojectKey = toPosix2(path7.relative(input.root, dir));
|
|
2535
3644
|
const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
|
|
2536
3645
|
for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
|
|
2537
|
-
for (const loaded of closure(file,
|
|
3646
|
+
for (const loaded of closure(file, "every")) {
|
|
2538
3647
|
const gate = gatePaths.get(loaded);
|
|
2539
3648
|
if (gate !== void 0) {
|
|
2540
3649
|
throw new AffectedError(
|
|
@@ -2544,16 +3653,16 @@ function affectedInputs(manifest, input) {
|
|
|
2544
3653
|
config.add(loaded);
|
|
2545
3654
|
}
|
|
2546
3655
|
}
|
|
2547
|
-
const fromSubproject = (file) =>
|
|
3656
|
+
const fromSubproject = (file) => path7.join(dir, file);
|
|
2548
3657
|
const closures = /* @__PURE__ */ Object.create(null);
|
|
2549
3658
|
for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
|
|
2550
3659
|
const absolute2 = fromSubproject(file);
|
|
2551
|
-
if (!
|
|
3660
|
+
if (!fs7.existsSync(absolute2)) {
|
|
2552
3661
|
continue;
|
|
2553
3662
|
}
|
|
2554
|
-
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2),
|
|
3663
|
+
closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), "gate"));
|
|
2555
3664
|
}
|
|
2556
|
-
const keyed = (files) => files.map((file) => toPosix2(
|
|
3665
|
+
const keyed = (files) => files.map((file) => toPosix2(path7.relative(input.root, path7.join(dir, file))));
|
|
2557
3666
|
const dependencies = gateDependencies({
|
|
2558
3667
|
gates: manifest.gates.map((gate) => ({
|
|
2559
3668
|
key: keyed([gate.path])[0],
|
|
@@ -2563,9 +3672,10 @@ function affectedInputs(manifest, input) {
|
|
|
2563
3672
|
covering: Object.fromEntries(
|
|
2564
3673
|
Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
|
|
2565
3674
|
),
|
|
2566
|
-
closures
|
|
3675
|
+
closures,
|
|
3676
|
+
blind
|
|
2567
3677
|
});
|
|
2568
|
-
const subproject = toPosix2(
|
|
3678
|
+
const subproject = toPosix2(path7.relative(input.root, dir));
|
|
2569
3679
|
const prefix = subproject === "" ? "" : `${subproject}/`;
|
|
2570
3680
|
const gates = /* @__PURE__ */ Object.create(null);
|
|
2571
3681
|
for (const gate of manifest.gates) {
|
|
@@ -2576,10 +3686,26 @@ function affectedInputs(manifest, input) {
|
|
|
2576
3686
|
specs: found.specs.map(
|
|
2577
3687
|
(spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
|
|
2578
3688
|
),
|
|
2579
|
-
files: found.files
|
|
3689
|
+
files: found.files,
|
|
3690
|
+
...found.blind === void 0 ? {} : { blind: found.blind }
|
|
2580
3691
|
};
|
|
2581
3692
|
}
|
|
2582
|
-
|
|
3693
|
+
const packageMocks = Object.keys(input.files).filter(
|
|
3694
|
+
(key) => isNestedMock(key, subproject) && mockNames(key).some(
|
|
3695
|
+
(name) => builtinModules2.includes(name) || installed(name, path7.join(input.root, key))
|
|
3696
|
+
)
|
|
3697
|
+
);
|
|
3698
|
+
return {
|
|
3699
|
+
subproject,
|
|
3700
|
+
config: [.../* @__PURE__ */ new Set([...keysOf([...config]), ...packageMocks])].sort(),
|
|
3701
|
+
gates
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
function mockNames(key) {
|
|
3705
|
+
const segments = key.replace(/\.[^./]+$/, "").split("/");
|
|
3706
|
+
return segments.flatMap(
|
|
3707
|
+
(segment, index) => segment === "__mocks__" && index < segments.length - 1 ? [segments.slice(index + 1).join("/")] : []
|
|
3708
|
+
);
|
|
2583
3709
|
}
|
|
2584
3710
|
var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
|
|
2585
3711
|
var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
|
|
@@ -2615,11 +3741,14 @@ var READ_BY_NAME = [
|
|
|
2615
3741
|
/^\.swcrc$/
|
|
2616
3742
|
];
|
|
2617
3743
|
function loadedScope(manifest, configs, listed2) {
|
|
3744
|
+
return resolving(() => scopeOf(manifest, configs, listed2));
|
|
3745
|
+
}
|
|
3746
|
+
function scopeOf(manifest, configs, listed2) {
|
|
2618
3747
|
const dir = realpath(manifest.dir);
|
|
2619
3748
|
const cache = /* @__PURE__ */ new Map();
|
|
2620
3749
|
const context = { dir, aliases: manifest.settings.importAliases, cache };
|
|
2621
3750
|
const runner = configs.files.slice(1);
|
|
2622
|
-
const texts = runner.map((file) => withoutComments(
|
|
3751
|
+
const texts = runner.map((file) => withoutComments(fs7.readFileSync(file, "utf8")));
|
|
2623
3752
|
if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
|
|
2624
3753
|
return { every: NOT_THE_HELPER2 };
|
|
2625
3754
|
}
|
|
@@ -2634,21 +3763,21 @@ function loadedScope(manifest, configs, listed2) {
|
|
|
2634
3763
|
fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
|
|
2635
3764
|
}
|
|
2636
3765
|
}
|
|
2637
|
-
if ([...parts].some((file) => PROJECTS.test(withoutComments(
|
|
3766
|
+
if ([...parts].some((file) => PROJECTS.test(withoutComments(fs7.readFileSync(file, "utf8"))))) {
|
|
2638
3767
|
return {
|
|
2639
3768
|
every: "the runner config, or a file it is put together from, names projects or a workspace"
|
|
2640
3769
|
};
|
|
2641
3770
|
}
|
|
2642
|
-
const named =
|
|
2643
|
-
const mocks =
|
|
3771
|
+
const named = fs7.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path7.join(dir, name));
|
|
3772
|
+
const mocks = path7.join(dir, "__mocks__");
|
|
2644
3773
|
const seeds = [
|
|
2645
3774
|
...configs.files,
|
|
2646
3775
|
...named,
|
|
2647
|
-
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) =>
|
|
2648
|
-
...listed2.filter((file) => file.startsWith(`${mocks}${
|
|
2649
|
-
].filter((file) =>
|
|
3776
|
+
...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path7.join(dir, file)),
|
|
3777
|
+
...listed2.filter((file) => file.startsWith(`${mocks}${path7.sep}`))
|
|
3778
|
+
].filter((file) => fs7.existsSync(file)).map(realpath);
|
|
2650
3779
|
const naming = new Set(
|
|
2651
|
-
[...configs.files, ...named].filter((file) =>
|
|
3780
|
+
[...configs.files, ...named].filter((file) => fs7.existsSync(file)).map(realpath)
|
|
2652
3781
|
);
|
|
2653
3782
|
const seen = new Set(seeds);
|
|
2654
3783
|
const queue = [...seeds];
|
|
@@ -2660,7 +3789,7 @@ function loadedScope(manifest, configs, listed2) {
|
|
|
2660
3789
|
}
|
|
2661
3790
|
};
|
|
2662
3791
|
for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
|
|
2663
|
-
if (
|
|
3792
|
+
if (fs7.statSync(file).isDirectory()) {
|
|
2664
3793
|
continue;
|
|
2665
3794
|
}
|
|
2666
3795
|
const edges = fileEdges(file, context);
|
|
@@ -2671,19 +3800,16 @@ function loadedScope(manifest, configs, listed2) {
|
|
|
2671
3800
|
for (const specifier of edges.unnamed) {
|
|
2672
3801
|
endingAs(specifier, listed2).forEach(add);
|
|
2673
3802
|
}
|
|
2674
|
-
|
|
2675
|
-
if (fs6.existsSync(mock)) {
|
|
2676
|
-
add(mock);
|
|
2677
|
-
}
|
|
2678
|
-
if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
|
|
3803
|
+
if (naming.has(file) || SCRIPT.test(file) && path7.dirname(file) === dir) {
|
|
2679
3804
|
for (const named2 of namedIn(file)) {
|
|
2680
3805
|
const text = named2.replace(/^<rootDir>\//, "");
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3806
|
+
const here = resolveFiles(path7.resolve(path7.dirname(file), text), false);
|
|
3807
|
+
const found = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false);
|
|
3808
|
+
for (const one of text.includes("*") ? [] : found) {
|
|
3809
|
+
if (!SCRIPT.test(one)) {
|
|
3810
|
+
naming.add(realpath(one));
|
|
2685
3811
|
}
|
|
2686
|
-
add(
|
|
3812
|
+
add(one);
|
|
2687
3813
|
}
|
|
2688
3814
|
}
|
|
2689
3815
|
}
|
|
@@ -2702,7 +3828,7 @@ function endingAs(specifier, files) {
|
|
|
2702
3828
|
});
|
|
2703
3829
|
}
|
|
2704
3830
|
function namedIn(file) {
|
|
2705
|
-
const text =
|
|
3831
|
+
const text = fs7.readFileSync(file, "utf8");
|
|
2706
3832
|
if (SCRIPT.test(file)) {
|
|
2707
3833
|
return [...text.matchAll(QUOTED)].map((match) => match[2]);
|
|
2708
3834
|
}
|
|
@@ -2740,298 +3866,475 @@ function valuesOf(text, key) {
|
|
|
2740
3866
|
return found;
|
|
2741
3867
|
}
|
|
2742
3868
|
function withoutComments(text) {
|
|
2743
|
-
|
|
3869
|
+
const out = [];
|
|
3870
|
+
const before = { last: "", word: "" };
|
|
3871
|
+
let space = false;
|
|
3872
|
+
const keep = (part) => {
|
|
3873
|
+
out.push(part);
|
|
3874
|
+
for (const one of part) {
|
|
3875
|
+
if (/\s/.test(one)) {
|
|
3876
|
+
space = true;
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
const word = /[\w$]/.test(one);
|
|
3880
|
+
before.word = !word ? "" : space || !/[\w$]/.test(before.last) ? one : before.word + one;
|
|
3881
|
+
before.last = one;
|
|
3882
|
+
space = false;
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
2744
3885
|
for (let at = 0; at < text.length; at += 1) {
|
|
2745
3886
|
const char = text[at];
|
|
3887
|
+
const next = text[at + 1];
|
|
2746
3888
|
if (char === "'" || char === '"' || char === "`") {
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
end += text[end] === "\\" ? 2 : 1;
|
|
2750
|
-
}
|
|
2751
|
-
out += text.slice(at, end + 1);
|
|
3889
|
+
const end = stringEnd(text, at);
|
|
3890
|
+
keep(text.slice(at, end + 1));
|
|
2752
3891
|
at = end;
|
|
2753
|
-
} else if (char === "/" &&
|
|
3892
|
+
} else if (char === "/" && next === "/" && !SCHEME.test(text.slice(Math.max(0, at - 40), at))) {
|
|
2754
3893
|
const end = text.indexOf("\n", at);
|
|
2755
3894
|
at = end === -1 ? text.length : end - 1;
|
|
2756
|
-
} else if (char === "/" &&
|
|
3895
|
+
} else if (char === "/" && next === "*") {
|
|
2757
3896
|
const end = text.indexOf("*/", at + 2);
|
|
2758
|
-
|
|
3897
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
3898
|
+
keep(` ${text.slice(at, stop).replace(/[^\n]/g, "")}`);
|
|
3899
|
+
at = stop - 1;
|
|
3900
|
+
} else if (char === "/" && next !== "/") {
|
|
3901
|
+
const end = patternEnd(text, at, mayStartPattern(before));
|
|
3902
|
+
keep(text.slice(at, end + 1));
|
|
3903
|
+
at = end;
|
|
3904
|
+
} else {
|
|
3905
|
+
keep(char);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return out.join("");
|
|
3909
|
+
}
|
|
3910
|
+
var SCHEME = /(?:^|[^\w$.+-])[A-Za-z][\w+.-]*:$/;
|
|
3911
|
+
function patternEnd(text, at, pattern) {
|
|
3912
|
+
let inClass = false;
|
|
3913
|
+
let end = at + 1;
|
|
3914
|
+
for (; end < text.length && text[end] !== "\n"; end += 1) {
|
|
3915
|
+
const char = text[end];
|
|
3916
|
+
if (char === "\\") {
|
|
3917
|
+
end += 1;
|
|
3918
|
+
} else if (char === "[") {
|
|
3919
|
+
inClass = true;
|
|
3920
|
+
} else if (char === "]") {
|
|
3921
|
+
inClass = false;
|
|
3922
|
+
} else if (char === "/" && !inClass) {
|
|
3923
|
+
const comment = text[end + 1] === "/" || text[end + 1] === "*";
|
|
3924
|
+
return pattern || !comment ? end : at;
|
|
3925
|
+
} else if (!pattern && (char === "'" || char === '"' || char === "`")) {
|
|
3926
|
+
return at;
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
return pattern ? end : at;
|
|
3930
|
+
}
|
|
3931
|
+
function stringEnd(text, at) {
|
|
3932
|
+
const quote = text[at];
|
|
3933
|
+
let end = at + 1;
|
|
3934
|
+
while (end < text.length && text[end] !== quote && (quote === "`" || text[end] !== "\n")) {
|
|
3935
|
+
if (text[end] === "\\") {
|
|
3936
|
+
end += 2;
|
|
3937
|
+
} else if (quote === "`" && text.startsWith("${", end)) {
|
|
3938
|
+
end = placeholderEnd(text, end + 2) + 1;
|
|
2759
3939
|
} else {
|
|
2760
|
-
|
|
3940
|
+
end += 1;
|
|
2761
3941
|
}
|
|
2762
3942
|
}
|
|
2763
|
-
return
|
|
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";
|
|
2775
|
-
|
|
2776
|
-
// src/core/mutation-result.ts
|
|
2777
|
-
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
2778
|
-
var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
|
|
2779
|
-
var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
|
|
2780
|
-
function normalizeCode(text) {
|
|
2781
|
-
return text.replace(/\s+/g, " ").trim();
|
|
2782
|
-
}
|
|
2783
|
-
function normalizePath(file) {
|
|
2784
|
-
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3943
|
+
return end;
|
|
2785
3944
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
3945
|
+
function placeholderEnd(text, at) {
|
|
3946
|
+
let depth = 0;
|
|
3947
|
+
for (let index = at; index < text.length; index += 1) {
|
|
3948
|
+
const char = text[index];
|
|
3949
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
3950
|
+
index = stringEnd(text, index);
|
|
3951
|
+
} else if (char === "{") {
|
|
3952
|
+
depth += 1;
|
|
3953
|
+
} else if (char === "}") {
|
|
3954
|
+
if (depth === 0) {
|
|
3955
|
+
return index;
|
|
3956
|
+
}
|
|
3957
|
+
depth -= 1;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
return text.length;
|
|
3961
|
+
}
|
|
3962
|
+
var BEFORE_PATTERN = /* @__PURE__ */ new Set([
|
|
3963
|
+
"return",
|
|
3964
|
+
"typeof",
|
|
3965
|
+
"instanceof",
|
|
3966
|
+
"in",
|
|
3967
|
+
"of",
|
|
3968
|
+
"new",
|
|
3969
|
+
"delete",
|
|
3970
|
+
"void",
|
|
3971
|
+
"throw",
|
|
3972
|
+
"case",
|
|
3973
|
+
"do",
|
|
3974
|
+
"else",
|
|
3975
|
+
"yield",
|
|
3976
|
+
"await"
|
|
3977
|
+
]);
|
|
3978
|
+
function mayStartPattern(before) {
|
|
3979
|
+
if (before.word !== "") {
|
|
3980
|
+
return BEFORE_PATTERN.has(before.word);
|
|
3981
|
+
}
|
|
3982
|
+
return before.last === "" || !")]".includes(before.last);
|
|
3983
|
+
}
|
|
3984
|
+
function jestInPackage(dir, configs) {
|
|
3985
|
+
const file = path7.join(dir, "package.json");
|
|
3986
|
+
return configs.runner === "jest" && configs.files.length === 1 && fs7.existsSync(file) ? [{ file, text: fs7.readFileSync(file, "utf8"), mapperOnly: true }] : [];
|
|
3987
|
+
}
|
|
3988
|
+
var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
|
|
3989
|
+
function stringValue(text) {
|
|
3990
|
+
const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
|
|
3991
|
+
return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
|
|
3992
|
+
}
|
|
3993
|
+
function partsOf2(text, at) {
|
|
3994
|
+
const parts = [];
|
|
3995
|
+
let depth = 0;
|
|
3996
|
+
let start = at + 1;
|
|
3997
|
+
for (let index = at; index < text.length; index += 1) {
|
|
3998
|
+
const char = text[index];
|
|
3999
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
4000
|
+
let end = index + 1;
|
|
4001
|
+
while (end < text.length && text[end] !== char) {
|
|
4002
|
+
end += text[end] === "\\" ? 2 : 1;
|
|
4003
|
+
}
|
|
4004
|
+
index = end;
|
|
4005
|
+
} else if ("{[(".includes(char)) {
|
|
4006
|
+
depth += 1;
|
|
4007
|
+
} else if ("}])".includes(char)) {
|
|
4008
|
+
depth -= 1;
|
|
4009
|
+
if (depth === 0) {
|
|
4010
|
+
parts.push(text.slice(start, index));
|
|
4011
|
+
return { parts: parts.filter((part) => part.trim() !== ""), end: index };
|
|
4012
|
+
}
|
|
4013
|
+
} else if (char === "," && depth === 1) {
|
|
4014
|
+
parts.push(text.slice(start, index));
|
|
4015
|
+
start = index + 1;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
return null;
|
|
2788
4019
|
}
|
|
2789
|
-
function
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
const first = lines[start.line - 1];
|
|
2793
|
-
const last = lines[end.line - 1];
|
|
2794
|
-
if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
|
|
4020
|
+
function property(part) {
|
|
4021
|
+
const match = new RegExp(`^\\s*(${STRING_LITERAL}|[\\w$]+)\\s*:([\\s\\S]*)$`).exec(part);
|
|
4022
|
+
if (match === null) {
|
|
2795
4023
|
return null;
|
|
2796
4024
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
4025
|
+
const key = match[1];
|
|
4026
|
+
return { key: stringValue(key) ?? key, value: match[2].trim() };
|
|
4027
|
+
}
|
|
4028
|
+
function aliasPath(expression, file, text, depth = 0) {
|
|
4029
|
+
const value = expression.trim();
|
|
4030
|
+
const here = path7.dirname(file);
|
|
4031
|
+
if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
|
|
4032
|
+
return here;
|
|
4033
|
+
}
|
|
4034
|
+
const url = new RegExp(
|
|
4035
|
+
`^(?:(?:url\\.)?fileURLToPath\\(\\s*new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\)|new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\.pathname)$`
|
|
4036
|
+
).exec(value);
|
|
4037
|
+
if (url !== null) {
|
|
4038
|
+
return path7.resolve(here, stringValue(url[1] ?? url[2]));
|
|
4039
|
+
}
|
|
4040
|
+
const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
|
|
4041
|
+
if (call !== null) {
|
|
4042
|
+
const found = partsOf2(value, call[0].length - 1);
|
|
4043
|
+
if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
const [first, ...rest] = found.parts;
|
|
4047
|
+
const base = aliasPath(first, file, text, depth + 1);
|
|
4048
|
+
const parts = rest.map(stringValue);
|
|
4049
|
+
return base === null || parts.some((part) => part === null) ? null : path7.resolve(base, ...parts);
|
|
2799
4050
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
4051
|
+
if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
|
|
4052
|
+
const declarations = text.match(
|
|
4053
|
+
new RegExp(`\\b(?:const|let|var|function|class)\\s+${value.replace(/\$/g, "\\$")}\\b`, "g")
|
|
4054
|
+
);
|
|
4055
|
+
const bound = new RegExp(`\\bconst\\s+${value.replace(/\$/g, "\\$")}\\s*=\\s*([^;\\n]+)`).exec(
|
|
4056
|
+
text
|
|
4057
|
+
);
|
|
4058
|
+
return declarations?.length === 1 && bound !== null ? aliasPath(bound[1], file, text, depth + 1) : null;
|
|
4059
|
+
}
|
|
4060
|
+
return null;
|
|
2805
4061
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
4062
|
+
var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
|
|
4063
|
+
function isDirectory(file) {
|
|
4064
|
+
try {
|
|
4065
|
+
return fs7.statSync(file).isDirectory();
|
|
4066
|
+
} catch {
|
|
4067
|
+
return false;
|
|
4068
|
+
}
|
|
2808
4069
|
}
|
|
2809
|
-
function
|
|
2810
|
-
const
|
|
2811
|
-
return {
|
|
2812
|
-
file,
|
|
2813
|
-
line: mutant.location.start.line,
|
|
2814
|
-
column: mutant.location.start.column,
|
|
2815
|
-
status: mutant.status,
|
|
2816
|
-
mutator: mutant.mutatorName,
|
|
2817
|
-
original: normalizeCode(entry.original),
|
|
2818
|
-
replacement: normalizeCode(entry.replacement),
|
|
2819
|
-
occurrence: entry.occurrence,
|
|
2820
|
-
sameKeyCount: entry.sameKeyCount,
|
|
2821
|
-
allowance: {
|
|
2822
|
-
mutator: mutant.mutatorName,
|
|
2823
|
-
original: normalizeCode(entry.original),
|
|
2824
|
-
replacement: normalizeCode(entry.replacement),
|
|
2825
|
-
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
2826
|
-
reason: ""
|
|
2827
|
-
}
|
|
2828
|
-
};
|
|
4070
|
+
function regexPrefix(literal) {
|
|
4071
|
+
const match = /^\/\^((?:[\w@~#%-]|\\[./@~#-])*)\\\/\/$/.exec(literal.trim());
|
|
4072
|
+
return match === null ? null : `${match[1].replace(/\\(.)/g, "$1")}/`;
|
|
2829
4073
|
}
|
|
2830
|
-
function
|
|
2831
|
-
const
|
|
2832
|
-
return
|
|
4074
|
+
function mapperPrefix(pattern) {
|
|
4075
|
+
const match = /^\^((?:[\w@~#%/-]|\\[./@~#-])*)\(\.\*\)\$?$/.exec(pattern);
|
|
4076
|
+
return match === null ? null : match[1].replace(/\\(.)/g, "$1");
|
|
2833
4077
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
const
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
2854
|
-
violations.push(...shapeViolations);
|
|
2855
|
-
violations.push(
|
|
2856
|
-
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
2857
|
-
);
|
|
2858
|
-
const result = files.get(gatePath);
|
|
2859
|
-
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
2860
|
-
violations.push({
|
|
2861
|
-
file: gate.path,
|
|
2862
|
-
message: "no mutants in the report (the gate was not mutation tested)"
|
|
2863
|
-
});
|
|
2864
|
-
continue;
|
|
2865
|
-
}
|
|
2866
|
-
const diskSource = input.readSource(gate.path);
|
|
2867
|
-
const source = result.source ?? diskSource;
|
|
2868
|
-
if (source === void 0) {
|
|
2869
|
-
violations.push({
|
|
2870
|
-
file: gate.path,
|
|
2871
|
-
message: "source is neither in the report nor readable on disk"
|
|
2872
|
-
});
|
|
2873
|
-
continue;
|
|
2874
|
-
}
|
|
2875
|
-
if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
|
|
2876
|
-
violations.push({
|
|
2877
|
-
file: gate.path,
|
|
2878
|
-
message: "the report was made from a different version of this file. Run Stryker again"
|
|
2879
|
-
});
|
|
2880
|
-
continue;
|
|
2881
|
-
}
|
|
2882
|
-
const located = [];
|
|
2883
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2884
|
-
const sorted2 = [...result.mutants].sort(
|
|
2885
|
-
(a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
|
|
2886
|
-
);
|
|
2887
|
-
let locationsValid = true;
|
|
2888
|
-
for (const mutant of sorted2) {
|
|
2889
|
-
const original = sliceByLocation(normalizeNewlines(source), mutant.location);
|
|
2890
|
-
if (original === null) {
|
|
2891
|
-
violations.push({
|
|
2892
|
-
file: gate.path,
|
|
2893
|
-
line: mutant.location.start.line,
|
|
2894
|
-
message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
|
|
2895
|
-
});
|
|
2896
|
-
locationsValid = false;
|
|
2897
|
-
continue;
|
|
2898
|
-
}
|
|
2899
|
-
const replacement = mutant.replacement ?? "";
|
|
2900
|
-
const key = keyOf(mutant.mutatorName, original, replacement);
|
|
2901
|
-
const group = byKey.get(key) ?? [];
|
|
2902
|
-
const entry = {
|
|
2903
|
-
mutant,
|
|
2904
|
-
original,
|
|
2905
|
-
replacement,
|
|
2906
|
-
occurrence: group.length + 1,
|
|
2907
|
-
sameKeyCount: 0
|
|
2908
|
-
};
|
|
2909
|
-
group.push(entry);
|
|
2910
|
-
byKey.set(key, group);
|
|
2911
|
-
located.push(entry);
|
|
2912
|
-
}
|
|
2913
|
-
for (const group of byKey.values()) {
|
|
2914
|
-
for (const entry of group) {
|
|
2915
|
-
entry.sameKeyCount = group.length;
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
if (!locationsValid) {
|
|
2919
|
-
continue;
|
|
4078
|
+
var ROOT_DIR_HERE = /* @__PURE__ */ new Set([
|
|
4079
|
+
"__dirname",
|
|
4080
|
+
"import.meta.dirname",
|
|
4081
|
+
"import.meta.url",
|
|
4082
|
+
"'.'",
|
|
4083
|
+
'"."',
|
|
4084
|
+
"'./'",
|
|
4085
|
+
'"./"'
|
|
4086
|
+
]);
|
|
4087
|
+
function unexplainedMaps(input) {
|
|
4088
|
+
const { dir, aliases } = input;
|
|
4089
|
+
const reasons = [];
|
|
4090
|
+
const mapped = [];
|
|
4091
|
+
const shown = (file) => toPosix2(path7.relative(dir, file));
|
|
4092
|
+
const under = (base, rest) => rest === "" ? `${base}/` : path7.join(base, rest);
|
|
4093
|
+
const named = (prefix, target) => {
|
|
4094
|
+
const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
|
|
4095
|
+
if (alias === void 0) {
|
|
4096
|
+
return false;
|
|
2920
4097
|
}
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
4098
|
+
const full = alias.target + prefix.slice(alias.prefix.length);
|
|
4099
|
+
const slash = full === "" || full.endsWith("/");
|
|
4100
|
+
return path7.resolve(dir, full) === path7.resolve(target) && slash === target.endsWith("/");
|
|
4101
|
+
};
|
|
4102
|
+
const advice = (prefix, target) => {
|
|
4103
|
+
const relative = toPosix2(path7.relative(dir, target));
|
|
4104
|
+
const shownTarget = relative === "" ? "./" : `${relative}/`;
|
|
4105
|
+
const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
|
|
4106
|
+
const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
|
|
4107
|
+
return ` (add { "prefix": "${prefix}", "target": "${shownTarget}" } to settings.importAliases${before})`;
|
|
4108
|
+
};
|
|
4109
|
+
const missing = (file, key, word, prefix, target) => `${shown(file)} maps ${key} with ${word} in a way settings.importAliases does not name` + (prefix !== void 0 && target !== void 0 && isDirectory(target) ? advice(prefix, target) : "");
|
|
4110
|
+
for (const { file, text: raw, mapperOnly } of input.runner) {
|
|
4111
|
+
let viteAlias2 = function(part, list) {
|
|
4112
|
+
let key = null;
|
|
4113
|
+
let prefix = null;
|
|
4114
|
+
let to = null;
|
|
4115
|
+
if (list) {
|
|
4116
|
+
const object = /^\s*\{/.test(part) ? partsOf2(part, part.indexOf("{")) : null;
|
|
4117
|
+
const fields = Object.fromEntries(
|
|
4118
|
+
(object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
|
|
4119
|
+
);
|
|
4120
|
+
if (object === null || Object.keys(fields).sort().join() !== "find,replacement" || part.slice(object.end + 1).trim() !== "") {
|
|
4121
|
+
return unreadableAlias2();
|
|
2938
4122
|
}
|
|
2939
|
-
const
|
|
2940
|
-
const
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
4123
|
+
const find = fields.find;
|
|
4124
|
+
const replacement = fields.replacement;
|
|
4125
|
+
key = stringValue(find);
|
|
4126
|
+
if (key !== null) {
|
|
4127
|
+
prefix = key.endsWith("/") ? null : `${key}/`;
|
|
4128
|
+
const target = aliasPath(replacement, file, text);
|
|
4129
|
+
to = target === null ? null : `${target}/`;
|
|
4130
|
+
} else {
|
|
4131
|
+
key = find.trim();
|
|
4132
|
+
prefix = regexPrefix(find);
|
|
4133
|
+
const slashed = new RegExp(`^([\\s\\S]+?)\\s*\\+\\s*(?:'/'|"/")$`).exec(replacement);
|
|
4134
|
+
const target = slashed === null ? null : aliasPath(slashed[1], file, text);
|
|
4135
|
+
to = target === null ? null : `${target}/`;
|
|
4136
|
+
if (prefix === null && to === null) {
|
|
4137
|
+
return unreadableAlias2();
|
|
4138
|
+
}
|
|
2944
4139
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
});
|
|
2950
|
-
return;
|
|
4140
|
+
} else {
|
|
4141
|
+
const entry = property(part);
|
|
4142
|
+
if (entry === null) {
|
|
4143
|
+
return unreadableAlias2();
|
|
2951
4144
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
4145
|
+
key = entry.key;
|
|
4146
|
+
prefix = key.endsWith("/") ? null : `${key}/`;
|
|
4147
|
+
const target = aliasPath(entry.value, file, text);
|
|
4148
|
+
to = target === null ? null : `${target}/`;
|
|
4149
|
+
}
|
|
4150
|
+
if (prefix === null || to === null || PACKAGE_NAME.test(key) && installed(key, file)) {
|
|
4151
|
+
return missing(file, key, "alias");
|
|
4152
|
+
}
|
|
4153
|
+
if (!named(prefix, to)) {
|
|
4154
|
+
return missing(file, key, "alias", prefix, to);
|
|
4155
|
+
}
|
|
4156
|
+
mapped.push(prefix);
|
|
4157
|
+
return null;
|
|
4158
|
+
}, unreadableAlias2 = function() {
|
|
4159
|
+
return `${shown(file)} names alias in a way that cannot be read as a list of aliases`;
|
|
2955
4160
|
};
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
"
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
const
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
summary.timeout += 1;
|
|
2975
|
-
if (expected.has(entry)) {
|
|
2976
|
-
summary.expectedTimeout += 1;
|
|
2977
|
-
} else {
|
|
2978
|
-
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
4161
|
+
var viteAlias = viteAlias2, unreadableAlias = unreadableAlias2;
|
|
4162
|
+
const text = withoutComments(raw);
|
|
4163
|
+
const words = mapperOnly === true ? /\bmoduleNameMapper\b/g : /\b(?:alias|moduleNameMapper)\b/g;
|
|
4164
|
+
for (const match of text.matchAll(words)) {
|
|
4165
|
+
const word = match[0];
|
|
4166
|
+
const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
|
|
4167
|
+
const at = opening === null ? -1 : match.index + word.length + opening[0].length;
|
|
4168
|
+
const found = at === -1 ? null : partsOf2(text, at);
|
|
4169
|
+
const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
|
|
4170
|
+
if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
|
|
4171
|
+
reasons.push(unreadable);
|
|
4172
|
+
continue;
|
|
4173
|
+
}
|
|
4174
|
+
for (const part of found.parts) {
|
|
4175
|
+
if (word === "alias") {
|
|
4176
|
+
const reason = viteAlias2(part, text[at] === "[");
|
|
4177
|
+
if (reason !== null) {
|
|
4178
|
+
reasons.push(reason);
|
|
2979
4179
|
}
|
|
4180
|
+
continue;
|
|
2980
4181
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
if (allowed.has(entry)) {
|
|
2985
|
-
summary.allowed += 1;
|
|
4182
|
+
const entry = property(part);
|
|
4183
|
+
if (entry === null) {
|
|
4184
|
+
reasons.push(unreadable);
|
|
2986
4185
|
continue;
|
|
2987
4186
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
4187
|
+
const value = stringValue(entry.value);
|
|
4188
|
+
const root = [...text.matchAll(/\brootDir\b['"]?\s*:\s*([^,}\n]+)/g)].map((one) => one[1].trim()).find((set) => !ROOT_DIR_HERE.has(set));
|
|
4189
|
+
if (root !== void 0 && (value === null || value.includes("<rootDir>"))) {
|
|
4190
|
+
reasons.push(`${shown(file)} sets rootDir to a directory that cannot be told: ${root}`);
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
if (value !== null && !value.includes("$")) {
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
const prefix = mapperPrefix(entry.key);
|
|
4197
|
+
const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
|
|
4198
|
+
const to = target === null ? null : under(path7.dirname(file), target[1]);
|
|
4199
|
+
if (prefix === null || to === null) {
|
|
4200
|
+
reasons.push(missing(file, entry.key, word));
|
|
4201
|
+
} else if (!named(prefix, to)) {
|
|
4202
|
+
reasons.push(missing(file, entry.key, word, prefix, to));
|
|
4203
|
+
} else {
|
|
4204
|
+
mapped.push(prefix);
|
|
4205
|
+
}
|
|
2996
4206
|
}
|
|
2997
4207
|
}
|
|
2998
4208
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
4209
|
+
const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path7.basename(file)));
|
|
4210
|
+
const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
|
|
4211
|
+
for (const { file, text } of input.compiler) {
|
|
4212
|
+
if (text === null) {
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
const plain2 = withoutComments(text);
|
|
4216
|
+
const name = path7.basename(file);
|
|
4217
|
+
const swc = name === ".swcrc";
|
|
4218
|
+
if (!/^[jt]sconfig/.test(name) && !swc) {
|
|
4219
|
+
const babel = /"babel"\s*:\s*/.exec(plain2);
|
|
4220
|
+
const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
|
|
4221
|
+
const found2 = partsOf2(plain2, babel.index + babel[0].length);
|
|
4222
|
+
return found2 === null ? plain2 : found2.parts.join(",");
|
|
4223
|
+
})();
|
|
4224
|
+
const maps = /module-resolver|\balias\b/.exec(own2);
|
|
4225
|
+
if (maps !== null) {
|
|
4226
|
+
reasons.push(`${shown(file)} maps modules (${maps[0]}), which cannot be told`);
|
|
4227
|
+
}
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
const paths = /"paths"\s*:\s*/.exec(plain2);
|
|
4231
|
+
if (paths === null || !swc && input.testRunner === "jest") {
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
const at = paths.index + paths[0].length;
|
|
4235
|
+
const found = plain2[at] === "{" ? partsOf2(plain2, at) : null;
|
|
4236
|
+
if (found === null) {
|
|
4237
|
+
reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
const own = /"baseUrl"\s*:\s*"([^"]*)"/.exec(plain2);
|
|
4241
|
+
if (own === null && !swc && withBase.length > 0) {
|
|
4242
|
+
reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
const base = path7.resolve(path7.dirname(file), own?.[1] ?? ".");
|
|
4246
|
+
for (const part of found.parts) {
|
|
4247
|
+
const entry = property(part);
|
|
4248
|
+
if (entry === null) {
|
|
4249
|
+
reasons.push(
|
|
4250
|
+
`${shown(file)} names paths in a way that cannot be read as a list of aliases`
|
|
4251
|
+
);
|
|
4252
|
+
continue;
|
|
4253
|
+
}
|
|
4254
|
+
const list = entry.value.startsWith("[") ? partsOf2(entry.value, 0) : null;
|
|
4255
|
+
const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
|
|
4256
|
+
const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
|
|
4257
|
+
if (prefix === null && target !== null && !target.includes("*") && path7.resolve(base, target).split(path7.sep).includes("node_modules")) {
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
|
|
4261
|
+
if (!swc && prefix !== null && mapped.some((one) => prefix.startsWith(one))) {
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
if (prefix === null || directory === null) {
|
|
4265
|
+
reasons.push(missing(file, entry.key, "paths"));
|
|
4266
|
+
} else if (!named(prefix, under(base, directory))) {
|
|
4267
|
+
reasons.push(missing(file, entry.key, "paths", prefix, under(base, directory)));
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
3011
4270
|
}
|
|
3012
|
-
return
|
|
4271
|
+
return reasons.length === 0 ? null : [...new Set(reasons)].join("; ");
|
|
3013
4272
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
4273
|
+
|
|
4274
|
+
// src/commands/mutation.ts
|
|
4275
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4276
|
+
import fs10 from "fs";
|
|
4277
|
+
import path10 from "path";
|
|
4278
|
+
import { pathToFileURL } from "url";
|
|
4279
|
+
|
|
4280
|
+
// src/main-worktree.ts
|
|
4281
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4282
|
+
import fs8 from "fs";
|
|
4283
|
+
import path8 from "path";
|
|
4284
|
+
function git2(dir, args) {
|
|
4285
|
+
const result = spawnSync3("git", args, {
|
|
4286
|
+
cwd: dir,
|
|
4287
|
+
env: gitEnvironment(process.env),
|
|
4288
|
+
encoding: "utf8"
|
|
4289
|
+
});
|
|
4290
|
+
return result.error || result.status !== 0 ? null : result.stdout.trim().split("\n");
|
|
4291
|
+
}
|
|
4292
|
+
function linkedWorktreeOf(dir) {
|
|
4293
|
+
const found = git2(dir, [
|
|
4294
|
+
"rev-parse",
|
|
4295
|
+
"--path-format=absolute",
|
|
4296
|
+
"--show-toplevel",
|
|
4297
|
+
"--git-dir",
|
|
4298
|
+
"--git-common-dir"
|
|
4299
|
+
]);
|
|
4300
|
+
if (found === null || found.length !== 3) {
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
4303
|
+
const [top, gitDir, common] = found;
|
|
4304
|
+
if (gitDir === common || path8.basename(common) !== ".git") {
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
try {
|
|
4308
|
+
const main2 = fs8.realpathSync(path8.dirname(common));
|
|
4309
|
+
const real = fs8.realpathSync(top);
|
|
4310
|
+
return main2 === real ? null : { top: real, main: main2 };
|
|
4311
|
+
} catch {
|
|
4312
|
+
return null;
|
|
4313
|
+
}
|
|
3017
4314
|
}
|
|
3018
|
-
function
|
|
3019
|
-
|
|
4315
|
+
function inMainWorktree(linked, file) {
|
|
4316
|
+
const relative = path8.relative(linked.top, file);
|
|
4317
|
+
if (relative === "" || relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
4318
|
+
return null;
|
|
4319
|
+
}
|
|
4320
|
+
return path8.join(linked.main, relative);
|
|
3020
4321
|
}
|
|
3021
4322
|
|
|
3022
4323
|
// src/commands/mutation-result.ts
|
|
4324
|
+
import fs9 from "fs";
|
|
4325
|
+
import path9 from "path";
|
|
3023
4326
|
function reportPathOf(manifest, override) {
|
|
3024
|
-
return
|
|
4327
|
+
return path9.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
|
|
3025
4328
|
}
|
|
3026
|
-
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
|
|
4329
|
+
function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0, retried = null) {
|
|
3027
4330
|
const manifest = readManifest(dir);
|
|
3028
4331
|
const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
|
|
3029
4332
|
const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
|
|
3030
4333
|
const reportPath = reportPathOf(manifest, reportOverride);
|
|
3031
|
-
const shown =
|
|
4334
|
+
const shown = path9.relative(manifest.dir, reportPath) || reportPath;
|
|
3032
4335
|
let report;
|
|
3033
4336
|
try {
|
|
3034
|
-
report = JSON.parse(
|
|
4337
|
+
report = JSON.parse(fs9.readFileSync(reportPath, "utf8"));
|
|
3035
4338
|
} catch (error) {
|
|
3036
4339
|
io2.err(
|
|
3037
4340
|
`test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
|
|
@@ -3048,7 +4351,7 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3048
4351
|
maxTimeouts: manifest.settings.maxTimeouts,
|
|
3049
4352
|
readSource: (gatePath) => {
|
|
3050
4353
|
try {
|
|
3051
|
-
return
|
|
4354
|
+
return fs9.readFileSync(path9.join(manifest.dir, gatePath), "utf8");
|
|
3052
4355
|
} catch {
|
|
3053
4356
|
return void 0;
|
|
3054
4357
|
}
|
|
@@ -3096,8 +4399,34 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3096
4399
|
io2.err(` - ${formatSurvivor(mutant)}`);
|
|
3097
4400
|
}
|
|
3098
4401
|
io2.err("");
|
|
3099
|
-
|
|
3100
|
-
|
|
4402
|
+
if (retried === null || retried === "failed") {
|
|
4403
|
+
io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
|
|
4404
|
+
if (retried === null) {
|
|
4405
|
+
io2.err(
|
|
4406
|
+
"can time out instead. `test-gates mutation` runs such timeouts again one at a time"
|
|
4407
|
+
);
|
|
4408
|
+
io2.err("(--concurrency 1) before it fails; these were not run again. Only for a");
|
|
4409
|
+
} else {
|
|
4410
|
+
io2.err(
|
|
4411
|
+
"can time out instead. They were run again one at a time (--concurrency 1), but that"
|
|
4412
|
+
);
|
|
4413
|
+
io2.err("run gave no result (see above), so these are those of the first run. Only for a");
|
|
4414
|
+
}
|
|
4415
|
+
} else if (retried.missing === 0) {
|
|
4416
|
+
io2.err("They timed out again when they were run one at a time (--concurrency 1), so they");
|
|
4417
|
+
io2.err("most likely never finish. Look at each mutant first. Only for a");
|
|
4418
|
+
} else {
|
|
4419
|
+
const again = retried.statuses["Timeout"] ?? 0;
|
|
4420
|
+
if (again > 0) {
|
|
4421
|
+
io2.err(
|
|
4422
|
+
`${again} of them timed out again when they were run one at a time (--concurrency 1).`
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
io2.err(
|
|
4426
|
+
`${retried.missing} of them were not in the report of the run again (--concurrency 1),`
|
|
4427
|
+
);
|
|
4428
|
+
io2.err("so they keep the timeout of the first run. Look at each mutant first. Only for a");
|
|
4429
|
+
}
|
|
3101
4430
|
io2.err("mutant that can never finish (a loop that no longer ends), add it to");
|
|
3102
4431
|
io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
|
|
3103
4432
|
io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
|
|
@@ -3116,16 +4445,16 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
|
|
|
3116
4445
|
// src/commands/mutation.ts
|
|
3117
4446
|
var CONFIG_MARK = "\0test-gates-stryker-config\0";
|
|
3118
4447
|
function loadStrykerConfig(manifest, config) {
|
|
3119
|
-
const file =
|
|
4448
|
+
const file = path10.resolve(manifest.dir, config);
|
|
3120
4449
|
if (file.endsWith(".json")) {
|
|
3121
4450
|
try {
|
|
3122
|
-
return JSON.parse(
|
|
4451
|
+
return JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
3123
4452
|
} catch {
|
|
3124
4453
|
return void 0;
|
|
3125
4454
|
}
|
|
3126
4455
|
}
|
|
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)});`;
|
|
3128
|
-
const result =
|
|
4456
|
+
const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns, cleanTempDir: config.cleanTempDir,}) + ${JSON.stringify(CONFIG_MARK)});`;
|
|
4457
|
+
const result = spawnSync4(process.execPath, ["--input-type=module", "--eval", script], {
|
|
3129
4458
|
cwd: manifest.dir,
|
|
3130
4459
|
encoding: "utf8",
|
|
3131
4460
|
timeout: 6e4
|
|
@@ -3138,7 +4467,7 @@ function loadStrykerConfig(manifest, config) {
|
|
|
3138
4467
|
}
|
|
3139
4468
|
function modifiedAt(file) {
|
|
3140
4469
|
try {
|
|
3141
|
-
const stat =
|
|
4470
|
+
const stat = fs10.statSync(file, { bigint: true });
|
|
3142
4471
|
return `${stat.mtimeNs} ${stat.ino}`;
|
|
3143
4472
|
} catch {
|
|
3144
4473
|
return null;
|
|
@@ -3147,6 +4476,7 @@ function modifiedAt(file) {
|
|
|
3147
4476
|
function guardRecordPath(incrementalFile) {
|
|
3148
4477
|
return `${incrementalFile}.test-gates.json`;
|
|
3149
4478
|
}
|
|
4479
|
+
var forcedLine = (reason) => `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${reason}`;
|
|
3150
4480
|
function coveringSpecs(report, gates) {
|
|
3151
4481
|
const fileOf = /* @__PURE__ */ new Map();
|
|
3152
4482
|
for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
|
|
@@ -3173,11 +4503,27 @@ function coveringSpecs(report, gates) {
|
|
|
3173
4503
|
}
|
|
3174
4504
|
return covering;
|
|
3175
4505
|
}
|
|
3176
|
-
function
|
|
4506
|
+
function readSeed(incrementalFile, recordFile) {
|
|
4507
|
+
try {
|
|
4508
|
+
const incremental = fs10.readFileSync(incrementalFile);
|
|
4509
|
+
const record = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
|
|
4510
|
+
const named = record?.incremental;
|
|
4511
|
+
return named === hashOf(incremental) ? { incremental, record } : null;
|
|
4512
|
+
} catch {
|
|
4513
|
+
return null;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
function decideReuse(manifest, config, strykerArgs, part, affected, found = () => void 0) {
|
|
3177
4517
|
const loaded = loadStrykerConfig(manifest, config);
|
|
3178
4518
|
const options = incrementalOptions(loaded, strykerArgs);
|
|
3179
|
-
const base =
|
|
3180
|
-
const
|
|
4519
|
+
const base = fs10.realpathSync(manifest.dir);
|
|
4520
|
+
const checkersArg = strykerArgs.some(
|
|
4521
|
+
(arg) => arg === "--checkers" || arg.startsWith("--checkers=")
|
|
4522
|
+
);
|
|
4523
|
+
const configs = () => runnerConfigs(base, config, {
|
|
4524
|
+
...loaded ?? {},
|
|
4525
|
+
...checkersArg ? { checkers: ["--checkers"] } : {}
|
|
4526
|
+
});
|
|
3181
4527
|
const decision = {
|
|
3182
4528
|
incremental: false,
|
|
3183
4529
|
incrementalFile: null,
|
|
@@ -3194,25 +4540,44 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3194
4540
|
last: void 0,
|
|
3195
4541
|
ci: affected ? affectedRefused(process.env) : null,
|
|
3196
4542
|
unchanged: null,
|
|
3197
|
-
configs
|
|
4543
|
+
configs,
|
|
4544
|
+
temp: {
|
|
4545
|
+
dir: loaded === void 0 ? null : path10.resolve(base, options.tempDirName),
|
|
4546
|
+
keeps: loaded?.cleanTempDir === false
|
|
4547
|
+
},
|
|
4548
|
+
mainWorktree: null,
|
|
4549
|
+
seed: null
|
|
3198
4550
|
};
|
|
4551
|
+
found(decision.temp);
|
|
3199
4552
|
if (loaded !== void 0 && !options.incremental) {
|
|
3200
4553
|
return decision;
|
|
3201
4554
|
}
|
|
3202
4555
|
decision.incremental = true;
|
|
3203
|
-
const incrementalFile =
|
|
4556
|
+
const incrementalFile = path10.resolve(base, options.incrementalFile);
|
|
3204
4557
|
const recordFile = guardRecordPath(incrementalFile);
|
|
3205
4558
|
decision.incrementalFile = incrementalFile;
|
|
3206
4559
|
decision.recordFile = recordFile;
|
|
3207
4560
|
let snapshot = null;
|
|
3208
4561
|
let root = base;
|
|
3209
4562
|
let failure = "the Stryker config could not be loaded to see where the incremental file is";
|
|
4563
|
+
const reports = reportFiles(loaded).map((file) => path10.resolve(base, file));
|
|
4564
|
+
const ignores = strykerIgnores(loaded);
|
|
4565
|
+
const read2 = (file) => {
|
|
4566
|
+
const relative = path10.relative(base, file);
|
|
4567
|
+
const inside = !relative.startsWith("..") && !path10.isAbsolute(relative);
|
|
4568
|
+
return !reports.includes(file) && !(inside && ignores(relative.split(path10.sep).join("/")));
|
|
4569
|
+
};
|
|
3210
4570
|
if (loaded !== void 0) {
|
|
3211
4571
|
const scope = (listed2) => {
|
|
3212
4572
|
try {
|
|
3213
|
-
const
|
|
3214
|
-
|
|
3215
|
-
|
|
4573
|
+
const found2 = loadedScope(manifest, configs(), listed2);
|
|
4574
|
+
if (!("files" in found2)) {
|
|
4575
|
+
decision.scope = found2;
|
|
4576
|
+
return null;
|
|
4577
|
+
}
|
|
4578
|
+
const files = new Set([...found2.files].filter(read2));
|
|
4579
|
+
decision.scope = { files: files.size };
|
|
4580
|
+
return files;
|
|
3216
4581
|
} catch (error) {
|
|
3217
4582
|
decision.scope = { every: error.message };
|
|
3218
4583
|
return null;
|
|
@@ -3226,8 +4591,9 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3226
4591
|
written: [
|
|
3227
4592
|
incrementalFile,
|
|
3228
4593
|
recordFile,
|
|
3229
|
-
|
|
3230
|
-
|
|
4594
|
+
path10.resolve(base, options.tempDirName),
|
|
4595
|
+
path10.resolve(base, manifest.settings.strykerReportFile),
|
|
4596
|
+
...reports
|
|
3231
4597
|
],
|
|
3232
4598
|
args: comparableArgs(strykerArgs)
|
|
3233
4599
|
}));
|
|
@@ -3239,17 +4605,32 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3239
4605
|
decision.snapshot = snapshot;
|
|
3240
4606
|
decision.failure = snapshot === null ? failure : null;
|
|
3241
4607
|
decision.root = root;
|
|
3242
|
-
const
|
|
4608
|
+
const tempDir = path10.basename(options.tempDirName);
|
|
4609
|
+
if (!part && !options.force && snapshot?.mode === "git" && !path10.relative(root, base).split(path10.sep).includes(tempDir) && !fs10.existsSync(incrementalFile) && !fs10.existsSync(recordFile)) {
|
|
4610
|
+
const linked = linkedWorktreeOf(base);
|
|
4611
|
+
const mainIncremental = linked === null ? null : inMainWorktree(linked, incrementalFile);
|
|
4612
|
+
const mainRecord = linked === null ? null : inMainWorktree(linked, recordFile);
|
|
4613
|
+
if (linked !== null && mainIncremental !== null && mainRecord !== null) {
|
|
4614
|
+
decision.mainWorktree = linked.main;
|
|
4615
|
+
const seed = readSeed(mainIncremental, mainRecord);
|
|
4616
|
+
if (seed !== null) {
|
|
4617
|
+
decision.seed = seed;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
const incremental = decision.seed === null ? hashFile(incrementalFile) : hashOf(decision.seed.incremental);
|
|
3243
4622
|
decision.incrementalHash = incremental;
|
|
3244
4623
|
if (part) {
|
|
3245
4624
|
decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
|
|
3246
4625
|
return decision;
|
|
3247
4626
|
}
|
|
3248
|
-
let stored;
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
4627
|
+
let stored = decision.seed?.record;
|
|
4628
|
+
if (decision.seed === null) {
|
|
4629
|
+
try {
|
|
4630
|
+
stored = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
|
|
4631
|
+
} catch {
|
|
4632
|
+
stored = void 0;
|
|
4633
|
+
}
|
|
3253
4634
|
}
|
|
3254
4635
|
const ci = decision.ci;
|
|
3255
4636
|
let force = null;
|
|
@@ -3260,14 +4641,21 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3260
4641
|
force = failure;
|
|
3261
4642
|
} else if (!affected) {
|
|
3262
4643
|
force = refuseReuse({ record: stored, incremental, snapshot });
|
|
4644
|
+
if (force === null) {
|
|
4645
|
+
const read3 = readRecord({ record: stored, incremental, snapshot });
|
|
4646
|
+
last = "record" in read3 ? read3.record.affected : void 0;
|
|
4647
|
+
if (decision.seed !== null) {
|
|
4648
|
+
force = blindInMainRecord(manifest, { root, snapshot, configs, last });
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
3263
4651
|
} else if (ci !== null) {
|
|
3264
4652
|
force = ci;
|
|
3265
4653
|
} else {
|
|
3266
|
-
const
|
|
3267
|
-
if ("refused" in
|
|
3268
|
-
force =
|
|
4654
|
+
const read3 = readRecord({ record: stored, incremental, snapshot });
|
|
4655
|
+
if ("refused" in read3) {
|
|
4656
|
+
force = read3.refused;
|
|
3269
4657
|
} else {
|
|
3270
|
-
last =
|
|
4658
|
+
last = read3.record.affected;
|
|
3271
4659
|
try {
|
|
3272
4660
|
const inputs = affectedInputs(manifest, {
|
|
3273
4661
|
root,
|
|
@@ -3279,14 +4667,16 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3279
4667
|
)
|
|
3280
4668
|
});
|
|
3281
4669
|
plan = planAffected({
|
|
3282
|
-
before: { files:
|
|
4670
|
+
before: { files: read3.record.snapshot.files, affected: last },
|
|
3283
4671
|
now: snapshot.files,
|
|
3284
4672
|
subproject: inputs.subproject,
|
|
3285
4673
|
config: inputs.config,
|
|
3286
4674
|
gates: manifest.gates.map((gate) => ({
|
|
3287
4675
|
path: gate.path,
|
|
3288
|
-
files: inputs.gates[gate.path]?.files ?? []
|
|
3289
|
-
|
|
4676
|
+
files: inputs.gates[gate.path]?.files ?? [],
|
|
4677
|
+
blind: inputs.gates[gate.path]?.blind
|
|
4678
|
+
})),
|
|
4679
|
+
seeded: decision.seed !== null
|
|
3290
4680
|
});
|
|
3291
4681
|
} catch (error) {
|
|
3292
4682
|
if (!(error instanceof AffectedError)) {
|
|
@@ -3295,7 +4685,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3295
4685
|
plan = { full: `what a gate depends on cannot be told: ${error.message}` };
|
|
3296
4686
|
}
|
|
3297
4687
|
if ("full" in plan) {
|
|
3298
|
-
if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
4688
|
+
if (decision.seed === null && refuseReuse({ record: stored, incremental, snapshot }) === null) {
|
|
3299
4689
|
decision.unchanged = plan.full;
|
|
3300
4690
|
plan = null;
|
|
3301
4691
|
} else {
|
|
@@ -3312,9 +4702,45 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
|
|
|
3312
4702
|
decision.last = last;
|
|
3313
4703
|
return decision;
|
|
3314
4704
|
}
|
|
3315
|
-
function
|
|
3316
|
-
const
|
|
3317
|
-
const
|
|
4705
|
+
function blindInMainRecord(manifest, input) {
|
|
4706
|
+
const { last } = input;
|
|
4707
|
+
const unknown = last === void 0 ? "each gate" : manifest.gates.find((gate) => !Object.hasOwn(last.gates, gate.path))?.path;
|
|
4708
|
+
if (last === void 0 || unknown !== void 0) {
|
|
4709
|
+
return `the record of the main worktree does not say what ${unknown} depends on, so it may read files git does not list, which differ between worktrees`;
|
|
4710
|
+
}
|
|
4711
|
+
try {
|
|
4712
|
+
const inputs = affectedInputs(manifest, {
|
|
4713
|
+
root: input.root,
|
|
4714
|
+
files: input.snapshot.files,
|
|
4715
|
+
configs: input.configs(),
|
|
4716
|
+
tests: last.tests,
|
|
4717
|
+
covering: Object.fromEntries(
|
|
4718
|
+
Object.entries(last.gates).map(([gate, entry]) => [gate, entry.specs])
|
|
4719
|
+
)
|
|
4720
|
+
});
|
|
4721
|
+
for (const gate of manifest.gates) {
|
|
4722
|
+
const blind = inputs.gates[gate.path]?.blind ?? last.gates[gate.path]?.blind;
|
|
4723
|
+
if (blind !== void 0) {
|
|
4724
|
+
return `${gate.path} may read files git does not list, which differ between worktrees (${blind})`;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
return null;
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
if (!(error instanceof AffectedError)) {
|
|
4730
|
+
throw error;
|
|
4731
|
+
}
|
|
4732
|
+
return `what a gate depends on cannot be told, so it may read files git does not list, which differ between worktrees: ${error.message}`;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
function guardIncremental(manifest, config, strykerArgs, part, affected, found) {
|
|
4736
|
+
const decision = decideReuse(manifest, config, strykerArgs, part, affected, found);
|
|
4737
|
+
const nothing = {
|
|
4738
|
+
force: null,
|
|
4739
|
+
mutate: null,
|
|
4740
|
+
said: [],
|
|
4741
|
+
temp: decision.temp,
|
|
4742
|
+
record: () => void 0
|
|
4743
|
+
};
|
|
3318
4744
|
if (!decision.incremental) {
|
|
3319
4745
|
return affected ? {
|
|
3320
4746
|
...nothing,
|
|
@@ -3323,71 +4749,96 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
3323
4749
|
]
|
|
3324
4750
|
} : nothing;
|
|
3325
4751
|
}
|
|
3326
|
-
const { snapshot, root, plan, last,
|
|
4752
|
+
const { snapshot, root, plan, last, configs, force } = decision;
|
|
3327
4753
|
const incrementalFile = decision.incrementalFile;
|
|
3328
4754
|
const recordFile = decision.recordFile;
|
|
3329
4755
|
const incremental = decision.incrementalHash;
|
|
3330
4756
|
const options = { force: decision.forced };
|
|
3331
4757
|
if (part) {
|
|
3332
|
-
|
|
4758
|
+
fs10.rmSync(recordFile, { force: true });
|
|
3333
4759
|
return { ...nothing, force };
|
|
3334
4760
|
}
|
|
3335
|
-
|
|
4761
|
+
if (decision.seed !== null) {
|
|
4762
|
+
fs10.mkdirSync(path10.dirname(incrementalFile), { recursive: true });
|
|
4763
|
+
fs10.writeFileSync(incrementalFile, decision.seed.incremental);
|
|
4764
|
+
}
|
|
4765
|
+
fs10.rmSync(recordFile, { force: true });
|
|
3336
4766
|
const before = modifiedAt(incrementalFile);
|
|
3337
4767
|
const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
|
|
3338
4768
|
const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
|
|
3339
4769
|
let ranges = [];
|
|
4770
|
+
let timedOut = 0;
|
|
4771
|
+
let expected = 0;
|
|
3340
4772
|
if (everyGate === null && incremental !== null && !options.force) {
|
|
3341
|
-
const kept = rerun === null ? manifest.gates
|
|
4773
|
+
const kept = rerun === null ? manifest.gates : manifest.gates.filter((gate) => !rerun.includes(gate.path));
|
|
3342
4774
|
try {
|
|
3343
|
-
|
|
3344
|
-
|
|
4775
|
+
({
|
|
4776
|
+
ranges,
|
|
4777
|
+
rerun: timedOut,
|
|
4778
|
+
expected
|
|
4779
|
+
} = timeoutRanges(
|
|
4780
|
+
JSON.parse(fs10.readFileSync(incrementalFile, "utf8")),
|
|
3345
4781
|
kept
|
|
3346
|
-
);
|
|
4782
|
+
));
|
|
3347
4783
|
} catch {
|
|
3348
|
-
ranges = [];
|
|
3349
4784
|
}
|
|
3350
4785
|
}
|
|
3351
4786
|
const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
|
|
4787
|
+
const ran = affected ? rerun : everyGate !== null || options.force || incremental === null ? null : [];
|
|
3352
4788
|
let said = [];
|
|
3353
4789
|
if (affected && !options.force) {
|
|
3354
4790
|
if (force !== null) {
|
|
3355
4791
|
said = describePlan({ full: force });
|
|
3356
4792
|
} else if (plan !== null) {
|
|
3357
|
-
said = describePlan(plan, 5,
|
|
4793
|
+
said = describePlan(plan, 5, timedOut, expected);
|
|
3358
4794
|
} else if (decision.unchanged !== null) {
|
|
3359
4795
|
said = [
|
|
3360
4796
|
`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 ${
|
|
4797
|
+
...ranges.length > 0 ? [` and ${timedOut} mutant(s) that timed out in the last run are run again`] : [],
|
|
4798
|
+
...expected > 0 ? [` ${describeExpected(expected)}`] : []
|
|
3362
4799
|
];
|
|
3363
4800
|
}
|
|
3364
|
-
} else
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
4801
|
+
} else {
|
|
4802
|
+
if (ranges.length > 0) {
|
|
4803
|
+
said.push(
|
|
4804
|
+
`test-gates mutation: ${timedOut} mutant(s) that timed out in the last run are run again, the other results are reused`
|
|
4805
|
+
);
|
|
4806
|
+
}
|
|
4807
|
+
if (expected > 0) {
|
|
4808
|
+
said.push(`test-gates mutation: ${describeExpected(expected)}`);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
if (decision.seed !== null) {
|
|
4812
|
+
if (said.length === 0 && everyGate !== null) {
|
|
4813
|
+
said.push(forcedLine(everyGate));
|
|
4814
|
+
}
|
|
4815
|
+
said.unshift(
|
|
4816
|
+
`test-gates mutation: this worktree has no record of its own, so it starts from the one of the main worktree (${decision.mainWorktree})`
|
|
4817
|
+
);
|
|
3368
4818
|
}
|
|
3369
4819
|
return {
|
|
3370
4820
|
// Every gate is run again: the same as a run of every mutant.
|
|
3371
4821
|
force: everyGate,
|
|
3372
4822
|
mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
|
|
3373
4823
|
said,
|
|
4824
|
+
temp: decision.temp,
|
|
3374
4825
|
record: (judged) => {
|
|
3375
4826
|
const written = hashFile(incrementalFile);
|
|
3376
4827
|
if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
|
|
3377
4828
|
return;
|
|
3378
4829
|
}
|
|
3379
4830
|
const record = { format: RECORD_FORMAT, incremental: written, snapshot };
|
|
3380
|
-
if (
|
|
3381
|
-
record.affected = affectedRecord(judged,
|
|
4831
|
+
if (affectedRefused(process.env) === null && judged !== null) {
|
|
4832
|
+
record.affected = affectedRecord(judged, ran, last);
|
|
3382
4833
|
}
|
|
3383
4834
|
try {
|
|
3384
|
-
|
|
4835
|
+
fs10.writeFileSync(recordFile, `${JSON.stringify(record)}
|
|
3385
4836
|
`);
|
|
3386
4837
|
} catch {
|
|
3387
4838
|
}
|
|
3388
4839
|
}
|
|
3389
4840
|
};
|
|
3390
|
-
function affectedRecord(judged,
|
|
4841
|
+
function affectedRecord(judged, ran2, previous) {
|
|
3391
4842
|
const gates = manifest.gates.map((gate) => gate.path);
|
|
3392
4843
|
const dirty = new Set(
|
|
3393
4844
|
[...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
|
|
@@ -3395,7 +4846,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
3395
4846
|
const fresh = coveringSpecs(judged.report, gates);
|
|
3396
4847
|
const covering = /* @__PURE__ */ Object.create(null);
|
|
3397
4848
|
for (const gate of gates) {
|
|
3398
|
-
const specs =
|
|
4849
|
+
const specs = ran2 === null || ran2.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
|
|
3399
4850
|
if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
|
|
3400
4851
|
covering[gate] = specs;
|
|
3401
4852
|
}
|
|
@@ -3412,12 +4863,16 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
|
|
|
3412
4863
|
for (const gate of Object.keys(covering)) {
|
|
3413
4864
|
const entry = inputs.gates[gate];
|
|
3414
4865
|
if (entry !== void 0) {
|
|
3415
|
-
recorded[gate] = {
|
|
4866
|
+
recorded[gate] = {
|
|
4867
|
+
specs: entry.specs,
|
|
4868
|
+
files: entry.files,
|
|
4869
|
+
...entry.blind === void 0 ? {} : { blind: entry.blind }
|
|
4870
|
+
};
|
|
3416
4871
|
}
|
|
3417
4872
|
}
|
|
3418
4873
|
return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
|
|
3419
4874
|
} catch (error) {
|
|
3420
|
-
if (error instanceof AffectedError) {
|
|
4875
|
+
if (error instanceof AffectedError || !affected) {
|
|
3421
4876
|
return void 0;
|
|
3422
4877
|
}
|
|
3423
4878
|
throw error;
|
|
@@ -3463,53 +4918,120 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
|
|
|
3463
4918
|
);
|
|
3464
4919
|
}
|
|
3465
4920
|
const reportFile = reportPathOf(manifest, null);
|
|
3466
|
-
|
|
3467
|
-
|
|
4921
|
+
fs10.rmSync(reportFile, { force: true });
|
|
4922
|
+
let removed = [];
|
|
4923
|
+
const guard = guardIncremental(manifest, config, strykerArgs, part, affected, (temp) => {
|
|
4924
|
+
removed = removeStaleSandboxes(manifest.dir, temp);
|
|
4925
|
+
});
|
|
3468
4926
|
if (guard.said.length > 0) {
|
|
3469
4927
|
guard.said.forEach((line) => io2.out(line));
|
|
3470
4928
|
} 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
|
-
);
|
|
4929
|
+
io2.out(forcedLine(guard.force));
|
|
3474
4930
|
}
|
|
4931
|
+
removed.forEach((line) => io2.out(line));
|
|
3475
4932
|
const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
|
|
3476
4933
|
const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
|
|
3477
|
-
const
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
4934
|
+
const unmark = markRun(manifest.dir, guard.temp);
|
|
4935
|
+
let retried;
|
|
4936
|
+
try {
|
|
4937
|
+
const result = spawnSync4(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
|
|
4938
|
+
cwd: manifest.dir,
|
|
4939
|
+
stdio: "inherit",
|
|
4940
|
+
shell: process.platform === "win32"
|
|
4941
|
+
});
|
|
4942
|
+
if (result.error) {
|
|
4943
|
+
io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
|
|
4944
|
+
return 1;
|
|
4945
|
+
}
|
|
4946
|
+
if (result.status !== 0) {
|
|
4947
|
+
io2.err(
|
|
4948
|
+
`test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
|
|
4949
|
+
);
|
|
4950
|
+
return 1;
|
|
4951
|
+
}
|
|
4952
|
+
retried = retryTimeouts(
|
|
4953
|
+
stryker,
|
|
4954
|
+
[config, ...strykerArgs],
|
|
4955
|
+
manifest,
|
|
4956
|
+
reportFile,
|
|
4957
|
+
part ? manifest.gates.filter((gate) => chosen.selected.includes(gate.path)) : manifest.gates,
|
|
4958
|
+
io2
|
|
3489
4959
|
);
|
|
3490
|
-
|
|
4960
|
+
} finally {
|
|
4961
|
+
unmark();
|
|
3491
4962
|
}
|
|
3492
4963
|
let judged = null;
|
|
3493
|
-
const status = runMutationResult(
|
|
3494
|
-
|
|
3495
|
-
|
|
4964
|
+
const status = runMutationResult(
|
|
4965
|
+
dir,
|
|
4966
|
+
null,
|
|
4967
|
+
io2,
|
|
4968
|
+
part ? chosen.selected : [],
|
|
4969
|
+
(seen) => {
|
|
4970
|
+
judged = seen;
|
|
4971
|
+
},
|
|
4972
|
+
retried
|
|
4973
|
+
);
|
|
3496
4974
|
guard.record(judged);
|
|
3497
4975
|
if (part) {
|
|
3498
|
-
|
|
4976
|
+
fs10.rmSync(reportFile, { force: true });
|
|
3499
4977
|
}
|
|
3500
4978
|
return status;
|
|
3501
4979
|
}
|
|
4980
|
+
function retryTimeouts(stryker, args, manifest, reportFile, gates, io2) {
|
|
4981
|
+
let first;
|
|
4982
|
+
try {
|
|
4983
|
+
first = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
|
|
4984
|
+
} catch {
|
|
4985
|
+
return null;
|
|
4986
|
+
}
|
|
4987
|
+
if (typeof first !== "object" || first === null) {
|
|
4988
|
+
return null;
|
|
4989
|
+
}
|
|
4990
|
+
const { ranges, rerun } = timeoutRanges(first, gates);
|
|
4991
|
+
if (ranges.length === 0 || rerun <= manifest.settings.maxTimeouts) {
|
|
4992
|
+
return null;
|
|
4993
|
+
}
|
|
4994
|
+
io2.out(
|
|
4995
|
+
`test-gates mutation: ${rerun} mutant(s) timed out outside expectedTimeouts, running them again one at a time (--concurrency 1)`
|
|
4996
|
+
);
|
|
4997
|
+
fs10.rmSync(reportFile, { force: true });
|
|
4998
|
+
const result = spawnSync4(
|
|
4999
|
+
stryker,
|
|
5000
|
+
["run", ...args, "--mutate", ranges.join(","), "--concurrency", "1", "--force"],
|
|
5001
|
+
{ cwd: manifest.dir, stdio: "inherit", shell: process.platform === "win32" }
|
|
5002
|
+
);
|
|
5003
|
+
let retry = null;
|
|
5004
|
+
if (!result.error && result.status === 0) {
|
|
5005
|
+
try {
|
|
5006
|
+
retry = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
|
|
5007
|
+
} catch {
|
|
5008
|
+
retry = null;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
if (retry === null || typeof retry !== "object") {
|
|
5012
|
+
const why = result.error ? result.error.message : result.signal ? `signal ${result.signal}` : result.status === 0 ? "no report" : `exit ${result.status}`;
|
|
5013
|
+
io2.err(
|
|
5014
|
+
`test-gates mutation: running the timed-out mutants again failed (${why}). The first run is judged`
|
|
5015
|
+
);
|
|
5016
|
+
fs10.writeFileSync(reportFile, JSON.stringify(first));
|
|
5017
|
+
return "failed";
|
|
5018
|
+
}
|
|
5019
|
+
const merged = mergeRetry(first, retry, gates);
|
|
5020
|
+
fs10.writeFileSync(reportFile, JSON.stringify(merged.report));
|
|
5021
|
+
io2.out(formatRetry(merged.outcome));
|
|
5022
|
+
return merged.outcome;
|
|
5023
|
+
}
|
|
3502
5024
|
|
|
3503
5025
|
// src/commands/doctor.ts
|
|
3504
5026
|
function read(file) {
|
|
3505
5027
|
try {
|
|
3506
|
-
return
|
|
5028
|
+
return fs11.readFileSync(file, "utf8");
|
|
3507
5029
|
} catch {
|
|
3508
5030
|
return null;
|
|
3509
5031
|
}
|
|
3510
5032
|
}
|
|
3511
|
-
function
|
|
3512
|
-
const result =
|
|
5033
|
+
function git3(dir, args) {
|
|
5034
|
+
const result = spawnSync5("git", args, {
|
|
3513
5035
|
cwd: dir,
|
|
3514
5036
|
env: gitEnvironment(process.env),
|
|
3515
5037
|
encoding: "utf8"
|
|
@@ -3517,20 +5039,20 @@ function git2(dir, args) {
|
|
|
3517
5039
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
3518
5040
|
}
|
|
3519
5041
|
function hooksOf(dir) {
|
|
3520
|
-
const root =
|
|
3521
|
-
const hooksDir =
|
|
5042
|
+
const root = git3(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
|
|
5043
|
+
const hooksDir = git3(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
|
|
3522
5044
|
const candidates = [
|
|
3523
|
-
...["pre-push", "pre-commit"].map((name) =>
|
|
3524
|
-
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) =>
|
|
3525
|
-
...["lefthook.yml", ".lefthook.yml"].map((name) =>
|
|
5045
|
+
...["pre-push", "pre-commit"].map((name) => path11.join(root, ".husky", name)),
|
|
5046
|
+
...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path11.join(hooksDir, name)),
|
|
5047
|
+
...["lefthook.yml", ".lefthook.yml"].map((name) => path11.join(root, name))
|
|
3526
5048
|
];
|
|
3527
5049
|
let script = "";
|
|
3528
5050
|
try {
|
|
3529
|
-
const manifest = JSON.parse(read(
|
|
5051
|
+
const manifest = JSON.parse(read(path11.join(dir, "package.json")) ?? "{}");
|
|
3530
5052
|
script = manifest.scripts?.["test:gates:mutation"] ?? "";
|
|
3531
5053
|
} catch {
|
|
3532
5054
|
}
|
|
3533
|
-
const subproject =
|
|
5055
|
+
const subproject = path11.relative(fs11.realpathSync(root), dir).split(path11.sep).join("/");
|
|
3534
5056
|
const seen = /* @__PURE__ */ new Set();
|
|
3535
5057
|
const uses = [];
|
|
3536
5058
|
for (const file of candidates) {
|
|
@@ -3538,12 +5060,12 @@ function hooksOf(dir) {
|
|
|
3538
5060
|
if (text === null) {
|
|
3539
5061
|
continue;
|
|
3540
5062
|
}
|
|
3541
|
-
const real =
|
|
5063
|
+
const real = fs11.realpathSync(file);
|
|
3542
5064
|
if (seen.has(real)) {
|
|
3543
5065
|
continue;
|
|
3544
5066
|
}
|
|
3545
5067
|
seen.add(real);
|
|
3546
|
-
const shown =
|
|
5068
|
+
const shown = path11.relative(root, file).split(path11.sep).join("/");
|
|
3547
5069
|
uses.push(...hookUses({ file: shown, text, subproject, script }));
|
|
3548
5070
|
}
|
|
3549
5071
|
return uses;
|
|
@@ -3560,8 +5082,8 @@ function runDoctor(dir, io2) {
|
|
|
3560
5082
|
const affected = decideReuse(manifest, config, [], false, true);
|
|
3561
5083
|
io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
|
|
3562
5084
|
if (plain2.snapshot !== null) {
|
|
3563
|
-
const subproject =
|
|
3564
|
-
const prefix = subproject === "" ? "" : `${subproject.split(
|
|
5085
|
+
const subproject = path11.relative(plain2.root, fs11.realpathSync(manifest.dir));
|
|
5086
|
+
const prefix = subproject === "" ? "" : `${subproject.split(path11.sep).join("/")}/`;
|
|
3565
5087
|
const keys = Object.keys(plain2.snapshot.files);
|
|
3566
5088
|
const outside = keys.filter((key) => !key.startsWith(prefix)).length;
|
|
3567
5089
|
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";
|
|
@@ -3572,6 +5094,15 @@ function runDoctor(dir, io2) {
|
|
|
3572
5094
|
io2.out(
|
|
3573
5095
|
reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
|
|
3574
5096
|
);
|
|
5097
|
+
if (plain2.seed !== null) {
|
|
5098
|
+
io2.out(
|
|
5099
|
+
` this worktree has no record of its own: the next run starts from the one of the main worktree (${plain2.mainWorktree})`
|
|
5100
|
+
);
|
|
5101
|
+
} else if (plain2.mainWorktree !== null) {
|
|
5102
|
+
io2.out(
|
|
5103
|
+
` this worktree has no record of its own, and the main worktree (${plain2.mainWorktree}) has none this worktree can start from`
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
3575
5106
|
if (affected.ci !== null) {
|
|
3576
5107
|
io2.out(` next run with --affected: every mutant, ${affected.ci}`);
|
|
3577
5108
|
} else if (affected.plan !== null && "rerun" in affected.plan) {
|
|
@@ -3588,15 +5119,20 @@ function runDoctor(dir, io2) {
|
|
|
3588
5119
|
}
|
|
3589
5120
|
}
|
|
3590
5121
|
let blind = null;
|
|
5122
|
+
let blindGates = [];
|
|
3591
5123
|
if (plain2.snapshot !== null) {
|
|
3592
5124
|
try {
|
|
3593
|
-
affectedInputs(manifest, {
|
|
5125
|
+
const inputs = affectedInputs(manifest, {
|
|
3594
5126
|
root: plain2.root,
|
|
3595
5127
|
files: plain2.snapshot.files,
|
|
3596
5128
|
configs: plain2.configs(),
|
|
3597
5129
|
tests: [],
|
|
3598
5130
|
covering: {}
|
|
3599
5131
|
});
|
|
5132
|
+
blindGates = manifest.gates.flatMap((gate) => {
|
|
5133
|
+
const reason = inputs.gates[gate.path]?.blind;
|
|
5134
|
+
return reason === void 0 ? [] : [{ gate: gate.path, reason }];
|
|
5135
|
+
});
|
|
3600
5136
|
} catch (error) {
|
|
3601
5137
|
if (!(error instanceof AffectedError)) {
|
|
3602
5138
|
throw error;
|
|
@@ -3604,6 +5140,8 @@ function runDoctor(dir, io2) {
|
|
|
3604
5140
|
blind = error.message;
|
|
3605
5141
|
}
|
|
3606
5142
|
}
|
|
5143
|
+
const left = sandboxPlan(manifest.dir, plain2.temp);
|
|
5144
|
+
const count = "kept" in left.plan ? left.plan.kept.length : left.plan.remove.length;
|
|
3607
5145
|
const { lines, status } = formatFindings(
|
|
3608
5146
|
diagnose({
|
|
3609
5147
|
incremental: plain2.incremental,
|
|
@@ -3611,8 +5149,16 @@ function runDoctor(dir, io2) {
|
|
|
3611
5149
|
failure: plain2.failure,
|
|
3612
5150
|
widened: plain2.widened,
|
|
3613
5151
|
scope: plain2.scope,
|
|
3614
|
-
hooks: hooksOf(
|
|
3615
|
-
blind
|
|
5152
|
+
hooks: hooksOf(fs11.realpathSync(manifest.dir)),
|
|
5153
|
+
blind,
|
|
5154
|
+
blindGates,
|
|
5155
|
+
gates: manifest.gates.length,
|
|
5156
|
+
sandboxes: count === 0 ? null : {
|
|
5157
|
+
tempDir: left.shown,
|
|
5158
|
+
count,
|
|
5159
|
+
kept: "kept" in left.plan ? left.plan.why : null,
|
|
5160
|
+
running: "kept" in left.plan ? left.plan.running : null
|
|
5161
|
+
}
|
|
3616
5162
|
})
|
|
3617
5163
|
);
|
|
3618
5164
|
lines.forEach((line) => io2.out(line));
|
|
@@ -3620,8 +5166,8 @@ function runDoctor(dir, io2) {
|
|
|
3620
5166
|
}
|
|
3621
5167
|
|
|
3622
5168
|
// src/commands/lcov.ts
|
|
3623
|
-
import
|
|
3624
|
-
import
|
|
5169
|
+
import fs12 from "fs";
|
|
5170
|
+
import path12 from "path";
|
|
3625
5171
|
|
|
3626
5172
|
// src/core/lcov.ts
|
|
3627
5173
|
function normalizePath2(file, rootDir) {
|
|
@@ -3648,9 +5194,9 @@ function parseLcov(text, rootDir) {
|
|
|
3648
5194
|
const line = rawLine.trim();
|
|
3649
5195
|
if (line.startsWith("SF:")) {
|
|
3650
5196
|
closeRecord();
|
|
3651
|
-
const
|
|
3652
|
-
const file = files.get(
|
|
3653
|
-
files.set(
|
|
5197
|
+
const path15 = normalizePath2(line.slice(3), rootDir);
|
|
5198
|
+
const file = files.get(path15) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
|
|
5199
|
+
files.set(path15, file);
|
|
3654
5200
|
record = { file, hasDa: false, found: 0, hit: 0 };
|
|
3655
5201
|
} else if (line === "end_of_record") {
|
|
3656
5202
|
closeRecord();
|
|
@@ -3734,10 +5280,10 @@ function runLcov(dir, fileOption, io2) {
|
|
|
3734
5280
|
"no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
|
|
3735
5281
|
);
|
|
3736
5282
|
}
|
|
3737
|
-
const lcovPath =
|
|
5283
|
+
const lcovPath = path12.resolve(manifest.dir, file);
|
|
3738
5284
|
let text;
|
|
3739
5285
|
try {
|
|
3740
|
-
text =
|
|
5286
|
+
text = fs12.readFileSync(lcovPath, "utf8");
|
|
3741
5287
|
} catch (error) {
|
|
3742
5288
|
io2.err(
|
|
3743
5289
|
`test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
|
|
@@ -3780,10 +5326,10 @@ function runLcov(dir, fileOption, io2) {
|
|
|
3780
5326
|
}
|
|
3781
5327
|
|
|
3782
5328
|
// src/commands/selfcheck.ts
|
|
3783
|
-
import { spawn, spawnSync as
|
|
3784
|
-
import
|
|
5329
|
+
import { spawn, spawnSync as spawnSync6 } from "child_process";
|
|
5330
|
+
import fs14 from "fs";
|
|
3785
5331
|
import os2 from "os";
|
|
3786
|
-
import
|
|
5332
|
+
import path14 from "path";
|
|
3787
5333
|
|
|
3788
5334
|
// src/core/jobs.ts
|
|
3789
5335
|
var SWITCHES = {
|
|
@@ -3823,6 +5369,34 @@ function knownCommand(program, args) {
|
|
|
3823
5369
|
function jobFlags(runner, dir) {
|
|
3824
5370
|
return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
|
|
3825
5371
|
}
|
|
5372
|
+
var LIMITS = {
|
|
5373
|
+
jest: ["--runInBand", "-i", "--maxWorkers", "-w"],
|
|
5374
|
+
vitest: ["--no-file-parallelism", "--fileParallelism", "--maxWorkers", "--minWorkers"]
|
|
5375
|
+
};
|
|
5376
|
+
var CONFIG_LIMITS = [
|
|
5377
|
+
"maxWorkers",
|
|
5378
|
+
"minWorkers",
|
|
5379
|
+
"maxThreads",
|
|
5380
|
+
"minThreads",
|
|
5381
|
+
"maxForks",
|
|
5382
|
+
"minForks",
|
|
5383
|
+
"fileParallelism",
|
|
5384
|
+
"singleThread",
|
|
5385
|
+
"singleFork"
|
|
5386
|
+
];
|
|
5387
|
+
function workerFlags(input) {
|
|
5388
|
+
if (input.lines <= 1) {
|
|
5389
|
+
return [];
|
|
5390
|
+
}
|
|
5391
|
+
const given = input.args.some(
|
|
5392
|
+
(arg) => LIMITS[input.runner].includes(arg.split("=")[0])
|
|
5393
|
+
);
|
|
5394
|
+
const configured = CONFIG_LIMITS.some((word) => input.configSource?.includes(word) === true);
|
|
5395
|
+
if (given || configured) {
|
|
5396
|
+
return [];
|
|
5397
|
+
}
|
|
5398
|
+
return [`--maxWorkers=${Math.max(1, Math.floor(input.cores / input.lines))}`];
|
|
5399
|
+
}
|
|
3826
5400
|
var REACHES = [
|
|
3827
5401
|
"process.",
|
|
3828
5402
|
"process?.",
|
|
@@ -3854,10 +5428,22 @@ var REACHES = [
|
|
|
3854
5428
|
];
|
|
3855
5429
|
var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
|
|
3856
5430
|
var COMPUTED = "loads a module whose name is computed";
|
|
5431
|
+
var STRING = /(^|[:,([={?+&|;])(\s*)(['"])((?:\\.|(?!\3)[^\\\n])*)\3(?=\s*(?:[,}\]);:+?&|]|$))/gm;
|
|
5432
|
+
function withoutStrings(source) {
|
|
5433
|
+
return source.replace(
|
|
5434
|
+
STRING,
|
|
5435
|
+
(all, before, space, quote, inside) => (
|
|
5436
|
+
// In a template (`{"home": "${home()}"}`) the quotes are text, and what is
|
|
5437
|
+
// between them may be code.
|
|
5438
|
+
inside.includes("${") ? all : `${before}${space}${quote}${" ".repeat(inside.length)}${quote}`
|
|
5439
|
+
)
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
3857
5442
|
var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
|
|
3858
5443
|
function reachesOutside(file, trusted) {
|
|
3859
5444
|
const dense = file.source.replace(/\s/g, "");
|
|
3860
|
-
const
|
|
5445
|
+
const code = withoutStrings(file.source).replace(/\s/g, "");
|
|
5446
|
+
const mark = REACHES.find((text) => code.includes(text));
|
|
3861
5447
|
if (mark !== void 0) {
|
|
3862
5448
|
return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
|
|
3863
5449
|
}
|
|
@@ -4000,8 +5586,8 @@ function cleanHold(input) {
|
|
|
4000
5586
|
|
|
4001
5587
|
// src/touch.ts
|
|
4002
5588
|
import { createHash as createHash2 } from "crypto";
|
|
4003
|
-
import
|
|
4004
|
-
import
|
|
5589
|
+
import fs13 from "fs";
|
|
5590
|
+
import path13 from "path";
|
|
4005
5591
|
import { fileURLToPath } from "url";
|
|
4006
5592
|
var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
|
|
4007
5593
|
function isObject(value) {
|
|
@@ -4035,7 +5621,7 @@ function isCounters(value) {
|
|
|
4035
5621
|
function readNotes(directory) {
|
|
4036
5622
|
const read2 = (file) => {
|
|
4037
5623
|
try {
|
|
4038
|
-
return JSON.parse(
|
|
5624
|
+
return JSON.parse(fs13.readFileSync(path13.join(directory, file), "utf8"));
|
|
4039
5625
|
} catch {
|
|
4040
5626
|
return null;
|
|
4041
5627
|
}
|
|
@@ -4043,8 +5629,8 @@ function readNotes(directory) {
|
|
|
4043
5629
|
const run = read2("run.json");
|
|
4044
5630
|
let notes = [];
|
|
4045
5631
|
try {
|
|
4046
|
-
for (const name of
|
|
4047
|
-
const note = read2(
|
|
5632
|
+
for (const name of fs13.readdirSync(path13.join(directory, "specs"))) {
|
|
5633
|
+
const note = read2(path13.join("specs", name));
|
|
4048
5634
|
if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
|
|
4049
5635
|
throw new Error("not a note");
|
|
4050
5636
|
}
|
|
@@ -4056,7 +5642,7 @@ function readNotes(directory) {
|
|
|
4056
5642
|
return {
|
|
4057
5643
|
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,
|
|
4058
5644
|
notes,
|
|
4059
|
-
totals: countersOfAll(read2(
|
|
5645
|
+
totals: countersOfAll(read2(path13.join("coverage", "coverage-final.json")))
|
|
4060
5646
|
};
|
|
4061
5647
|
}
|
|
4062
5648
|
|
|
@@ -4071,7 +5657,7 @@ function resolveGateCommand(manifest) {
|
|
|
4071
5657
|
if (manifest.settings.gateCommand !== null) {
|
|
4072
5658
|
return manifest.settings.gateCommand;
|
|
4073
5659
|
}
|
|
4074
|
-
const exists = (name) =>
|
|
5660
|
+
const exists = (name) => fs14.existsSync(path14.join(manifest.dir, name));
|
|
4075
5661
|
const jest = JEST_CONFIGS.find(exists);
|
|
4076
5662
|
const vitest = VITEST_CONFIGS.find(exists);
|
|
4077
5663
|
if (jest && vitest) {
|
|
@@ -4092,7 +5678,7 @@ function resolveGateCommand(manifest) {
|
|
|
4092
5678
|
var TOUCH_PREFIX = ".test-gates-touch-";
|
|
4093
5679
|
function realPath(file) {
|
|
4094
5680
|
try {
|
|
4095
|
-
return
|
|
5681
|
+
return fs14.realpathSync(file);
|
|
4096
5682
|
} catch {
|
|
4097
5683
|
return file;
|
|
4098
5684
|
}
|
|
@@ -4100,13 +5686,16 @@ function realPath(file) {
|
|
|
4100
5686
|
function shareOneRun(manifest, targets, runGate) {
|
|
4101
5687
|
let touchDir;
|
|
4102
5688
|
try {
|
|
4103
|
-
for (const name of
|
|
4104
|
-
const left =
|
|
4105
|
-
|
|
4106
|
-
|
|
5689
|
+
for (const name of fs14.readdirSync(manifest.dir)) {
|
|
5690
|
+
const left = path14.join(manifest.dir, name);
|
|
5691
|
+
try {
|
|
5692
|
+
if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
|
|
5693
|
+
fs14.rmSync(left, { recursive: true, force: true });
|
|
5694
|
+
}
|
|
5695
|
+
} catch {
|
|
4107
5696
|
}
|
|
4108
5697
|
}
|
|
4109
|
-
touchDir =
|
|
5698
|
+
touchDir = fs14.mkdtempSync(path14.join(manifest.dir, TOUCH_PREFIX));
|
|
4110
5699
|
} catch {
|
|
4111
5700
|
return { reason: "the subproject cannot be written to" };
|
|
4112
5701
|
}
|
|
@@ -4117,14 +5706,14 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4117
5706
|
return { reason: "the gate run with every spec does not pass" };
|
|
4118
5707
|
}
|
|
4119
5708
|
const base = realPath(manifest.dir);
|
|
4120
|
-
const absolute = (file) => realPath(
|
|
5709
|
+
const absolute = (file) => realPath(path14.resolve(base, file));
|
|
4121
5710
|
const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
|
|
4122
5711
|
const left = readNotes(touchDir);
|
|
4123
5712
|
plan = planControls({
|
|
4124
5713
|
all: manifest.gates.map((gate) => ({
|
|
4125
5714
|
path: gate.path,
|
|
4126
|
-
file:
|
|
4127
|
-
specFile:
|
|
5715
|
+
file: path14.join(base, gate.path),
|
|
5716
|
+
specFile: path14.join(base, gate.spec ?? "")
|
|
4128
5717
|
})),
|
|
4129
5718
|
targets: targets.map((gate) => gate.path),
|
|
4130
5719
|
run: left.run,
|
|
@@ -4133,7 +5722,7 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4133
5722
|
failurePattern: manifest.settings.selfcheckFailurePattern
|
|
4134
5723
|
});
|
|
4135
5724
|
} finally {
|
|
4136
|
-
|
|
5725
|
+
fs14.rmSync(touchDir, { recursive: true, force: true });
|
|
4137
5726
|
}
|
|
4138
5727
|
if (plan.reason !== null) {
|
|
4139
5728
|
return { reason: plan.reason };
|
|
@@ -4143,15 +5732,15 @@ function shareOneRun(manifest, targets, runGate) {
|
|
|
4143
5732
|
return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
|
|
4144
5733
|
}
|
|
4145
5734
|
function installedVersion2(start, name) {
|
|
4146
|
-
let current =
|
|
5735
|
+
let current = path14.resolve(start);
|
|
4147
5736
|
for (; ; ) {
|
|
4148
5737
|
try {
|
|
4149
5738
|
const manifest = JSON.parse(
|
|
4150
|
-
|
|
5739
|
+
fs14.readFileSync(path14.join(current, "node_modules", name, "package.json"), "utf8")
|
|
4151
5740
|
);
|
|
4152
5741
|
return typeof manifest.version === "string" ? manifest.version : null;
|
|
4153
5742
|
} catch {
|
|
4154
|
-
const parent =
|
|
5743
|
+
const parent = path14.dirname(current);
|
|
4155
5744
|
if (parent === current) {
|
|
4156
5745
|
return null;
|
|
4157
5746
|
}
|
|
@@ -4180,24 +5769,39 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
4180
5769
|
const results = /* @__PURE__ */ new Map();
|
|
4181
5770
|
let root;
|
|
4182
5771
|
try {
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
5772
|
+
root = fs14.realpathSync(fs14.mkdtempSync(path14.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
5773
|
+
} catch {
|
|
5774
|
+
return results;
|
|
5775
|
+
}
|
|
5776
|
+
try {
|
|
5777
|
+
for (const name of fs14.readdirSync(os2.tmpdir())) {
|
|
5778
|
+
const left = path14.join(os2.tmpdir(), name);
|
|
5779
|
+
try {
|
|
5780
|
+
if (name.startsWith(JOBS_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
|
|
5781
|
+
fs14.rmSync(left, { recursive: true, force: true });
|
|
5782
|
+
}
|
|
5783
|
+
} catch {
|
|
4187
5784
|
}
|
|
4188
5785
|
}
|
|
4189
|
-
root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
|
|
4190
5786
|
} catch {
|
|
4191
|
-
return results;
|
|
4192
5787
|
}
|
|
4193
5788
|
const queue = [...gates];
|
|
5789
|
+
const lines = Math.min(jobs, gates.length);
|
|
5790
|
+
const workers = workerFlags({
|
|
5791
|
+
runner,
|
|
5792
|
+
args,
|
|
5793
|
+
configSource: gateConfigSource(manifest, args),
|
|
5794
|
+
lines,
|
|
5795
|
+
// (os.availableParallelism is in Node 18.14 and later.)
|
|
5796
|
+
cores: os2.availableParallelism?.() ?? os2.cpus().length
|
|
5797
|
+
});
|
|
4194
5798
|
const line = async (dir) => {
|
|
4195
5799
|
for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
|
|
4196
5800
|
const spec = gate.spec;
|
|
4197
5801
|
results.set(
|
|
4198
5802
|
gate,
|
|
4199
5803
|
await new Promise((resolve) => {
|
|
4200
|
-
const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
|
|
5804
|
+
const child = spawn(executable, [...args, ...jobFlags(runner, dir), ...workers], {
|
|
4201
5805
|
cwd: manifest.dir,
|
|
4202
5806
|
env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
|
|
4203
5807
|
shell: process.platform === "win32"
|
|
@@ -4216,13 +5820,10 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
|
|
|
4216
5820
|
};
|
|
4217
5821
|
try {
|
|
4218
5822
|
await Promise.all(
|
|
4219
|
-
Array.from(
|
|
4220
|
-
{ length: Math.min(jobs, gates.length) },
|
|
4221
|
-
(_unused, index) => line(path12.join(root, `line-${index}`))
|
|
4222
|
-
)
|
|
5823
|
+
Array.from({ length: lines }, (_unused, index) => line(path14.join(root, `line-${index}`)))
|
|
4223
5824
|
);
|
|
4224
5825
|
} finally {
|
|
4225
|
-
|
|
5826
|
+
fs14.rmSync(root, { recursive: true, force: true });
|
|
4226
5827
|
}
|
|
4227
5828
|
return results;
|
|
4228
5829
|
}
|
|
@@ -4251,7 +5852,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
4251
5852
|
const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
|
|
4252
5853
|
const [program, ...args] = command;
|
|
4253
5854
|
const executable = findLocalBin(manifest.dir, program) ?? program;
|
|
4254
|
-
const runGate = (env) =>
|
|
5855
|
+
const runGate = (env) => spawnSync6(executable, args, {
|
|
4255
5856
|
cwd: manifest.dir,
|
|
4256
5857
|
env: {
|
|
4257
5858
|
...process.env,
|
|
@@ -4329,6 +5930,11 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
4329
5930
|
...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
|
|
4330
5931
|
].sort((left, right) => right.length - left.length);
|
|
4331
5932
|
const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
|
|
5933
|
+
if (results.size === 0) {
|
|
5934
|
+
io2.out(
|
|
5935
|
+
`test-gates selfcheck: one negative control at a time (no directory for the runs next to others could be made in ${os2.tmpdir()})`
|
|
5936
|
+
);
|
|
5937
|
+
}
|
|
4332
5938
|
for (const [gate, result] of results) {
|
|
4333
5939
|
const output = `${result.stdout}${result.stderr}`;
|
|
4334
5940
|
const clean = cleanHold({
|
|
@@ -4346,7 +5952,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
|
|
|
4346
5952
|
}
|
|
4347
5953
|
}
|
|
4348
5954
|
const again = atOnce.length - heldAtOnce.size;
|
|
4349
|
-
if (again > 0) {
|
|
5955
|
+
if (again > 0 && results.size > 0) {
|
|
4350
5956
|
io2.out(
|
|
4351
5957
|
`test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
|
|
4352
5958
|
);
|