@t09tanaka/test-gates 0.0.0-stage → 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.
@@ -0,0 +1,377 @@
1
+ /** One allowed equivalent mutant. Matched by mutator + original + replacement, never by line number. */
2
+ interface EquivalentMutant {
3
+ mutator: string;
4
+ original: string;
5
+ replacement: string;
6
+ reason: string;
7
+ /** 1-based index among the mutants of the file that share mutator + original + replacement. */
8
+ occurrence?: number;
9
+ }
10
+ interface Gate {
11
+ path: string;
12
+ decides: string;
13
+ impact: string;
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[];
20
+ }
21
+ interface Candidate {
22
+ path: string;
23
+ decides: string;
24
+ blocker: string;
25
+ }
26
+ /** A regular expression written in JSON. A bare string is shorthand for `{ pattern }`. */
27
+ interface PatternRule {
28
+ pattern: string;
29
+ flags?: string;
30
+ reason?: string;
31
+ }
32
+ interface NamedImportRule {
33
+ module: string;
34
+ names: string[];
35
+ reason?: string;
36
+ }
37
+ interface ImportAlias {
38
+ prefix: string;
39
+ target: string;
40
+ }
41
+ interface SpecRewrite {
42
+ from: string;
43
+ to: string;
44
+ }
45
+ /** `settings` of test-gates.json. Everything is optional. */
46
+ interface Settings {
47
+ gateExtensions?: string[];
48
+ spec?: {
49
+ suffixes?: string[];
50
+ rewrite?: SpecRewrite[];
51
+ };
52
+ impureImports?: {
53
+ defaults?: boolean;
54
+ add?: (string | PatternRule)[];
55
+ allow?: string[];
56
+ };
57
+ impureNamedImports?: {
58
+ defaults?: boolean;
59
+ add?: NamedImportRule[];
60
+ };
61
+ impurePaths?: {
62
+ defaults?: boolean;
63
+ add?: (string | PatternRule)[];
64
+ };
65
+ forbiddenSource?: {
66
+ defaults?: boolean;
67
+ add?: (string | PatternRule)[];
68
+ };
69
+ importAliases?: ImportAlias[];
70
+ gateCommand?: string[];
71
+ selfcheck?: {
72
+ mode?: 'all' | 'first';
73
+ failurePattern?: string;
74
+ };
75
+ stryker?: {
76
+ configFile?: string;
77
+ reportFile?: string;
78
+ };
79
+ lcov?: {
80
+ file?: string;
81
+ summaryExclude?: string[];
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
+ };
98
+ }
99
+ interface Manifest {
100
+ $schema?: string;
101
+ gates: Gate[];
102
+ candidates: Candidate[];
103
+ settings?: Settings;
104
+ }
105
+ interface CompiledRule {
106
+ regex: RegExp;
107
+ reason: string;
108
+ }
109
+ /** Settings with defaults applied and every pattern compiled. */
110
+ interface ResolvedSettings {
111
+ gateExtensions: string[];
112
+ /** `null` means "derive `.spec<ext>` and `.test<ext>` from the gate's extension". */
113
+ specSuffixes: string[] | null;
114
+ specRewrite: {
115
+ regex: RegExp;
116
+ to: string;
117
+ }[];
118
+ impureImports: CompiledRule[];
119
+ allowedImports: RegExp[];
120
+ impureNamedImports: Required<NamedImportRule>[];
121
+ impurePaths: CompiledRule[];
122
+ forbiddenSource: CompiledRule[];
123
+ importAliases: ImportAlias[];
124
+ gateCommand: string[] | null;
125
+ selfcheckMode: 'all' | 'first';
126
+ selfcheckFailurePattern: string | null;
127
+ strykerConfigFile: string | null;
128
+ strykerReportFile: string;
129
+ lcovFile: string | null;
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;
139
+ }
140
+ /** One finding. Printed as `file:line: message`. */
141
+ interface Violation {
142
+ file: string;
143
+ line?: number;
144
+ message: string;
145
+ }
146
+
147
+ interface ResolvedGate extends Gate {
148
+ /** Where the spec may live, in order of preference (relative to the subproject). */
149
+ specCandidates: string[];
150
+ /**
151
+ * The spec that exists. When none exists this is the first candidate, so that a config built
152
+ * from it fails on the missing coverage instead of silently dropping the gate. `null` only
153
+ * when the project has no spec convention (`settings.spec.suffixes: []`).
154
+ */
155
+ spec: string | null;
156
+ /** Every candidate that exists. More than one is ambiguous and `test-gates check` rejects it. */
157
+ existingSpecs: string[];
158
+ }
159
+ interface LoadedManifest {
160
+ /** Absolute path of the subproject. */
161
+ dir: string;
162
+ gates: ResolvedGate[];
163
+ candidates: Candidate[];
164
+ settings: ResolvedSettings;
165
+ /** Problems inside the manifest entries (`test-gates check` reports them). */
166
+ violations: Violation[];
167
+ }
168
+ /** Reads `<dir>/test-gates.json`. Throws ManifestError when it cannot be used at all. */
169
+ declare function readManifest(dir: string): LoadedManifest;
170
+ /**
171
+ * Reads and validates `<dir>/test-gates.json` for use in a config file.
172
+ * Throws when the manifest has any structural problem: a config derived from a broken list
173
+ * would gate the wrong set of files.
174
+ */
175
+ declare function loadGates(dir?: string): LoadedManifest;
176
+ /**
177
+ * Specs the gate run executes: the spec of every gate, minus the one `test-gates selfcheck`
178
+ * asks to leave out through TEST_GATES_EXCLUDE_SPEC.
179
+ */
180
+ declare function gateSpecs(manifest: LoadedManifest, env?: Record<string, string | undefined>): string[];
181
+
182
+ /** Every static rule of the gates. Returns all violations, not just the first. */
183
+ declare function checkGates(manifest: LoadedManifest): Violation[];
184
+
185
+ declare const MANIFEST_FILE = "test-gates.json";
186
+ /** The manifest cannot be used at all (exit code 2), as opposed to a violation inside it (exit code 1). */
187
+ declare class ManifestError extends Error {
188
+ }
189
+ interface ParsedManifest {
190
+ /** Entries whose `path` is usable. Other problems of an entry are reported in `violations`. */
191
+ gates: Gate[];
192
+ candidates: Candidate[];
193
+ settings: unknown;
194
+ violations: Violation[];
195
+ }
196
+ /**
197
+ * Parses test-gates.json. Throws ManifestError when the file is not usable at all;
198
+ * returns per-entry problems as violations.
199
+ */
200
+ declare function parseManifest(text: string): ParsedManifest;
201
+
202
+ /** Where the Stryker JSON report is written and read when `settings.stryker.reportFile` is not set. */
203
+ declare const DEFAULT_REPORT_FILE = "reports/mutation/mutation.json";
204
+ declare const DEFAULT_GATE_EXTENSIONS: string[];
205
+ /**
206
+ * Modules a pure module must not import at runtime. `import type` is never checked.
207
+ * `@prisma/client` is deliberately absent: `Prisma.Decimal` and enums are values a pure
208
+ * module may use. Only `PrismaClient` is banned, through DEFAULT_IMPURE_NAMED_IMPORTS.
209
+ */
210
+ declare const DEFAULT_IMPURE_IMPORTS: PatternRule[];
211
+ declare const DEFAULT_IMPURE_NAMED_IMPORTS: Required<NamedImportRule>[];
212
+ /** Paths that cannot be gates. */
213
+ declare const DEFAULT_IMPURE_PATHS: PatternRule[];
214
+ /** Lines that must not appear in a gate. Each pattern is tested against one line at a time. */
215
+ declare const DEFAULT_FORBIDDEN_SOURCE: PatternRule[];
216
+ /**
217
+ * Applies defaults to `settings` of test-gates.json and compiles every pattern.
218
+ * Throws SettingsError on anything it does not understand: a typo in a setting must not
219
+ * silently fall back to a default.
220
+ */
221
+ declare function resolveSettings(raw: unknown): ResolvedSettings;
222
+
223
+ interface Position {
224
+ line: number;
225
+ column: number;
226
+ }
227
+ interface ReportLocation {
228
+ start: Position;
229
+ end: Position;
230
+ }
231
+ interface ReportMutant {
232
+ mutatorName: string;
233
+ replacement?: string;
234
+ status: string;
235
+ location: ReportLocation;
236
+ }
237
+ interface ReportFile {
238
+ source?: string;
239
+ mutants?: ReportMutant[];
240
+ }
241
+ /** The part of the mutation-testing-elements JSON report that the verdict depends on. */
242
+ interface MutationReport {
243
+ files?: Record<string, ReportFile>;
244
+ }
245
+ /** A survivor that no allowance covers. `allowance` can be pasted into `equivalentMutants`. */
246
+ interface UnallowedSurvivor {
247
+ file: string;
248
+ line: number;
249
+ column: number;
250
+ status: string;
251
+ mutator: string;
252
+ original: string;
253
+ replacement: string;
254
+ /** 1-based position among the mutants of the file with the same mutator/original/replacement. */
255
+ occurrence: number;
256
+ /** How many mutants of the file share that key. `occurrence` is required when this is > 1. */
257
+ sameKeyCount: number;
258
+ allowance: EquivalentMutant;
259
+ }
260
+ interface MutationSummary {
261
+ total: number;
262
+ /** Killed + Timeout. */
263
+ detected: number;
264
+ /**
265
+ * The Timeout part of `detected`. Shown on its own: under heavy load mutants time out that
266
+ * would otherwise survive, so a high number means the run may be hiding survivors.
267
+ */
268
+ timeout: number;
269
+ /** Survived / NoCoverage mutants matched by an allowance. */
270
+ allowed: number;
271
+ /** RuntimeError + CompileError: the mutated code is not a program, so nothing was evaluated. */
272
+ notEvaluable: number;
273
+ /** Survived / NoCoverage mutants with no allowance. */
274
+ unallowed: number;
275
+ /** The part of `timeout` that `expectedTimeouts` entries cover. */
276
+ expectedTimeout: number;
277
+ }
278
+ interface MutationVerdict {
279
+ violations: Violation[];
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[];
286
+ summary: MutationSummary;
287
+ }
288
+ /** Whitespace is not part of the key, so reformatting a gate does not invalidate an allowance. */
289
+ declare function normalizeCode(text: string): string;
290
+ /**
291
+ * Cuts the text a report location points at. Lines and columns are 1-based and the end is
292
+ * exclusive. Returns null when the location is outside the source.
293
+ */
294
+ declare function sliceByLocation(source: string, location: ReportLocation): string | null;
295
+ /**
296
+ * Decides whether a Stryker run passes.
297
+ *
298
+ * Killed and Timeout mutants are detected. Survived and NoCoverage mutants must each be covered
299
+ * by one allowance of the gate (`equivalentMutants`), matched by mutator + original source text
300
+ * + replacement. RuntimeError and CompileError are counted but do not fail. Ignored, Pending
301
+ * and any unknown status fail: a mutant that was not evaluated proves nothing.
302
+ *
303
+ * An allowance fails the run when it matches no surviving mutant (stale), when it matches
304
+ * several mutants and has no `occurrence`, or when two allowances point at the same mutant.
305
+ *
306
+ * @param input.readSource Source of a gate on disk; `undefined` when it cannot be read. Used
307
+ * when the report carries no source, and to reject a report made from an older file.
308
+ */
309
+ declare function judgeMutationReport(input: {
310
+ gates: Gate[];
311
+ report: MutationReport;
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;
318
+ }): MutationVerdict;
319
+ /** `file:line:column / mutator / original → replacement / occurrence n of m`. */
320
+ declare function formatSurvivor(survivor: UnallowedSurvivor): string;
321
+ /** The real numbers, on one line. Deliberately not a percentage. */
322
+ declare function formatSummary(summary: MutationSummary): string;
323
+
324
+ interface LcovFile {
325
+ /** Instrumented lines. */
326
+ found: number;
327
+ /** Instrumented lines executed at least once. */
328
+ hit: number;
329
+ /** Instrumented lines never executed, ascending. Empty when the record has no DA entries. */
330
+ uncovered: number[];
331
+ }
332
+ /**
333
+ * Reads an lcov tracefile into per-file line coverage.
334
+ *
335
+ * When a file appears in several records (merged tracefiles), lines are united through their
336
+ * `DA:` entries, so a line counts as hit if any record hit it. A record without `DA:` entries
337
+ * contributes its `LF:` / `LH:` totals instead.
338
+ *
339
+ * @param rootDir When given, absolute `SF:` paths under it are made relative to it.
340
+ */
341
+ declare function parseLcov(text: string, rootDir?: string): Map<string, LcovFile>;
342
+ interface LcovGateResult {
343
+ path: string;
344
+ found: number;
345
+ hit: number;
346
+ ok: boolean;
347
+ }
348
+ interface LcovVerdict {
349
+ violations: Violation[];
350
+ results: LcovGateResult[];
351
+ }
352
+ /**
353
+ * Requires 100% line coverage for every gate. A gate with no record (never loaded by a test)
354
+ * or with no instrumented line fails: an absent measurement is not a pass.
355
+ */
356
+ declare function judgeLcov(gatePaths: string[], coverage: Map<string, LcovFile>): LcovVerdict;
357
+
358
+ /** Environment variable through which `test-gates selfcheck` tells the gate config which spec to leave out. */
359
+ declare const EXCLUDE_ENV = "TEST_GATES_EXCLUDE_SPEC";
360
+ /**
361
+ * Decides whether one negative control held: with the spec of `gatePath` left out, the gate
362
+ * command must fail, and it must fail because of the coverage threshold of that very file.
363
+ * A run that fails for another reason (a config that does not load) proves nothing.
364
+ *
365
+ * @param input.status Exit code of the gate command; null when it was killed or never started.
366
+ * @param input.failurePattern Replaces the built-in threshold detection. `{gate}` stands for
367
+ * the gate path. Tested against the whole output.
368
+ * @returns Why the control did not hold, or null when it held.
369
+ */
370
+ declare function judgeSelfcheck(input: {
371
+ status: number | null;
372
+ output: string;
373
+ gatePath: string;
374
+ failurePattern?: string | null;
375
+ }): string | null;
376
+
377
+ export { type Candidate, DEFAULT_FORBIDDEN_SOURCE, DEFAULT_GATE_EXTENSIONS, DEFAULT_IMPURE_IMPORTS, DEFAULT_IMPURE_NAMED_IMPORTS, DEFAULT_IMPURE_PATHS, DEFAULT_REPORT_FILE, EXCLUDE_ENV, type EquivalentMutant, type Gate, type LoadedManifest, MANIFEST_FILE, type Manifest, ManifestError, type MutationReport, type MutationSummary, type MutationVerdict, type ResolvedGate, type ResolvedSettings, type Settings, type UnallowedSurvivor, type Violation, checkGates, formatSummary, formatSurvivor, gateSpecs, judgeLcov, judgeMutationReport, judgeSelfcheck, loadGates, normalizeCode, parseLcov, parseManifest, readManifest, resolveSettings, sliceByLocation };