@t09tanaka/test-gates 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +191 -19
- package/dist/{chunk-LMD3ANJ2.js → chunk-XEKQKLKY.js} +48 -4
- package/dist/cli.js +364 -133
- package/dist/index.cjs +225 -81
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +178 -78
- package/dist/jest.cjs +48 -4
- package/dist/jest.js +1 -1
- package/dist/stryker.cjs +48 -4
- package/dist/stryker.js +1 -1
- package/dist/vitest.cjs +50 -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 +164 -27
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
|
}
|
|
@@ -334,7 +337,9 @@ function resolveSettings(raw) {
|
|
|
334
337
|
"gateCommand",
|
|
335
338
|
"selfcheck",
|
|
336
339
|
"stryker",
|
|
337
|
-
"lcov"
|
|
340
|
+
"lcov",
|
|
341
|
+
"imports",
|
|
342
|
+
"mutation"
|
|
338
343
|
],
|
|
339
344
|
"settings"
|
|
340
345
|
);
|
|
@@ -435,6 +440,42 @@ function resolveSettings(raw) {
|
|
|
435
440
|
"settings.selfcheck.failurePattern"
|
|
436
441
|
);
|
|
437
442
|
}
|
|
443
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
444
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
445
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
446
|
+
}
|
|
447
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
448
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
449
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
450
|
+
}
|
|
451
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
452
|
+
const at = `settings.imports.allow[${index}]`;
|
|
453
|
+
if (typeof item === "string" && item !== "") {
|
|
454
|
+
return { module: item };
|
|
455
|
+
}
|
|
456
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
457
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
458
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
459
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
460
|
+
}
|
|
461
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
462
|
+
}
|
|
463
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
464
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
465
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
466
|
+
if (names.length === 0) {
|
|
467
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
468
|
+
}
|
|
469
|
+
return { module: item.module, names };
|
|
470
|
+
}
|
|
471
|
+
throw new SettingsError(
|
|
472
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
476
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
477
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
478
|
+
}
|
|
438
479
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
439
480
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
440
481
|
return {
|
|
@@ -456,6 +497,9 @@ function resolveSettings(raw) {
|
|
|
456
497
|
selfcheckFailurePattern,
|
|
457
498
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
458
499
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
500
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
501
|
+
importAllow,
|
|
502
|
+
maxTimeouts: mutation.maxTimeouts ?? null,
|
|
459
503
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
460
504
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
461
505
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -650,6 +694,70 @@ function parseImports(source) {
|
|
|
650
694
|
}
|
|
651
695
|
return found.sort((a, b) => a.line - b.line);
|
|
652
696
|
}
|
|
697
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
|
|
698
|
+
function findUncheckableImports(source) {
|
|
699
|
+
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/core/import-policy.ts
|
|
703
|
+
var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
|
|
704
|
+
function isProjectImport(specifier, aliases) {
|
|
705
|
+
return specifier.startsWith(".") || aliases.some((alias) => specifier.startsWith(alias.prefix));
|
|
706
|
+
}
|
|
707
|
+
function resolveSpecifier(specifier, fromFile, aliases) {
|
|
708
|
+
let joined = null;
|
|
709
|
+
if (specifier.startsWith(".")) {
|
|
710
|
+
const slash = fromFile.lastIndexOf("/");
|
|
711
|
+
joined = slash === -1 ? specifier : `${fromFile.slice(0, slash)}/${specifier}`;
|
|
712
|
+
} else {
|
|
713
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
714
|
+
if (alias) {
|
|
715
|
+
joined = alias.target + specifier.slice(alias.prefix.length);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (joined === null) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const segments = [];
|
|
722
|
+
for (const segment of joined.split("/")) {
|
|
723
|
+
if (segment === "..") {
|
|
724
|
+
segments.pop();
|
|
725
|
+
} else if (segment !== "." && segment !== "") {
|
|
726
|
+
segments.push(segment);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return segments.join("/").replace(SCRIPT_EXTENSION, "");
|
|
730
|
+
}
|
|
731
|
+
function matches(rule, specifier) {
|
|
732
|
+
if (rule.regex) {
|
|
733
|
+
return rule.regex.test(specifier);
|
|
734
|
+
}
|
|
735
|
+
return specifier === rule.module || specifier.startsWith(`${rule.module}/`);
|
|
736
|
+
}
|
|
737
|
+
function judgePackageImport(ref, allow) {
|
|
738
|
+
const rules = allow.filter((rule) => matches(rule, ref.module));
|
|
739
|
+
if (rules.length === 0) {
|
|
740
|
+
return `runtime import of "${ref.module}" is not in settings.imports.allow (allowlist mode allows relative imports, import aliases and listed packages only)`;
|
|
741
|
+
}
|
|
742
|
+
if (rules.some((rule) => rule.names === void 0)) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const allowedNames = rules.flatMap((rule) => rule.names);
|
|
746
|
+
if (ref.whole) {
|
|
747
|
+
return `only ${allowedNames.join(", ")} may be imported from "${ref.module}", as named imports (settings.imports.allow)`;
|
|
748
|
+
}
|
|
749
|
+
const extra = ref.names.filter((name) => !allowedNames.includes(name));
|
|
750
|
+
return extra.length === 0 ? null : `runtime import of ${extra.join(", ")} from "${ref.module}" is not in settings.imports.allow (allowed: ${allowedNames.join(", ")})`;
|
|
751
|
+
}
|
|
752
|
+
function judgeProjectImport(target, impurePaths, gateExtensions) {
|
|
753
|
+
const candidates = [target, ...gateExtensions.map((extension) => target + extension)];
|
|
754
|
+
for (const rule of impurePaths) {
|
|
755
|
+
if (candidates.some((candidate) => rule.regex.test(candidate))) {
|
|
756
|
+
return rule.reason;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
653
761
|
|
|
654
762
|
// src/core/scan.ts
|
|
655
763
|
var IGNORE_DIRECTIVE = /\b(istanbul|c8|v8)\s+ignore\b/;
|
|
@@ -667,7 +775,7 @@ var DISABLED_TESTS = [
|
|
|
667
775
|
}
|
|
668
776
|
];
|
|
669
777
|
var MODULE_MOCK = /\b(?:jest|vi)\s*\.\s*(?:mock|doMock|unstable_mockModule|setMock)\s*\(\s*(?:import\s*\(\s*)?(['"`])([^'"`\n]+)\1/g;
|
|
670
|
-
var
|
|
778
|
+
var SCRIPT_EXTENSION2 = /\.[cm]?[jt]sx?$/;
|
|
671
779
|
function eachLine(source, visit) {
|
|
672
780
|
source.split("\n").forEach((line, index) => visit(line, index + 1));
|
|
673
781
|
}
|
|
@@ -704,6 +812,7 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
704
812
|
if (imported.typeOnly) {
|
|
705
813
|
continue;
|
|
706
814
|
}
|
|
815
|
+
const before = violations.length;
|
|
707
816
|
if (!settings.allowedImports.some((regex) => regex.test(imported.module))) {
|
|
708
817
|
for (const rule of settings.impureImports) {
|
|
709
818
|
if (rule.regex.test(imported.module)) {
|
|
@@ -730,34 +839,31 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
730
839
|
});
|
|
731
840
|
}
|
|
732
841
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
break;
|
|
842
|
+
if (settings.importMode === "allowlist" && violations.length === before) {
|
|
843
|
+
const reason = isProjectImport(imported.module, settings.importAliases) ? judgeProjectImport(
|
|
844
|
+
resolveSpecifier(imported.module, gatePath, settings.importAliases),
|
|
845
|
+
settings.impurePaths,
|
|
846
|
+
settings.gateExtensions
|
|
847
|
+
) : judgePackageImport(imported, settings.importAllow);
|
|
848
|
+
if (reason !== null) {
|
|
849
|
+
violations.push({
|
|
850
|
+
file: gatePath,
|
|
851
|
+
line: imported.line,
|
|
852
|
+
message: isProjectImport(imported.module, settings.importAliases) ? `runtime import of "${imported.module}" (${reason})` : reason
|
|
853
|
+
});
|
|
746
854
|
}
|
|
747
855
|
}
|
|
748
856
|
}
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
} else if (segment !== "." && segment !== "") {
|
|
757
|
-
segments.push(segment);
|
|
857
|
+
if (settings.importMode === "allowlist") {
|
|
858
|
+
for (const line of findUncheckableImports(source)) {
|
|
859
|
+
violations.push({
|
|
860
|
+
file: gatePath,
|
|
861
|
+
line,
|
|
862
|
+
message: "require() / import() with a computed specifier cannot be checked (allowlist mode)"
|
|
863
|
+
});
|
|
758
864
|
}
|
|
759
865
|
}
|
|
760
|
-
return
|
|
866
|
+
return violations.sort((a, b) => a.line - b.line);
|
|
761
867
|
}
|
|
762
868
|
function scanSpecSource(gatePath, specPath, source, settings) {
|
|
763
869
|
const violations = [];
|
|
@@ -775,7 +881,7 @@ function scanSpecSource(gatePath, specPath, source, settings) {
|
|
|
775
881
|
}
|
|
776
882
|
}
|
|
777
883
|
});
|
|
778
|
-
const gateModule = gatePath.replace(
|
|
884
|
+
const gateModule = gatePath.replace(SCRIPT_EXTENSION2, "");
|
|
779
885
|
const gateDirectory = gateModule.replace(/\/index$/, "");
|
|
780
886
|
for (const match of source.matchAll(MODULE_MOCK)) {
|
|
781
887
|
const specifier = match[2];
|
|
@@ -904,6 +1010,9 @@ function checkGates(manifest) {
|
|
|
904
1010
|
}
|
|
905
1011
|
return violations;
|
|
906
1012
|
}
|
|
1013
|
+
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
1014
|
+
(extension) => `vitest.gates.config.${extension}`
|
|
1015
|
+
);
|
|
907
1016
|
|
|
908
1017
|
// src/core/mutation-result.ts
|
|
909
1018
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -938,6 +1047,27 @@ function sliceByLocation(source, location) {
|
|
|
938
1047
|
function keyOf(mutator, original, replacement) {
|
|
939
1048
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
940
1049
|
}
|
|
1050
|
+
function describeMutant(file, entry) {
|
|
1051
|
+
const { mutant } = entry;
|
|
1052
|
+
return {
|
|
1053
|
+
file,
|
|
1054
|
+
line: mutant.location.start.line,
|
|
1055
|
+
column: mutant.location.start.column,
|
|
1056
|
+
status: mutant.status,
|
|
1057
|
+
mutator: mutant.mutatorName,
|
|
1058
|
+
original: normalizeCode(entry.original),
|
|
1059
|
+
replacement: normalizeCode(entry.replacement),
|
|
1060
|
+
occurrence: entry.occurrence,
|
|
1061
|
+
sameKeyCount: entry.sameKeyCount,
|
|
1062
|
+
allowance: {
|
|
1063
|
+
mutator: mutant.mutatorName,
|
|
1064
|
+
original: normalizeCode(entry.original),
|
|
1065
|
+
replacement: normalizeCode(entry.replacement),
|
|
1066
|
+
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
1067
|
+
reason: ""
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
941
1071
|
function describeAllowance(entry) {
|
|
942
1072
|
const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
|
|
943
1073
|
return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
|
|
@@ -945,13 +1075,15 @@ function describeAllowance(entry) {
|
|
|
945
1075
|
function judgeMutationReport(input) {
|
|
946
1076
|
const violations = [];
|
|
947
1077
|
const survivors = [];
|
|
1078
|
+
const unexpectedTimeouts = [];
|
|
948
1079
|
const summary = {
|
|
949
1080
|
total: 0,
|
|
950
1081
|
detected: 0,
|
|
951
1082
|
timeout: 0,
|
|
952
1083
|
allowed: 0,
|
|
953
1084
|
notEvaluable: 0,
|
|
954
|
-
unallowed: 0
|
|
1085
|
+
unallowed: 0,
|
|
1086
|
+
expectedTimeout: 0
|
|
955
1087
|
};
|
|
956
1088
|
const files = /* @__PURE__ */ new Map();
|
|
957
1089
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
@@ -961,7 +1093,9 @@ function judgeMutationReport(input) {
|
|
|
961
1093
|
const gatePath = normalizePath(gate.path);
|
|
962
1094
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
963
1095
|
violations.push(...shapeViolations);
|
|
964
|
-
|
|
1096
|
+
violations.push(
|
|
1097
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
1098
|
+
);
|
|
965
1099
|
const result = files.get(gatePath);
|
|
966
1100
|
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
967
1101
|
violations.push({
|
|
@@ -1025,38 +1159,53 @@ function judgeMutationReport(input) {
|
|
|
1025
1159
|
if (!locationsValid) {
|
|
1026
1160
|
continue;
|
|
1027
1161
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
if (!
|
|
1031
|
-
return;
|
|
1162
|
+
const claim = (field, entries, noun, accept) => {
|
|
1163
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1164
|
+
if (!Array.isArray(entries)) {
|
|
1165
|
+
return claimed;
|
|
1032
1166
|
}
|
|
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
|
-
|
|
1167
|
+
entries.forEach((entry, index) => {
|
|
1168
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
1172
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
1173
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
1174
|
+
violations.push({
|
|
1175
|
+
file: MANIFEST_FILE,
|
|
1176
|
+
message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
1177
|
+
});
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const target = group[(entry.occurrence ?? 1) - 1];
|
|
1181
|
+
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
1182
|
+
if (!target || refusal !== null) {
|
|
1183
|
+
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (claimed.has(target)) {
|
|
1187
|
+
violations.push({
|
|
1188
|
+
file: MANIFEST_FILE,
|
|
1189
|
+
message: `${at}: another ${noun} already covers this mutant`
|
|
1190
|
+
});
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
claimed.add(target);
|
|
1194
|
+
});
|
|
1195
|
+
return claimed;
|
|
1196
|
+
};
|
|
1197
|
+
const allowed = claim(
|
|
1198
|
+
"equivalentMutants",
|
|
1199
|
+
gate.equivalentMutants,
|
|
1200
|
+
"allowance",
|
|
1201
|
+
(status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
|
|
1202
|
+
);
|
|
1203
|
+
const expected = claim(
|
|
1204
|
+
"expectedTimeouts",
|
|
1205
|
+
gate.expectedTimeouts,
|
|
1206
|
+
"entry",
|
|
1207
|
+
(status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
|
|
1208
|
+
);
|
|
1060
1209
|
for (const entry of located) {
|
|
1061
1210
|
const { mutant } = entry;
|
|
1062
1211
|
summary.total += 1;
|
|
@@ -1064,6 +1213,11 @@ function judgeMutationReport(input) {
|
|
|
1064
1213
|
summary.detected += 1;
|
|
1065
1214
|
if (mutant.status === "Timeout") {
|
|
1066
1215
|
summary.timeout += 1;
|
|
1216
|
+
if (expected.has(entry)) {
|
|
1217
|
+
summary.expectedTimeout += 1;
|
|
1218
|
+
} else {
|
|
1219
|
+
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
1220
|
+
}
|
|
1067
1221
|
}
|
|
1068
1222
|
} else if (NOT_EVALUABLE.has(mutant.status)) {
|
|
1069
1223
|
summary.notEvaluable += 1;
|
|
@@ -1073,24 +1227,7 @@ function judgeMutationReport(input) {
|
|
|
1073
1227
|
continue;
|
|
1074
1228
|
}
|
|
1075
1229
|
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
|
-
});
|
|
1230
|
+
survivors.push(describeMutant(gate.path, entry));
|
|
1094
1231
|
} else {
|
|
1095
1232
|
violations.push({
|
|
1096
1233
|
file: gate.path,
|
|
@@ -1106,7 +1243,14 @@ function judgeMutationReport(input) {
|
|
|
1106
1243
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
1107
1244
|
});
|
|
1108
1245
|
}
|
|
1109
|
-
|
|
1246
|
+
const maxTimeouts = input.maxTimeouts ?? null;
|
|
1247
|
+
if (maxTimeouts !== null && unexpectedTimeouts.length > maxTimeouts) {
|
|
1248
|
+
violations.push({
|
|
1249
|
+
file: MANIFEST_FILE,
|
|
1250
|
+
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
return { violations, survivors, unexpectedTimeouts, summary };
|
|
1110
1254
|
}
|
|
1111
1255
|
function formatSurvivor(survivor) {
|
|
1112
1256
|
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
|
+
/** `null`: no limit. */
|
|
138
|
+
maxTimeouts: number | null;
|
|
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. Informational unless
|
|
283
|
+
* `maxTimeouts` is set and exceeded, in which case `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. `null` or absent means no limit.
|
|
316
|
+
*/
|
|
317
|
+
maxTimeouts?: number | null;
|
|
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
|
+
/** `null`: no limit. */
|
|
138
|
+
maxTimeouts: number | null;
|
|
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. Informational unless
|
|
283
|
+
* `maxTimeouts` is set and exceeded, in which case `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. `null` or absent means no limit.
|
|
316
|
+
*/
|
|
317
|
+
maxTimeouts?: number | null;
|
|
278
318
|
}): MutationVerdict;
|
|
279
319
|
/** `file:line:column / mutator / original → replacement / occurrence n of m`. */
|
|
280
320
|
declare function formatSurvivor(survivor: UnallowedSurvivor): string;
|