@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.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
readText,
|
|
21
21
|
resolveSettings,
|
|
22
22
|
validateEquivalentMutants
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-F7WBEFIL.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];
|
|
@@ -239,6 +301,21 @@ function scanStrykerConfig(configPath, source) {
|
|
|
239
301
|
return violations;
|
|
240
302
|
}
|
|
241
303
|
|
|
304
|
+
// src/core/vitest-coverage.ts
|
|
305
|
+
function vitestCoverageViolation(input) {
|
|
306
|
+
if (input.vitestVersion === null || input.vitestVersion === "") {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const major = Number(input.vitestVersion.split(".")[0]);
|
|
310
|
+
if (!Number.isInteger(major) || major >= 4) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (/\bistanbul\b/.test(input.configSource) || /\bexperimentalAstAwareRemapping\s*:\s*true\b/.test(input.configSource)) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
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`;
|
|
317
|
+
}
|
|
318
|
+
|
|
242
319
|
// src/commands/check.ts
|
|
243
320
|
var STRYKER_CONFIG_NAMES = [
|
|
244
321
|
"stryker.gates.config.mjs",
|
|
@@ -329,8 +406,40 @@ function checkGates(manifest) {
|
|
|
329
406
|
violations.push(...scanStrykerConfig(strykerConfig, readText(dir, strykerConfig)));
|
|
330
407
|
}
|
|
331
408
|
}
|
|
409
|
+
violations.push(...checkVitestCoverage(manifest));
|
|
332
410
|
return violations;
|
|
333
411
|
}
|
|
412
|
+
var VITEST_GATE_CONFIGS = ["ts", "mts", "cts", "js", "mjs", "cjs"].map(
|
|
413
|
+
(extension) => `vitest.gates.config.${extension}`
|
|
414
|
+
);
|
|
415
|
+
function installedVitestVersion(startDir) {
|
|
416
|
+
let current = startDir;
|
|
417
|
+
for (; ; ) {
|
|
418
|
+
try {
|
|
419
|
+
const pkg = JSON.parse(
|
|
420
|
+
fs.readFileSync(path.join(current, "node_modules", "vitest", "package.json"), "utf8")
|
|
421
|
+
);
|
|
422
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
423
|
+
} catch {
|
|
424
|
+
const parent = path.dirname(current);
|
|
425
|
+
if (parent === current) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
current = parent;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function checkVitestCoverage(manifest) {
|
|
433
|
+
const config = VITEST_GATE_CONFIGS.find((name) => fs.existsSync(path.join(manifest.dir, name)));
|
|
434
|
+
if (!config) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
const message = vitestCoverageViolation({
|
|
438
|
+
vitestVersion: installedVitestVersion(manifest.dir),
|
|
439
|
+
configSource: readText(manifest.dir, config)
|
|
440
|
+
});
|
|
441
|
+
return message === null ? [] : [{ file: config, message }];
|
|
442
|
+
}
|
|
334
443
|
|
|
335
444
|
// src/core/mutation-result.ts
|
|
336
445
|
var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
|
|
@@ -365,6 +474,27 @@ function sliceByLocation(source, location) {
|
|
|
365
474
|
function keyOf(mutator, original, replacement) {
|
|
366
475
|
return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
|
|
367
476
|
}
|
|
477
|
+
function describeMutant(file, entry) {
|
|
478
|
+
const { mutant } = entry;
|
|
479
|
+
return {
|
|
480
|
+
file,
|
|
481
|
+
line: mutant.location.start.line,
|
|
482
|
+
column: mutant.location.start.column,
|
|
483
|
+
status: mutant.status,
|
|
484
|
+
mutator: mutant.mutatorName,
|
|
485
|
+
original: normalizeCode(entry.original),
|
|
486
|
+
replacement: normalizeCode(entry.replacement),
|
|
487
|
+
occurrence: entry.occurrence,
|
|
488
|
+
sameKeyCount: entry.sameKeyCount,
|
|
489
|
+
allowance: {
|
|
490
|
+
mutator: mutant.mutatorName,
|
|
491
|
+
original: normalizeCode(entry.original),
|
|
492
|
+
replacement: normalizeCode(entry.replacement),
|
|
493
|
+
...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
|
|
494
|
+
reason: ""
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
368
498
|
function describeAllowance(entry) {
|
|
369
499
|
const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
|
|
370
500
|
return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
|
|
@@ -372,13 +502,15 @@ function describeAllowance(entry) {
|
|
|
372
502
|
function judgeMutationReport(input) {
|
|
373
503
|
const violations = [];
|
|
374
504
|
const survivors = [];
|
|
505
|
+
const unexpectedTimeouts = [];
|
|
375
506
|
const summary = {
|
|
376
507
|
total: 0,
|
|
377
508
|
detected: 0,
|
|
378
509
|
timeout: 0,
|
|
379
510
|
allowed: 0,
|
|
380
511
|
notEvaluable: 0,
|
|
381
|
-
unallowed: 0
|
|
512
|
+
unallowed: 0,
|
|
513
|
+
expectedTimeout: 0
|
|
382
514
|
};
|
|
383
515
|
const files = /* @__PURE__ */ new Map();
|
|
384
516
|
for (const [file, result] of Object.entries(input.report.files ?? {})) {
|
|
@@ -388,7 +520,9 @@ function judgeMutationReport(input) {
|
|
|
388
520
|
const gatePath = normalizePath(gate.path);
|
|
389
521
|
const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
|
|
390
522
|
violations.push(...shapeViolations);
|
|
391
|
-
|
|
523
|
+
violations.push(
|
|
524
|
+
...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
|
|
525
|
+
);
|
|
392
526
|
const result = files.get(gatePath);
|
|
393
527
|
if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
|
|
394
528
|
violations.push({
|
|
@@ -452,38 +586,53 @@ function judgeMutationReport(input) {
|
|
|
452
586
|
if (!locationsValid) {
|
|
453
587
|
continue;
|
|
454
588
|
}
|
|
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;
|
|
589
|
+
const claim = (field, entries, noun, accept) => {
|
|
590
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
591
|
+
if (!Array.isArray(entries)) {
|
|
592
|
+
return claimed;
|
|
468
593
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
594
|
+
entries.forEach((entry, index) => {
|
|
595
|
+
if (!isUsableEquivalentMutant(entry)) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
|
|
599
|
+
const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
|
|
600
|
+
if (entry.occurrence === void 0 && group.length > 1) {
|
|
601
|
+
violations.push({
|
|
602
|
+
file: MANIFEST_FILE,
|
|
603
|
+
message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const target = group[(entry.occurrence ?? 1) - 1];
|
|
608
|
+
const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
|
|
609
|
+
if (!target || refusal !== null) {
|
|
610
|
+
violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (claimed.has(target)) {
|
|
614
|
+
violations.push({
|
|
615
|
+
file: MANIFEST_FILE,
|
|
616
|
+
message: `${at}: another ${noun} already covers this mutant`
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
claimed.add(target);
|
|
621
|
+
});
|
|
622
|
+
return claimed;
|
|
623
|
+
};
|
|
624
|
+
const allowed = claim(
|
|
625
|
+
"equivalentMutants",
|
|
626
|
+
gate.equivalentMutants,
|
|
627
|
+
"allowance",
|
|
628
|
+
(status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
|
|
629
|
+
);
|
|
630
|
+
const expected = claim(
|
|
631
|
+
"expectedTimeouts",
|
|
632
|
+
gate.expectedTimeouts,
|
|
633
|
+
"entry",
|
|
634
|
+
(status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
|
|
635
|
+
);
|
|
487
636
|
for (const entry of located) {
|
|
488
637
|
const { mutant } = entry;
|
|
489
638
|
summary.total += 1;
|
|
@@ -491,6 +640,11 @@ function judgeMutationReport(input) {
|
|
|
491
640
|
summary.detected += 1;
|
|
492
641
|
if (mutant.status === "Timeout") {
|
|
493
642
|
summary.timeout += 1;
|
|
643
|
+
if (expected.has(entry)) {
|
|
644
|
+
summary.expectedTimeout += 1;
|
|
645
|
+
} else {
|
|
646
|
+
unexpectedTimeouts.push(describeMutant(gate.path, entry));
|
|
647
|
+
}
|
|
494
648
|
}
|
|
495
649
|
} else if (NOT_EVALUABLE.has(mutant.status)) {
|
|
496
650
|
summary.notEvaluable += 1;
|
|
@@ -500,24 +654,7 @@ function judgeMutationReport(input) {
|
|
|
500
654
|
continue;
|
|
501
655
|
}
|
|
502
656
|
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
|
-
});
|
|
657
|
+
survivors.push(describeMutant(gate.path, entry));
|
|
521
658
|
} else {
|
|
522
659
|
violations.push({
|
|
523
660
|
file: gate.path,
|
|
@@ -533,7 +670,14 @@ function judgeMutationReport(input) {
|
|
|
533
670
|
message: "no mutants were evaluated (the mutation test ran on nothing)"
|
|
534
671
|
});
|
|
535
672
|
}
|
|
536
|
-
|
|
673
|
+
const maxTimeouts = input.maxTimeouts ?? 0;
|
|
674
|
+
if (unexpectedTimeouts.length > maxTimeouts) {
|
|
675
|
+
violations.push({
|
|
676
|
+
file: MANIFEST_FILE,
|
|
677
|
+
message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return { violations, survivors, unexpectedTimeouts, summary };
|
|
537
681
|
}
|
|
538
682
|
function formatSurvivor(survivor) {
|
|
539
683
|
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
|
}
|
|
@@ -209,8 +212,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
209
212
|
{
|
|
210
213
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
211
214
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
218
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
212
219
|
}
|
|
213
220
|
];
|
|
221
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
214
222
|
var SettingsError = class extends Error {
|
|
215
223
|
};
|
|
216
224
|
function isRecord2(value) {
|
|
@@ -310,7 +318,9 @@ function resolveSettings(raw) {
|
|
|
310
318
|
"gateCommand",
|
|
311
319
|
"selfcheck",
|
|
312
320
|
"stryker",
|
|
313
|
-
"lcov"
|
|
321
|
+
"lcov",
|
|
322
|
+
"imports",
|
|
323
|
+
"mutation"
|
|
314
324
|
],
|
|
315
325
|
"settings"
|
|
316
326
|
);
|
|
@@ -411,6 +421,42 @@ function resolveSettings(raw) {
|
|
|
411
421
|
"settings.selfcheck.failurePattern"
|
|
412
422
|
);
|
|
413
423
|
}
|
|
424
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
425
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
426
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
427
|
+
}
|
|
428
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
429
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
430
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
431
|
+
}
|
|
432
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
433
|
+
const at = `settings.imports.allow[${index}]`;
|
|
434
|
+
if (typeof item === "string" && item !== "") {
|
|
435
|
+
return { module: item };
|
|
436
|
+
}
|
|
437
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
438
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
439
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
440
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
441
|
+
}
|
|
442
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
443
|
+
}
|
|
444
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
445
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
446
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
447
|
+
if (names.length === 0) {
|
|
448
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
449
|
+
}
|
|
450
|
+
return { module: item.module, names };
|
|
451
|
+
}
|
|
452
|
+
throw new SettingsError(
|
|
453
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
457
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
458
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
459
|
+
}
|
|
414
460
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
415
461
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
416
462
|
return {
|
|
@@ -432,6 +478,9 @@ function resolveSettings(raw) {
|
|
|
432
478
|
selfcheckFailurePattern,
|
|
433
479
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
434
480
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
481
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
482
|
+
importAllow,
|
|
483
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
435
484
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
436
485
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
437
486
|
(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
|
}
|
|
@@ -208,8 +211,13 @@ var DEFAULT_FORBIDDEN_SOURCE = [
|
|
|
208
211
|
{
|
|
209
212
|
pattern: `^\\s*(['"])use (client|server)\\1`,
|
|
210
213
|
reason: "'use client' / 'use server' directive (not a pure module)"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: "\\bprocess\\??\\.env\\b",
|
|
217
|
+
reason: "reads process.env (not a pure module). Take the value as an argument"
|
|
211
218
|
}
|
|
212
219
|
];
|
|
220
|
+
var DEFAULT_MAX_TIMEOUTS = 0;
|
|
213
221
|
var SettingsError = class extends Error {
|
|
214
222
|
};
|
|
215
223
|
function isRecord2(value) {
|
|
@@ -309,7 +317,9 @@ function resolveSettings(raw) {
|
|
|
309
317
|
"gateCommand",
|
|
310
318
|
"selfcheck",
|
|
311
319
|
"stryker",
|
|
312
|
-
"lcov"
|
|
320
|
+
"lcov",
|
|
321
|
+
"imports",
|
|
322
|
+
"mutation"
|
|
313
323
|
],
|
|
314
324
|
"settings"
|
|
315
325
|
);
|
|
@@ -410,6 +420,42 @@ function resolveSettings(raw) {
|
|
|
410
420
|
"settings.selfcheck.failurePattern"
|
|
411
421
|
);
|
|
412
422
|
}
|
|
423
|
+
const importsSection = section(settings.imports, ["mode", "allow"], "settings.imports");
|
|
424
|
+
if (importsSection.mode !== void 0 && importsSection.mode !== "blocklist" && importsSection.mode !== "allowlist") {
|
|
425
|
+
throw new SettingsError('settings.imports.mode: must be "blocklist" or "allowlist"');
|
|
426
|
+
}
|
|
427
|
+
const importAllowRaw = importsSection.allow ?? [];
|
|
428
|
+
if (!Array.isArray(importAllowRaw)) {
|
|
429
|
+
throw new SettingsError("settings.imports.allow: must be an array");
|
|
430
|
+
}
|
|
431
|
+
const importAllow = importAllowRaw.map((item, index) => {
|
|
432
|
+
const at = `settings.imports.allow[${index}]`;
|
|
433
|
+
if (typeof item === "string" && item !== "") {
|
|
434
|
+
return { module: item };
|
|
435
|
+
}
|
|
436
|
+
if (isRecord2(item) && typeof item.pattern === "string" && item.pattern !== "") {
|
|
437
|
+
rejectUnknownKeys(item, ["pattern", "flags"], at);
|
|
438
|
+
if (item.flags !== void 0 && typeof item.flags !== "string") {
|
|
439
|
+
throw new SettingsError(`${at}.flags: must be a string`);
|
|
440
|
+
}
|
|
441
|
+
return { regex: compile(item.pattern, item.flags, at) };
|
|
442
|
+
}
|
|
443
|
+
if (isRecord2(item) && typeof item.module === "string" && item.module !== "") {
|
|
444
|
+
rejectUnknownKeys(item, ["module", "names"], at);
|
|
445
|
+
const names = stringArray(item.names, `${at}.names`);
|
|
446
|
+
if (names.length === 0) {
|
|
447
|
+
throw new SettingsError(`${at}.names: must list at least one name`);
|
|
448
|
+
}
|
|
449
|
+
return { module: item.module, names };
|
|
450
|
+
}
|
|
451
|
+
throw new SettingsError(
|
|
452
|
+
`${at}: must be a package name, { pattern, flags? } or { module, names }`
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
const mutation = section(settings.mutation, ["maxTimeouts"], "settings.mutation");
|
|
456
|
+
if (mutation.maxTimeouts !== void 0 && !(Number.isInteger(mutation.maxTimeouts) && mutation.maxTimeouts >= 0)) {
|
|
457
|
+
throw new SettingsError("settings.mutation.maxTimeouts: must be an integer >= 0");
|
|
458
|
+
}
|
|
413
459
|
const stryker = section(settings.stryker, ["configFile", "reportFile"], "settings.stryker");
|
|
414
460
|
const lcov = section(settings.lcov, ["file", "summaryExclude"], "settings.lcov");
|
|
415
461
|
return {
|
|
@@ -431,6 +477,9 @@ function resolveSettings(raw) {
|
|
|
431
477
|
selfcheckFailurePattern,
|
|
432
478
|
strykerConfigFile: optionalString(stryker.configFile, "settings.stryker.configFile"),
|
|
433
479
|
strykerReportFile: optionalString(stryker.reportFile, "settings.stryker.reportFile") ?? DEFAULT_REPORT_FILE,
|
|
480
|
+
importMode: importsSection.mode ?? "blocklist",
|
|
481
|
+
importAllow,
|
|
482
|
+
maxTimeouts: mutation.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS,
|
|
434
483
|
lcovFile: optionalString(lcov.file, "settings.lcov.file"),
|
|
435
484
|
lcovSummaryExclude: lcov.summaryExclude === void 0 ? [] : stringArray(lcov.summaryExclude, "settings.lcov.summaryExclude").map(
|
|
436
485
|
(pattern, index) => compile(pattern, void 0, `settings.lcov.summaryExclude[${index}]`)
|