@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.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
readText,
|
|
21
21
|
resolveSettings,
|
|
22
22
|
validateEquivalentMutants
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-XEKQKLKY.js";
|
|
24
24
|
|
|
25
25
|
// src/commands/check.ts
|
|
26
26
|
import fs from "fs";
|
|
@@ -77,6 +77,70 @@ function parseImports(source) {
|
|
|
77
77
|
}
|
|
78
78
|
return found.sort((a, b) => a.line - b.line);
|
|
79
79
|
}
|
|
80
|
+
var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
|
|
81
|
+
function findUncheckableImports(source) {
|
|
82
|
+
return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/core/import-policy.ts
|
|
86
|
+
var SCRIPT_EXTENSION = /\.[cm]?[jt]sx?$/;
|
|
87
|
+
function isProjectImport(specifier, aliases) {
|
|
88
|
+
return specifier.startsWith(".") || aliases.some((alias) => specifier.startsWith(alias.prefix));
|
|
89
|
+
}
|
|
90
|
+
function resolveSpecifier(specifier, fromFile, aliases) {
|
|
91
|
+
let joined = null;
|
|
92
|
+
if (specifier.startsWith(".")) {
|
|
93
|
+
const slash = fromFile.lastIndexOf("/");
|
|
94
|
+
joined = slash === -1 ? specifier : `${fromFile.slice(0, slash)}/${specifier}`;
|
|
95
|
+
} else {
|
|
96
|
+
const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
|
|
97
|
+
if (alias) {
|
|
98
|
+
joined = alias.target + specifier.slice(alias.prefix.length);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (joined === null) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const segments = [];
|
|
105
|
+
for (const segment of joined.split("/")) {
|
|
106
|
+
if (segment === "..") {
|
|
107
|
+
segments.pop();
|
|
108
|
+
} else if (segment !== "." && segment !== "") {
|
|
109
|
+
segments.push(segment);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return segments.join("/").replace(SCRIPT_EXTENSION, "");
|
|
113
|
+
}
|
|
114
|
+
function matches(rule, specifier) {
|
|
115
|
+
if (rule.regex) {
|
|
116
|
+
return rule.regex.test(specifier);
|
|
117
|
+
}
|
|
118
|
+
return specifier === rule.module || specifier.startsWith(`${rule.module}/`);
|
|
119
|
+
}
|
|
120
|
+
function judgePackageImport(ref, allow) {
|
|
121
|
+
const rules = allow.filter((rule) => matches(rule, ref.module));
|
|
122
|
+
if (rules.length === 0) {
|
|
123
|
+
return `runtime import of "${ref.module}" is not in settings.imports.allow (allowlist mode allows relative imports, import aliases and listed packages only)`;
|
|
124
|
+
}
|
|
125
|
+
if (rules.some((rule) => rule.names === void 0)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const allowedNames = rules.flatMap((rule) => rule.names);
|
|
129
|
+
if (ref.whole) {
|
|
130
|
+
return `only ${allowedNames.join(", ")} may be imported from "${ref.module}", as named imports (settings.imports.allow)`;
|
|
131
|
+
}
|
|
132
|
+
const extra = ref.names.filter((name) => !allowedNames.includes(name));
|
|
133
|
+
return extra.length === 0 ? null : `runtime import of ${extra.join(", ")} from "${ref.module}" is not in settings.imports.allow (allowed: ${allowedNames.join(", ")})`;
|
|
134
|
+
}
|
|
135
|
+
function judgeProjectImport(target, impurePaths, gateExtensions) {
|
|
136
|
+
const candidates = [target, ...gateExtensions.map((extension) => target + extension)];
|
|
137
|
+
for (const rule of impurePaths) {
|
|
138
|
+
if (candidates.some((candidate) => rule.regex.test(candidate))) {
|
|
139
|
+
return rule.reason;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
80
144
|
|
|
81
145
|
// src/core/scan.ts
|
|
82
146
|
var IGNORE_DIRECTIVE = /\b(istanbul|c8|v8)\s+ignore\b/;
|
|
@@ -94,7 +158,7 @@ var DISABLED_TESTS = [
|
|
|
94
158
|
}
|
|
95
159
|
];
|
|
96
160
|
var MODULE_MOCK = /\b(?:jest|vi)\s*\.\s*(?:mock|doMock|unstable_mockModule|setMock)\s*\(\s*(?:import\s*\(\s*)?(['"`])([^'"`\n]+)\1/g;
|
|
97
|
-
var
|
|
161
|
+
var SCRIPT_EXTENSION2 = /\.[cm]?[jt]sx?$/;
|
|
98
162
|
function eachLine(source, visit) {
|
|
99
163
|
source.split("\n").forEach((line, index) => visit(line, index + 1));
|
|
100
164
|
}
|
|
@@ -131,6 +195,7 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
131
195
|
if (imported.typeOnly) {
|
|
132
196
|
continue;
|
|
133
197
|
}
|
|
198
|
+
const before = violations.length;
|
|
134
199
|
if (!settings.allowedImports.some((regex) => regex.test(imported.module))) {
|
|
135
200
|
for (const rule of settings.impureImports) {
|
|
136
201
|
if (rule.regex.test(imported.module)) {
|
|
@@ -157,34 +222,31 @@ function scanGateSource(gatePath, source, settings) {
|
|
|
157
222
|
});
|
|
158
223
|
}
|
|
159
224
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
break;
|
|
225
|
+
if (settings.importMode === "allowlist" && violations.length === before) {
|
|
226
|
+
const reason = isProjectImport(imported.module, settings.importAliases) ? judgeProjectImport(
|
|
227
|
+
resolveSpecifier(imported.module, gatePath, settings.importAliases),
|
|
228
|
+
settings.impurePaths,
|
|
229
|
+
settings.gateExtensions
|
|
230
|
+
) : judgePackageImport(imported, settings.importAllow);
|
|
231
|
+
if (reason !== null) {
|
|
232
|
+
violations.push({
|
|
233
|
+
file: gatePath,
|
|
234
|
+
line: imported.line,
|
|
235
|
+
message: isProjectImport(imported.module, settings.importAliases) ? `runtime import of "${imported.module}" (${reason})` : reason
|
|
236
|
+
});
|
|
173
237
|
}
|
|
174
238
|
}
|
|
175
239
|
}
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
} else if (segment !== "." && segment !== "") {
|
|
184
|
-
segments.push(segment);
|
|
240
|
+
if (settings.importMode === "allowlist") {
|
|
241
|
+
for (const line of findUncheckableImports(source)) {
|
|
242
|
+
violations.push({
|
|
243
|
+
file: gatePath,
|
|
244
|
+
line,
|
|
245
|
+
message: "require() / import() with a computed specifier cannot be checked (allowlist mode)"
|
|
246
|
+
});
|
|
185
247
|
}
|
|
186
248
|
}
|
|
187
|
-
return
|
|
249
|
+
return violations.sort((a, b) => a.line - b.line);
|
|
188
250
|
}
|
|
189
251
|
function scanSpecSource(gatePath, specPath, source, settings) {
|
|
190
252
|
const violations = [];
|
|
@@ -202,7 +264,7 @@ function scanSpecSource(gatePath, specPath, source, settings) {
|
|
|
202
264
|
}
|
|
203
265
|
}
|
|
204
266
|
});
|
|
205
|
-
const gateModule = gatePath.replace(
|
|
267
|
+
const gateModule = gatePath.replace(SCRIPT_EXTENSION2, "");
|
|
206
268
|
const gateDirectory = gateModule.replace(/\/index$/, "");
|
|
207
269
|
for (const match of source.matchAll(MODULE_MOCK)) {
|
|
208
270
|
const specifier = match[2];
|
|
@@ -331,6 +393,9 @@ function checkGates(manifest) {
|
|
|
331
393
|
}
|
|
332
394
|
return violations;
|
|
333
395
|
}
|
|
396
|
+
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
397
|
+
(extension) => `vitest.gates.config.${extension}`
|
|
398
|
+
);
|
|
334
399
|
|
|
335
400
|
// src/core/mutation-result.ts
|
|
336
401
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -365,6 +430,27 @@ function sliceByLocation(source, location) {
|
|
|
365
430
|
function keyOf(mutator, original, replacement) {
|
|
366
431
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
367
432
|
}
|
|
433
|
+
function describeMutant(file, entry) {
|
|
434
|
+
const { mutant } = entry;
|
|
435
|
+
return {
|
|
436
|
+
file,
|
|
437
|
+
line: mutant.location.start.line,
|
|
438
|
+
column: mutant.location.start.column,
|
|
439
|
+
status: mutant.status,
|
|
440
|
+
mutator: mutant.mutatorName,
|
|
441
|
+
original: normalizeCode(entry.original),
|
|
442
|
+
replacement: normalizeCode(entry.replacement),
|
|
443
|
+
occurrence: entry.occurrence,
|
|
444
|
+
sameKeyCount: entry.sameKeyCount,
|
|
445
|
+
allowance: {
|
|
446
|
+
mutator: mutant.mutatorName,
|
|
447
|
+
original: normalizeCode(entry.original),
|
|
448
|
+
replacement: normalizeCode(entry.replacement),
|
|
449
|
+
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
450
|
+
reason: ""
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
368
454
|
function describeAllowance(entry) {
|
|
369
455
|
const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
|
|
370
456
|
return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
|
|
@@ -372,13 +458,15 @@ function describeAllowance(entry) {
|
|
|
372
458
|
function judgeMutationReport(input) {
|
|
373
459
|
const violations = [];
|
|
374
460
|
const survivors = [];
|
|
461
|
+
const unexpectedTimeouts = [];
|
|
375
462
|
const summary = {
|
|
376
463
|
total: 0,
|
|
377
464
|
detected: 0,
|
|
378
465
|
timeout: 0,
|
|
379
466
|
allowed: 0,
|
|
380
467
|
notEvaluable: 0,
|
|
381
|
-
unallowed: 0
|
|
468
|
+
unallowed: 0,
|
|
469
|
+
expectedTimeout: 0
|
|
382
470
|
};
|
|
383
471
|
const files = /* @__PURE__ */ new Map();
|
|
384
472
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
@@ -388,7 +476,9 @@ function judgeMutationReport(input) {
|
|
|
388
476
|
const gatePath = normalizePath(gate.path);
|
|
389
477
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
390
478
|
violations.push(...shapeViolations);
|
|
391
|
-
|
|
479
|
+
violations.push(
|
|
480
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
481
|
+
);
|
|
392
482
|
const result = files.get(gatePath);
|
|
393
483
|
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
394
484
|
violations.push({
|
|
@@ -452,38 +542,53 @@ function judgeMutationReport(input) {
|
|
|
452
542
|
if (!locationsValid) {
|
|
453
543
|
continue;
|
|
454
544
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
if (!
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
const at = `${gate.path}: equivalentMutants[${index}] (${describeAllowance(allowance)})`;
|
|
461
|
-
const group = byKey.get(keyOf(allowance.mutator, allowance.original, allowance.replacement)) ?? [];
|
|
462
|
-
if (allowance.occurrence === void 0 && group.length > 1) {
|
|
463
|
-
violations.push({
|
|
464
|
-
file: MANIFEST_FILE,
|
|
465
|
-
message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
466
|
-
});
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
const target = group[(allowance.occurrence ?? 1) - 1];
|
|
470
|
-
if (!target || !SURVIVED.has(target.mutant.status)) {
|
|
471
|
-
const why = target ? `the mutant is now ${target.mutant.status}` : "no such mutant in this run";
|
|
472
|
-
violations.push({
|
|
473
|
-
file: MANIFEST_FILE,
|
|
474
|
-
message: `${at}: stale allowance (${why}). Remove it`
|
|
475
|
-
});
|
|
476
|
-
return;
|
|
545
|
+
const claim = (field, entries, noun, accept) => {
|
|
546
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
547
|
+
if (!Array.isArray(entries)) {
|
|
548
|
+
return claimed;
|
|
477
549
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
550
|
+
entries.forEach((entry, index) => {
|
|
551
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
555
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
556
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
557
|
+
violations.push({
|
|
558
|
+
file: MANIFEST_FILE,
|
|
559
|
+
message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const target = group[(entry.occurrence ?? 1) - 1];
|
|
564
|
+
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
565
|
+
if (!target || refusal !== null) {
|
|
566
|
+
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (claimed.has(target)) {
|
|
570
|
+
violations.push({
|
|
571
|
+
file: MANIFEST_FILE,
|
|
572
|
+
message: `${at}: another ${noun} already covers this mutant`
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
claimed.add(target);
|
|
577
|
+
});
|
|
578
|
+
return claimed;
|
|
579
|
+
};
|
|
580
|
+
const allowed = claim(
|
|
581
|
+
"equivalentMutants",
|
|
582
|
+
gate.equivalentMutants,
|
|
583
|
+
"allowance",
|
|
584
|
+
(status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
|
|
585
|
+
);
|
|
586
|
+
const expected = claim(
|
|
587
|
+
"expectedTimeouts",
|
|
588
|
+
gate.expectedTimeouts,
|
|
589
|
+
"entry",
|
|
590
|
+
(status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
|
|
591
|
+
);
|
|
487
592
|
for (const entry of located) {
|
|
488
593
|
const { mutant } = entry;
|
|
489
594
|
summary.total += 1;
|
|
@@ -491,6 +596,11 @@ function judgeMutationReport(input) {
|
|
|
491
596
|
summary.detected += 1;
|
|
492
597
|
if (mutant.status === "Timeout") {
|
|
493
598
|
summary.timeout += 1;
|
|
599
|
+
if (expected.has(entry)) {
|
|
600
|
+
summary.expectedTimeout += 1;
|
|
601
|
+
} else {
|
|
602
|
+
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
603
|
+
}
|
|
494
604
|
}
|
|
495
605
|
} else if (NOT_EVALUABLE.has(mutant.status)) {
|
|
496
606
|
summary.notEvaluable += 1;
|
|
@@ -500,24 +610,7 @@ function judgeMutationReport(input) {
|
|
|
500
610
|
continue;
|
|
501
611
|
}
|
|
502
612
|
summary.unallowed += 1;
|
|
503
|
-
survivors.push(
|
|
504
|
-
file: gate.path,
|
|
505
|
-
line: mutant.location.start.line,
|
|
506
|
-
column: mutant.location.start.column,
|
|
507
|
-
status: mutant.status,
|
|
508
|
-
mutator: mutant.mutatorName,
|
|
509
|
-
original: normalizeCode(entry.original),
|
|
510
|
-
replacement: normalizeCode(entry.replacement),
|
|
511
|
-
occurrence: entry.occurrence,
|
|
512
|
-
sameKeyCount: entry.sameKeyCount,
|
|
513
|
-
allowance: {
|
|
514
|
-
mutator: mutant.mutatorName,
|
|
515
|
-
original: normalizeCode(entry.original),
|
|
516
|
-
replacement: normalizeCode(entry.replacement),
|
|
517
|
-
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
518
|
-
reason: ""
|
|
519
|
-
}
|
|
520
|
-
});
|
|
613
|
+
survivors.push(describeMutant(gate.path, entry));
|
|
521
614
|
} else {
|
|
522
615
|
violations.push({
|
|
523
616
|
file: gate.path,
|
|
@@ -533,7 +626,14 @@ function judgeMutationReport(input) {
|
|
|
533
626
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
534
627
|
});
|
|
535
628
|
}
|
|
536
|
-
|
|
629
|
+
const maxTimeouts = input.maxTimeouts ?? null;
|
|
630
|
+
if (maxTimeouts !== null && unexpectedTimeouts.length > maxTimeouts) {
|
|
631
|
+
violations.push({
|
|
632
|
+
file: MANIFEST_FILE,
|
|
633
|
+
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return { violations, survivors, unexpectedTimeouts, summary };
|
|
537
637
|
}
|
|
538
638
|
function formatSurvivor(survivor) {
|
|
539
639
|
const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
|
package/dist/jest.cjs
CHANGED
|
@@ -71,17 +71,17 @@ function allowanceProblems(entry) {
|
|
|
71
71
|
}
|
|
72
72
|
return problems;
|
|
73
73
|
}
|
|
74
|
-
function validateEquivalentMutants(gatePath, value) {
|
|
74
|
+
function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
|
|
75
75
|
if (value === void 0) {
|
|
76
76
|
return [];
|
|
77
77
|
}
|
|
78
78
|
if (!Array.isArray(value)) {
|
|
79
|
-
return [{ file: MANIFEST_FILE, message: `${gatePath}:
|
|
79
|
+
return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
|
|
80
80
|
}
|
|
81
81
|
return value.flatMap(
|
|
82
82
|
(entry, index) => allowanceProblems(entry).map((problem) => ({
|
|
83
83
|
file: MANIFEST_FILE,
|
|
84
|
-
message: `${gatePath}:
|
|
84
|
+
message: `${gatePath}: ${field}[${index}]${problem}`
|
|
85
85
|
}))
|
|
86
86
|
);
|
|
87
87
|
}
|
|
@@ -161,6 +161,9 @@ function parseManifest(text) {
|
|
|
161
161
|
}
|
|
162
162
|
for (const gate of gates) {
|
|
163
163
|
violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
|
|
164
|
+
violations.push(
|
|
165
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
166
|
+
);
|
|
164
167
|
}
|
|
165
168
|
return { gates, candidates, settings: raw.settings, violations };
|
|
166
169
|
}
|
|
@@ -310,7 +313,9 @@ function resolveSettings(raw) {
|
|
|
310
313
|
"gateCommand",
|
|
311
314
|
"selfcheck",
|
|
312
315
|
"stryker",
|
|
313
|
-
"lcov"
|
|
316
|
+
"lcov",
|
|
317
|
+
"imports",
|
|
318
|
+
"mutation"
|
|
314
319
|
],
|
|
315
320
|
"settings"
|
|
316
321
|
);
|
|
@@ -411,6 +416,42 @@ function resolveSettings(raw) {
|
|
|
411
416
|
"settings.selfcheck.failurePattern"
|
|
412
417
|
);
|
|
413
418
|
}
|
|
419
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
420
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
421
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
422
|
+
}
|
|
423
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
424
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
425
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
426
|
+
}
|
|
427
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
428
|
+
const at = `settings.imports.allow[${index}]`;
|
|
429
|
+
if (typeof item === "string" && item !== "") {
|
|
430
|
+
return { module: item };
|
|
431
|
+
}
|
|
432
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
433
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
434
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
435
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
436
|
+
}
|
|
437
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
438
|
+
}
|
|
439
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
440
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
441
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
442
|
+
if (names.length === 0) {
|
|
443
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
444
|
+
}
|
|
445
|
+
return { module: item.module, names };
|
|
446
|
+
}
|
|
447
|
+
throw new SettingsError(
|
|
448
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
452
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
453
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
454
|
+
}
|
|
414
455
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
415
456
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
416
457
|
return {
|
|
@@ -432,6 +473,9 @@ function resolveSettings(raw) {
|
|
|
432
473
|
selfcheckFailurePattern,
|
|
433
474
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
434
475
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
476
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
477
|
+
importAllow,
|
|
478
|
+
maxTimeouts: mutation.maxTimeouts ?? null,
|
|
435
479
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
436
480
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
437
481
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
package/dist/jest.js
CHANGED
package/dist/stryker.cjs
CHANGED
|
@@ -70,17 +70,17 @@ function allowanceProblems(entry) {
|
|
|
70
70
|
}
|
|
71
71
|
return problems;
|
|
72
72
|
}
|
|
73
|
-
function validateEquivalentMutants(gatePath, value) {
|
|
73
|
+
function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
|
|
74
74
|
if (value === void 0) {
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
if (!Array.isArray(value)) {
|
|
78
|
-
return [{ file: MANIFEST_FILE, message: `${gatePath}:
|
|
78
|
+
return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
|
|
79
79
|
}
|
|
80
80
|
return value.flatMap(
|
|
81
81
|
(entry, index) => allowanceProblems(entry).map((problem) => ({
|
|
82
82
|
file: MANIFEST_FILE,
|
|
83
|
-
message: `${gatePath}:
|
|
83
|
+
message: `${gatePath}: ${field}[${index}]${problem}`
|
|
84
84
|
}))
|
|
85
85
|
);
|
|
86
86
|
}
|
|
@@ -160,6 +160,9 @@ function parseManifest(text) {
|
|
|
160
160
|
}
|
|
161
161
|
for (const gate of gates) {
|
|
162
162
|
violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
|
|
163
|
+
violations.push(
|
|
164
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
165
|
+
);
|
|
163
166
|
}
|
|
164
167
|
return { gates, candidates, settings: raw.settings, violations };
|
|
165
168
|
}
|
|
@@ -309,7 +312,9 @@ function resolveSettings(raw) {
|
|
|
309
312
|
"gateCommand",
|
|
310
313
|
"selfcheck",
|
|
311
314
|
"stryker",
|
|
312
|
-
"lcov"
|
|
315
|
+
"lcov",
|
|
316
|
+
"imports",
|
|
317
|
+
"mutation"
|
|
313
318
|
],
|
|
314
319
|
"settings"
|
|
315
320
|
);
|
|
@@ -410,6 +415,42 @@ function resolveSettings(raw) {
|
|
|
410
415
|
"settings.selfcheck.failurePattern"
|
|
411
416
|
);
|
|
412
417
|
}
|
|
418
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
419
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
420
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
421
|
+
}
|
|
422
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
423
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
424
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
425
|
+
}
|
|
426
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
427
|
+
const at = `settings.imports.allow[${index}]`;
|
|
428
|
+
if (typeof item === "string" && item !== "") {
|
|
429
|
+
return { module: item };
|
|
430
|
+
}
|
|
431
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
432
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
433
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
434
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
435
|
+
}
|
|
436
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
437
|
+
}
|
|
438
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
439
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
440
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
441
|
+
if (names.length === 0) {
|
|
442
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
443
|
+
}
|
|
444
|
+
return { module: item.module, names };
|
|
445
|
+
}
|
|
446
|
+
throw new SettingsError(
|
|
447
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
451
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
452
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
453
|
+
}
|
|
413
454
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
414
455
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
415
456
|
return {
|
|
@@ -431,6 +472,9 @@ function resolveSettings(raw) {
|
|
|
431
472
|
selfcheckFailurePattern,
|
|
432
473
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
433
474
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
475
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
476
|
+
importAllow,
|
|
477
|
+
maxTimeouts: mutation.maxTimeouts ?? null,
|
|
434
478
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
435
479
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
436
480
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
package/dist/stryker.js
CHANGED
package/dist/vitest.cjs
CHANGED
|
@@ -70,17 +70,17 @@ function allowanceProblems(entry) {
|
|
|
70
70
|
}
|
|
71
71
|
return problems;
|
|
72
72
|
}
|
|
73
|
-
function validateEquivalentMutants(gatePath, value) {
|
|
73
|
+
function validateEquivalentMutants(gatePath, value, field = "equivalentMutants") {
|
|
74
74
|
if (value === void 0) {
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
if (!Array.isArray(value)) {
|
|
78
|
-
return [{ file: MANIFEST_FILE, message: `${gatePath}:
|
|
78
|
+
return [{ file: MANIFEST_FILE, message: `${gatePath}: ${field} must be an array` }];
|
|
79
79
|
}
|
|
80
80
|
return value.flatMap(
|
|
81
81
|
(entry, index) => allowanceProblems(entry).map((problem) => ({
|
|
82
82
|
file: MANIFEST_FILE,
|
|
83
|
-
message: `${gatePath}:
|
|
83
|
+
message: `${gatePath}: ${field}[${index}]${problem}`
|
|
84
84
|
}))
|
|
85
85
|
);
|
|
86
86
|
}
|
|
@@ -160,6 +160,9 @@ function parseManifest(text) {
|
|
|
160
160
|
}
|
|
161
161
|
for (const gate of gates) {
|
|
162
162
|
violations.push(...validateEquivalentMutants(gate.path, gate.equivalentMutants));
|
|
163
|
+
violations.push(
|
|
164
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
165
|
+
);
|
|
163
166
|
}
|
|
164
167
|
return { gates, candidates, settings: raw.settings, violations };
|
|
165
168
|
}
|
|
@@ -309,7 +312,9 @@ function resolveSettings(raw) {
|
|
|
309
312
|
"gateCommand",
|
|
310
313
|
"selfcheck",
|
|
311
314
|
"stryker",
|
|
312
|
-
"lcov"
|
|
315
|
+
"lcov",
|
|
316
|
+
"imports",
|
|
317
|
+
"mutation"
|
|
313
318
|
],
|
|
314
319
|
"settings"
|
|
315
320
|
);
|
|
@@ -410,6 +415,42 @@ function resolveSettings(raw) {
|
|
|
410
415
|
"settings.selfcheck.failurePattern"
|
|
411
416
|
);
|
|
412
417
|
}
|
|
418
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
419
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
420
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
421
|
+
}
|
|
422
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
423
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
424
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
425
|
+
}
|
|
426
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
427
|
+
const at = `settings.imports.allow[${index}]`;
|
|
428
|
+
if (typeof item === "string" && item !== "") {
|
|
429
|
+
return { module: item };
|
|
430
|
+
}
|
|
431
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
432
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
433
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
434
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
435
|
+
}
|
|
436
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
437
|
+
}
|
|
438
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
439
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
440
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
441
|
+
if (names.length === 0) {
|
|
442
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
443
|
+
}
|
|
444
|
+
return { module: item.module, names };
|
|
445
|
+
}
|
|
446
|
+
throw new SettingsError(
|
|
447
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
451
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
452
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
453
|
+
}
|
|
413
454
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
414
455
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
415
456
|
return {
|
|
@@ -431,6 +472,9 @@ function resolveSettings(raw) {
|
|
|
431
472
|
selfcheckFailurePattern,
|
|
432
473
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
433
474
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
475
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
476
|
+
importAllow,
|
|
477
|
+
maxTimeouts: mutation.maxTimeouts ?? null,
|
|
434
478
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
435
479
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
436
480
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|
|
@@ -561,7 +605,7 @@ function resolveRootDir(rootDir) {
|
|
|
561
605
|
|
|
562
606
|
// src/vitest.ts
|
|
563
607
|
function createVitestGatesConfig(options = {}) {
|
|
564
|
-
const { rootDir: rootDirOption, test = {}, ...overrides } = options;
|
|
608
|
+
const { rootDir: rootDirOption, coverageProvider, test = {}, ...overrides } = options;
|
|
565
609
|
const rootDir = resolveRootDir(rootDirOption);
|
|
566
610
|
const manifest = loadGates(rootDir);
|
|
567
611
|
const { coverage = {}, ...testOverrides } = test;
|
|
@@ -581,6 +625,7 @@ function createVitestGatesConfig(options = {}) {
|
|
|
581
625
|
reporter: ["text"],
|
|
582
626
|
reportsDirectory: "coverage/gates",
|
|
583
627
|
...coverage,
|
|
628
|
+
...coverageProvider === void 0 ? {} : { provider: coverageProvider },
|
|
584
629
|
enabled: true,
|
|
585
630
|
include: manifest.gates.map((gate) => gate.path),
|
|
586
631
|
thresholds: { perFile: true, ...FULL_COVERAGE }
|