@t09tanaka/test-gates 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +194 -20
- package/dist/{chunk-LMD3ANJ2.js → chunk-F7WBEFIL.js} +53 -4
- package/dist/cli.js +368 -133
- package/dist/index.cjs +274 -81
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +222 -78
- package/dist/jest.cjs +53 -4
- package/dist/jest.js +1 -1
- package/dist/stryker.cjs +53 -4
- package/dist/stryker.js +1 -1
- package/dist/vitest.cjs +55 -5
- package/dist/vitest.d.cts +6 -0
- package/dist/vitest.d.ts +6 -0
- package/dist/vitest.js +3 -2
- package/package.json +9 -3
- package/schema/test-gates.schema.json +165 -28
package/dist/index.cjs
CHANGED
|
@@ -92,17 +92,17 @@ function allowanceProblems(entry) {
|
|
|
92
92
|
}
|
|
93
93
|
return problems;
|
|
94
94
|
}
|
|
95
|
-
function validateEquivalentMutants(gatePath, value) {
|
|
95
|
+
function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
|
|
96
96
|
if (value === void 0) {
|
|
97
97
|
return [];
|
|
98
98
|
}
|
|
99
99
|
if (!Array.isArray(value)) {
|
|
100
|
-
return [{ file: MANIFEST_FILE, message: `${gatePath}:
|
|
100
|
+
return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
|
|
101
101
|
}
|
|
102
102
|
return value.flatMap(
|
|
103
103
|
(entry, index) => allowanceProblems(entry).map((problem) => ({
|
|
104
104
|
file: MANIFEST_FILE,
|
|
105
|
-
message: `${gatePath}:
|
|
105
|
+
message: `${gatePath}: ${field}[${index}]${problem}`
|
|
106
106
|
}))
|
|
107
107
|
);
|
|
108
108
|
}
|
|
@@ -185,6 +185,9 @@ function parseManifest(text) {
|
|
|
185
185
|
}
|
|
186
186
|
for (const gate of gates) {
|
|
187
187
|
violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
|
|
188
|
+
violations.push(
|
|
189
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
190
|
+
);
|
|
188
191
|
}
|
|
189
192
|
return { gates, candidates, settings: raw.settings, violations };
|
|
190
193
|
}
|
|
@@ -233,8 +236,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
233
236
|
{
|
|
234
237
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
235
238
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
242
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
236
243
|
}
|
|
237
244
|
];
|
|
245
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
238
246
|
var SettingsError = class extends Error {
|
|
239
247
|
};
|
|
240
248
|
function isRecord2(value) {
|
|
@@ -334,7 +342,9 @@ function resolveSettings(raw) {
|
|
|
334
342
|
"gateCommand",
|
|
335
343
|
"selfcheck",
|
|
336
344
|
"stryker",
|
|
337
|
-
"lcov"
|
|
345
|
+
"lcov",
|
|
346
|
+
"imports",
|
|
347
|
+
"mutation"
|
|
338
348
|
],
|
|
339
349
|
"settings"
|
|
340
350
|
);
|
|
@@ -435,6 +445,42 @@ function resolveSettings(raw) {
|
|
|
435
445
|
"settings.selfcheck.failurePattern"
|
|
436
446
|
);
|
|
437
447
|
}
|
|
448
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
449
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
450
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
451
|
+
}
|
|
452
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
453
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
454
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
455
|
+
}
|
|
456
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
457
|
+
const at = `settings.imports.allow[${index}]`;
|
|
458
|
+
if (typeof item === "string" && item !== "") {
|
|
459
|
+
return { module: item };
|
|
460
|
+
}
|
|
461
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
462
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
463
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
464
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
465
|
+
}
|
|
466
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
467
|
+
}
|
|
468
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
469
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
470
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
471
|
+
if (names.length === 0) {
|
|
472
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
473
|
+
}
|
|
474
|
+
return { module: item.module, names };
|
|
475
|
+
}
|
|
476
|
+
throw new SettingsError(
|
|
477
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
481
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
482
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
483
|
+
}
|
|
438
484
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
439
485
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
440
486
|
return {
|
|
@@ -456,6 +502,9 @@ function resolveSettings(raw) {
|
|
|
456
502
|
selfcheckFailurePattern,
|
|
457
503
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
458
504
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
505
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
506
|
+
importAllow,
|
|
507
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
459
508
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
460
509
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
461
510
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -650,6 +699,70 @@ function parseImports(source) {
|
|
|
650
699
|
}
|
|
651
700
|
return found.sort((a, b) => a.line - b.line);
|
|
652
701
|
}
|
|
702
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
|
|
703
|
+
function findUncheckableImports(source) {
|
|
704
|
+
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/core/import-policy.ts
|
|
708
|
+
var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
|
|
709
|
+
function isProjectImport(specifier, aliases) {
|
|
710
|
+
return specifier.startsWith(".") || aliases.some((alias) => specifier.startsWith(alias.prefix));
|
|
711
|
+
}
|
|
712
|
+
function resolveSpecifier(specifier, fromFile, aliases) {
|
|
713
|
+
let joined = null;
|
|
714
|
+
if (specifier.startsWith(".")) {
|
|
715
|
+
const slash = fromFile.lastIndexOf("/");
|
|
716
|
+
joined = slash === -1 ? specifier : `${fromFile.slice(0, slash)}/${specifier}`;
|
|
717
|
+
} else {
|
|
718
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
719
|
+
if (alias) {
|
|
720
|
+
joined = alias.target + specifier.slice(alias.prefix.length);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (joined === null) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const segments = [];
|
|
727
|
+
for (const segment of joined.split("/")) {
|
|
728
|
+
if (segment === "..") {
|
|
729
|
+
segments.pop();
|
|
730
|
+
} else if (segment !== "." && segment !== "") {
|
|
731
|
+
segments.push(segment);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return segments.join("/").replace(SCRIPT_EXTENSION, "");
|
|
735
|
+
}
|
|
736
|
+
function matches(rule, specifier) {
|
|
737
|
+
if (rule.regex) {
|
|
738
|
+
return rule.regex.test(specifier);
|
|
739
|
+
}
|
|
740
|
+
return specifier === rule.module || specifier.startsWith(`${rule.module}/`);
|
|
741
|
+
}
|
|
742
|
+
function judgePackageImport(ref, allow) {
|
|
743
|
+
const rules = allow.filter((rule) => matches(rule, ref.module));
|
|
744
|
+
if (rules.length === 0) {
|
|
745
|
+
return `runtime import of "${ref.module}" is not in settings.imports.allow (allowlist mode allows relative imports, import aliases and listed packages only)`;
|
|
746
|
+
}
|
|
747
|
+
if (rules.some((rule) => rule.names === void 0)) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const allowedNames = rules.flatMap((rule) => rule.names);
|
|
751
|
+
if (ref.whole) {
|
|
752
|
+
return `only ${allowedNames.join(", ")} may be imported from "${ref.module}", as named imports (settings.imports.allow)`;
|
|
753
|
+
}
|
|
754
|
+
const extra = ref.names.filter((name) => !allowedNames.includes(name));
|
|
755
|
+
return extra.length === 0 ? null : `runtime import of ${extra.join(", ")} from "${ref.module}" is not in settings.imports.allow (allowed: ${allowedNames.join(", ")})`;
|
|
756
|
+
}
|
|
757
|
+
function judgeProjectImport(target, impurePaths, gateExtensions) {
|
|
758
|
+
const candidates = [target, ...gateExtensions.map((extension) => target + extension)];
|
|
759
|
+
for (const rule of impurePaths) {
|
|
760
|
+
if (candidates.some((candidate) => rule.regex.test(candidate))) {
|
|
761
|
+
return rule.reason;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
653
766
|
|
|
654
767
|
// src/core/scan.ts
|
|
655
768
|
var IGNORE_DIRECTIVE = /\b(istanbul|c8|v8)\s+ignore\b/;
|
|
@@ -667,7 +780,7 @@ var DISABLED_TESTS = [
|
|
|
667
780
|
}
|
|
668
781
|
];
|
|
669
782
|
var MODULE_MOCK = /\b(?:jest|vi)\s*\.\s*(?:mock|doMock|unstable_mockModule|setMock)\s*\(\s*(?:import\s*\(\s*)?(['"`])([^'"`\n]+)\1/g;
|
|
670
|
-
var
|
|
783
|
+
var SCRIPT_EXTENSION2 = /\.[cm]?[jt]sx?$/;
|
|
671
784
|
function eachLine(source, visit) {
|
|
672
785
|
source.split("\n").forEach((line, index) => visit(line, index + 1));
|
|
673
786
|
}
|
|
@@ -704,6 +817,7 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
704
817
|
if (imported.typeOnly) {
|
|
705
818
|
continue;
|
|
706
819
|
}
|
|
820
|
+
const before = violations.length;
|
|
707
821
|
if (!settings.allowedImports.some((regex) => regex.test(imported.module))) {
|
|
708
822
|
for (const rule of settings.impureImports) {
|
|
709
823
|
if (rule.regex.test(imported.module)) {
|
|
@@ -730,34 +844,31 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
730
844
|
});
|
|
731
845
|
}
|
|
732
846
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
break;
|
|
847
|
+
if (settings.importMode === "allowlist" && violations.length === before) {
|
|
848
|
+
const reason = isProjectImport(imported.module, settings.importAliases) ? judgeProjectImport(
|
|
849
|
+
resolveSpecifier(imported.module, gatePath, settings.importAliases),
|
|
850
|
+
settings.impurePaths,
|
|
851
|
+
settings.gateExtensions
|
|
852
|
+
) : judgePackageImport(imported, settings.importAllow);
|
|
853
|
+
if (reason !== null) {
|
|
854
|
+
violations.push({
|
|
855
|
+
file: gatePath,
|
|
856
|
+
line: imported.line,
|
|
857
|
+
message: isProjectImport(imported.module, settings.importAliases) ? `runtime import of "${imported.module}" (${reason})` : reason
|
|
858
|
+
});
|
|
746
859
|
}
|
|
747
860
|
}
|
|
748
861
|
}
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
} else if (segment !== "." && segment !== "") {
|
|
757
|
-
segments.push(segment);
|
|
862
|
+
if (settings.importMode === "allowlist") {
|
|
863
|
+
for (const line of findUncheckableImports(source)) {
|
|
864
|
+
violations.push({
|
|
865
|
+
file: gatePath,
|
|
866
|
+
line,
|
|
867
|
+
message: "require() / import() with a computed specifier cannot be checked (allowlist mode)"
|
|
868
|
+
});
|
|
758
869
|
}
|
|
759
870
|
}
|
|
760
|
-
return
|
|
871
|
+
return violations.sort((a, b) => a.line - b.line);
|
|
761
872
|
}
|
|
762
873
|
function scanSpecSource(gatePath, specPath, source, settings) {
|
|
763
874
|
const violations = [];
|
|
@@ -775,7 +886,7 @@ function scanSpecSource(gatePath, specPath, source, settings) {
|
|
|
775
886
|
}
|
|
776
887
|
}
|
|
777
888
|
});
|
|
778
|
-
const gateModule = gatePath.replace(
|
|
889
|
+
const gateModule = gatePath.replace(SCRIPT_EXTENSION2, "");
|
|
779
890
|
const gateDirectory = gateModule.replace(/\/index$/, "");
|
|
780
891
|
for (const match of source.matchAll(MODULE_MOCK)) {
|
|
781
892
|
const specifier = match[2];
|
|
@@ -812,6 +923,21 @@ function scanStrykerConfig(configPath, source) {
|
|
|
812
923
|
return violations;
|
|
813
924
|
}
|
|
814
925
|
|
|
926
|
+
// src/core/vitest-coverage.ts
|
|
927
|
+
function vitestCoverageViolation(input) {
|
|
928
|
+
if (input.vitestVersion === null || input.vitestVersion === "") {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
const major = Number(input.vitestVersion.split(".")[0]);
|
|
932
|
+
if (!Number.isInteger(major) || major >= 4) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
if (/\bistanbul\b/.test(input.configSource) || /\bexperimentalAstAwareRemapping\s*:\s*true\b/.test(input.configSource)) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
return `Vitest ${input.vitestVersion} with the v8 coverage provider does not count the skipped side of an "if" without "else", so a gate can show 100% branches with that side untested. Use coverageProvider: 'istanbul' (needs @vitest/coverage-istanbul), set coverage.experimentalAstAwareRemapping: true, or move to Vitest 4`;
|
|
939
|
+
}
|
|
940
|
+
|
|
815
941
|
// src/commands/check.ts
|
|
816
942
|
var STRYKER_CONFIG_NAMES = [
|
|
817
943
|
"stryker.gates.config.mjs",
|
|
@@ -902,8 +1028,40 @@ function checkGates(manifest) {
|
|
|
902
1028
|
violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
|
|
903
1029
|
}
|
|
904
1030
|
}
|
|
1031
|
+
violations.push(...checkVitestCoverage(manifest));
|
|
905
1032
|
return violations;
|
|
906
1033
|
}
|
|
1034
|
+
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
1035
|
+
(extension) => `vitest.gates.config.${extension}`
|
|
1036
|
+
);
|
|
1037
|
+
function installedVitestVersion(startDir) {
|
|
1038
|
+
let current = startDir;
|
|
1039
|
+
for (; ; ) {
|
|
1040
|
+
try {
|
|
1041
|
+
const pkg = JSON.parse(
|
|
1042
|
+
import_node_fs3.default.readFileSync(import_node_path3.default.join(current, "node_modules", "vitest", "package.json"), "utf8")
|
|
1043
|
+
);
|
|
1044
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
1045
|
+
} catch {
|
|
1046
|
+
const parent = import_node_path3.default.dirname(current);
|
|
1047
|
+
if (parent === current) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
current = parent;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function checkVitestCoverage(manifest) {
|
|
1055
|
+
const config = VITEST_GATE_CONFIGS.find((name) => import_node_fs3.default.existsSync(import_node_path3.default.join(manifest.dir, name)));
|
|
1056
|
+
if (!config) {
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1059
|
+
const message = vitestCoverageViolation({
|
|
1060
|
+
vitestVersion: installedVitestVersion(manifest.dir),
|
|
1061
|
+
configSource: readText(manifest.dir, config)
|
|
1062
|
+
});
|
|
1063
|
+
return message === null ? [] : [{ file: config, message }];
|
|
1064
|
+
}
|
|
907
1065
|
|
|
908
1066
|
// src/core/mutation-result.ts
|
|
909
1067
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -938,6 +1096,27 @@ function sliceByLocation(source, location) {
|
|
|
938
1096
|
function keyOf(mutator, original, replacement) {
|
|
939
1097
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
940
1098
|
}
|
|
1099
|
+
function describeMutant(file, entry) {
|
|
1100
|
+
const { mutant } = entry;
|
|
1101
|
+
return {
|
|
1102
|
+
file,
|
|
1103
|
+
line: mutant.location.start.line,
|
|
1104
|
+
column: mutant.location.start.column,
|
|
1105
|
+
status: mutant.status,
|
|
1106
|
+
mutator: mutant.mutatorName,
|
|
1107
|
+
original: normalizeCode(entry.original),
|
|
1108
|
+
replacement: normalizeCode(entry.replacement),
|
|
1109
|
+
occurrence: entry.occurrence,
|
|
1110
|
+
sameKeyCount: entry.sameKeyCount,
|
|
1111
|
+
allowance: {
|
|
1112
|
+
mutator: mutant.mutatorName,
|
|
1113
|
+
original: normalizeCode(entry.original),
|
|
1114
|
+
replacement: normalizeCode(entry.replacement),
|
|
1115
|
+
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
1116
|
+
reason: ""
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
941
1120
|
function describeAllowance(entry) {
|
|
942
1121
|
const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
|
|
943
1122
|
return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
|
|
@@ -945,13 +1124,15 @@ function describeAllowance(entry) {
|
|
|
945
1124
|
function judgeMutationReport(input) {
|
|
946
1125
|
const violations = [];
|
|
947
1126
|
const survivors = [];
|
|
1127
|
+
const unexpectedTimeouts = [];
|
|
948
1128
|
const summary = {
|
|
949
1129
|
total: 0,
|
|
950
1130
|
detected: 0,
|
|
951
1131
|
timeout: 0,
|
|
952
1132
|
allowed: 0,
|
|
953
1133
|
notEvaluable: 0,
|
|
954
|
-
unallowed: 0
|
|
1134
|
+
unallowed: 0,
|
|
1135
|
+
expectedTimeout: 0
|
|
955
1136
|
};
|
|
956
1137
|
const files = /* @__PURE__ */ new Map();
|
|
957
1138
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
@@ -961,7 +1142,9 @@ function judgeMutationReport(input) {
|
|
|
961
1142
|
const gatePath = normalizePath(gate.path);
|
|
962
1143
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
963
1144
|
violations.push(...shapeViolations);
|
|
964
|
-
|
|
1145
|
+
violations.push(
|
|
1146
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
1147
|
+
);
|
|
965
1148
|
const result = files.get(gatePath);
|
|
966
1149
|
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
967
1150
|
violations.push({
|
|
@@ -1025,38 +1208,53 @@ function judgeMutationReport(input) {
|
|
|
1025
1208
|
if (!locationsValid) {
|
|
1026
1209
|
continue;
|
|
1027
1210
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
if (!
|
|
1031
|
-
return;
|
|
1211
|
+
const claim = (field, entries, noun, accept) => {
|
|
1212
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1213
|
+
if (!Array.isArray(entries)) {
|
|
1214
|
+
return claimed;
|
|
1032
1215
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1216
|
+
entries.forEach((entry, index) => {
|
|
1217
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
1221
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
1222
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
1223
|
+
violations.push({
|
|
1224
|
+
file: MANIFEST_FILE,
|
|
1225
|
+
message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
1226
|
+
});
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const target = group[(entry.occurrence ?? 1) - 1];
|
|
1230
|
+
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
1231
|
+
if (!target || refusal !== null) {
|
|
1232
|
+
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (claimed.has(target)) {
|
|
1236
|
+
violations.push({
|
|
1237
|
+
file: MANIFEST_FILE,
|
|
1238
|
+
message: `${at}: another ${noun} already covers this mutant`
|
|
1239
|
+
});
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
claimed.add(target);
|
|
1243
|
+
});
|
|
1244
|
+
return claimed;
|
|
1245
|
+
};
|
|
1246
|
+
const allowed = claim(
|
|
1247
|
+
"equivalentMutants",
|
|
1248
|
+
gate.equivalentMutants,
|
|
1249
|
+
"allowance",
|
|
1250
|
+
(status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
|
|
1251
|
+
);
|
|
1252
|
+
const expected = claim(
|
|
1253
|
+
"expectedTimeouts",
|
|
1254
|
+
gate.expectedTimeouts,
|
|
1255
|
+
"entry",
|
|
1256
|
+
(status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
|
|
1257
|
+
);
|
|
1060
1258
|
for (const entry of located) {
|
|
1061
1259
|
const { mutant } = entry;
|
|
1062
1260
|
summary.total += 1;
|
|
@@ -1064,6 +1262,11 @@ function judgeMutationReport(input) {
|
|
|
1064
1262
|
summary.detected += 1;
|
|
1065
1263
|
if (mutant.status === "Timeout") {
|
|
1066
1264
|
summary.timeout += 1;
|
|
1265
|
+
if (expected.has(entry)) {
|
|
1266
|
+
summary.expectedTimeout += 1;
|
|
1267
|
+
} else {
|
|
1268
|
+
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
1269
|
+
}
|
|
1067
1270
|
}
|
|
1068
1271
|
} else if (NOT_EVALUABLE.has(mutant.status)) {
|
|
1069
1272
|
summary.notEvaluable += 1;
|
|
@@ -1073,24 +1276,7 @@ function judgeMutationReport(input) {
|
|
|
1073
1276
|
continue;
|
|
1074
1277
|
}
|
|
1075
1278
|
summary.unallowed += 1;
|
|
1076
|
-
survivors.push(
|
|
1077
|
-
file: gate.path,
|
|
1078
|
-
line: mutant.location.start.line,
|
|
1079
|
-
column: mutant.location.start.column,
|
|
1080
|
-
status: mutant.status,
|
|
1081
|
-
mutator: mutant.mutatorName,
|
|
1082
|
-
original: normalizeCode(entry.original),
|
|
1083
|
-
replacement: normalizeCode(entry.replacement),
|
|
1084
|
-
occurrence: entry.occurrence,
|
|
1085
|
-
sameKeyCount: entry.sameKeyCount,
|
|
1086
|
-
allowance: {
|
|
1087
|
-
mutator: mutant.mutatorName,
|
|
1088
|
-
original: normalizeCode(entry.original),
|
|
1089
|
-
replacement: normalizeCode(entry.replacement),
|
|
1090
|
-
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
1091
|
-
reason: ""
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1279
|
+
survivors.push(describeMutant(gate.path, entry));
|
|
1094
1280
|
} else {
|
|
1095
1281
|
violations.push({
|
|
1096
1282
|
file: gate.path,
|
|
@@ -1106,7 +1292,14 @@ function judgeMutationReport(input) {
|
|
|
1106
1292
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
1107
1293
|
});
|
|
1108
1294
|
}
|
|
1109
|
-
|
|
1295
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
1296
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
1297
|
+
violations.push({
|
|
1298
|
+
file: MANIFEST_FILE,
|
|
1299
|
+
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
return { violations, survivors, unexpectedTimeouts, summary };
|
|
1110
1303
|
}
|
|
1111
1304
|
function formatSurvivor(survivor) {
|
|
1112
1305
|
const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
|
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,11 @@ interface Gate {
|
|
|
12
12
|
decides: string;
|
|
13
13
|
impact: string;
|
|
14
14
|
equivalentMutants?: EquivalentMutant[];
|
|
15
|
+
/**
|
|
16
|
+
* Mutants that always time out (a loop that never ends). Same key as `equivalentMutants`.
|
|
17
|
+
* They are not counted against `settings.mutation.maxTimeouts`.
|
|
18
|
+
*/
|
|
19
|
+
expectedTimeouts?: EquivalentMutant[];
|
|
15
20
|
}
|
|
16
21
|
interface Candidate {
|
|
17
22
|
path: string;
|
|
@@ -75,6 +80,21 @@ interface Settings {
|
|
|
75
80
|
file?: string;
|
|
76
81
|
summaryExclude?: string[];
|
|
77
82
|
};
|
|
83
|
+
/** Since 0.2.0. */
|
|
84
|
+
imports?: {
|
|
85
|
+
mode?: 'blocklist' | 'allowlist';
|
|
86
|
+
allow?: (string | {
|
|
87
|
+
pattern: string;
|
|
88
|
+
flags?: string;
|
|
89
|
+
} | {
|
|
90
|
+
module: string;
|
|
91
|
+
names: string[];
|
|
92
|
+
})[];
|
|
93
|
+
};
|
|
94
|
+
/** Since 0.2.0. */
|
|
95
|
+
mutation?: {
|
|
96
|
+
maxTimeouts?: number;
|
|
97
|
+
};
|
|
78
98
|
}
|
|
79
99
|
interface Manifest {
|
|
80
100
|
$schema?: string;
|
|
@@ -108,6 +128,14 @@ interface ResolvedSettings {
|
|
|
108
128
|
strykerReportFile: string;
|
|
109
129
|
lcovFile: string | null;
|
|
110
130
|
lcovSummaryExclude: RegExp[];
|
|
131
|
+
importMode: 'blocklist' | 'allowlist';
|
|
132
|
+
importAllow: {
|
|
133
|
+
module?: string;
|
|
134
|
+
regex?: RegExp;
|
|
135
|
+
names?: string[];
|
|
136
|
+
}[];
|
|
137
|
+
/** Timeouts tolerated outside `expectedTimeouts`. */
|
|
138
|
+
maxTimeouts: number;
|
|
111
139
|
}
|
|
112
140
|
/** One finding. Printed as `file:line: message`. */
|
|
113
141
|
interface Violation {
|
|
@@ -244,10 +272,17 @@ interface MutationSummary {
|
|
|
244
272
|
notEvaluable: number;
|
|
245
273
|
/** Survived / NoCoverage mutants with no allowance. */
|
|
246
274
|
unallowed: number;
|
|
275
|
+
/** The part of `timeout` that `expectedTimeouts` entries cover. */
|
|
276
|
+
expectedTimeout: number;
|
|
247
277
|
}
|
|
248
278
|
interface MutationVerdict {
|
|
249
279
|
violations: Violation[];
|
|
250
280
|
survivors: UnallowedSurvivor[];
|
|
281
|
+
/**
|
|
282
|
+
* Timed-out mutants that no `expectedTimeouts` entry covers. When there are more of them
|
|
283
|
+
* than `maxTimeouts`, `violations` says so.
|
|
284
|
+
*/
|
|
285
|
+
unexpectedTimeouts: UnallowedSurvivor[];
|
|
251
286
|
summary: MutationSummary;
|
|
252
287
|
}
|
|
253
288
|
/** Whitespace is not part of the key, so reformatting a gate does not invalidate an allowance. */
|
|
@@ -275,6 +310,11 @@ declare function judgeMutationReport(input: {
|
|
|
275
310
|
gates: Gate[];
|
|
276
311
|
report: MutationReport;
|
|
277
312
|
readSource: (gatePath: string) => string | undefined;
|
|
313
|
+
/**
|
|
314
|
+
* `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
|
|
315
|
+
* tolerated. Absent means 0.
|
|
316
|
+
*/
|
|
317
|
+
maxTimeouts?: number;
|
|
278
318
|
}): MutationVerdict;
|
|
279
319
|
/** `file:line:column / mutator / original → replacement / occurrence n of m`. */
|
|
280
320
|
declare function formatSurvivor(survivor: UnallowedSurvivor): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ interface Gate {
|
|
|
12
12
|
decides: string;
|
|
13
13
|
impact: string;
|
|
14
14
|
equivalentMutants?: EquivalentMutant[];
|
|
15
|
+
/**
|
|
16
|
+
* Mutants that always time out (a loop that never ends). Same key as `equivalentMutants`.
|
|
17
|
+
* They are not counted against `settings.mutation.maxTimeouts`.
|
|
18
|
+
*/
|
|
19
|
+
expectedTimeouts?: EquivalentMutant[];
|
|
15
20
|
}
|
|
16
21
|
interface Candidate {
|
|
17
22
|
path: string;
|
|
@@ -75,6 +80,21 @@ interface Settings {
|
|
|
75
80
|
file?: string;
|
|
76
81
|
summaryExclude?: string[];
|
|
77
82
|
};
|
|
83
|
+
/** Since 0.2.0. */
|
|
84
|
+
imports?: {
|
|
85
|
+
mode?: 'blocklist' | 'allowlist';
|
|
86
|
+
allow?: (string | {
|
|
87
|
+
pattern: string;
|
|
88
|
+
flags?: string;
|
|
89
|
+
} | {
|
|
90
|
+
module: string;
|
|
91
|
+
names: string[];
|
|
92
|
+
})[];
|
|
93
|
+
};
|
|
94
|
+
/** Since 0.2.0. */
|
|
95
|
+
mutation?: {
|
|
96
|
+
maxTimeouts?: number;
|
|
97
|
+
};
|
|
78
98
|
}
|
|
79
99
|
interface Manifest {
|
|
80
100
|
$schema?: string;
|
|
@@ -108,6 +128,14 @@ interface ResolvedSettings {
|
|
|
108
128
|
strykerReportFile: string;
|
|
109
129
|
lcovFile: string | null;
|
|
110
130
|
lcovSummaryExclude: RegExp[];
|
|
131
|
+
importMode: 'blocklist' | 'allowlist';
|
|
132
|
+
importAllow: {
|
|
133
|
+
module?: string;
|
|
134
|
+
regex?: RegExp;
|
|
135
|
+
names?: string[];
|
|
136
|
+
}[];
|
|
137
|
+
/** Timeouts tolerated outside `expectedTimeouts`. */
|
|
138
|
+
maxTimeouts: number;
|
|
111
139
|
}
|
|
112
140
|
/** One finding. Printed as `file:line: message`. */
|
|
113
141
|
interface Violation {
|
|
@@ -244,10 +272,17 @@ interface MutationSummary {
|
|
|
244
272
|
notEvaluable: number;
|
|
245
273
|
/** Survived / NoCoverage mutants with no allowance. */
|
|
246
274
|
unallowed: number;
|
|
275
|
+
/** The part of `timeout` that `expectedTimeouts` entries cover. */
|
|
276
|
+
expectedTimeout: number;
|
|
247
277
|
}
|
|
248
278
|
interface MutationVerdict {
|
|
249
279
|
violations: Violation[];
|
|
250
280
|
survivors: UnallowedSurvivor[];
|
|
281
|
+
/**
|
|
282
|
+
* Timed-out mutants that no `expectedTimeouts` entry covers. When there are more of them
|
|
283
|
+
* than `maxTimeouts`, `violations` says so.
|
|
284
|
+
*/
|
|
285
|
+
unexpectedTimeouts: UnallowedSurvivor[];
|
|
251
286
|
summary: MutationSummary;
|
|
252
287
|
}
|
|
253
288
|
/** Whitespace is not part of the key, so reformatting a gate does not invalidate an allowance. */
|
|
@@ -275,6 +310,11 @@ declare function judgeMutationReport(input: {
|
|
|
275
310
|
gates: Gate[];
|
|
276
311
|
report: MutationReport;
|
|
277
312
|
readSource: (gatePath: string) => string | undefined;
|
|
313
|
+
/**
|
|
314
|
+
* `settings.mutation.maxTimeouts`: how many timeouts outside `expectedTimeouts` are
|
|
315
|
+
* tolerated. Absent means 0.
|
|
316
|
+
*/
|
|
317
|
+
maxTimeouts?: number;
|
|
278
318
|
}): MutationVerdict;
|
|
279
319
|
/** `file:line:column / mutator / original → replacement / occurrence n of m`. */
|
|
280
320
|
declare function formatSurvivor(survivor: UnallowedSurvivor): string;
|