@specific.dev/spectest 0.131.20 → 0.132.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/dist/browser.js +4 -2
- package/dist/change-rules.d.ts +45 -0
- package/dist/change-rules.js +179 -0
- package/dist/components/k3s-coverage.js +4 -4
- package/dist/components/supabase-coverage.js +12 -0
- package/dist/components/supabase.js +25 -12
- package/dist/coverage-bun.js +2 -1
- package/dist/coverage-next.d.ts +5 -0
- package/dist/coverage-next.js +27 -3
- package/dist/coverage-rust.d.ts +2 -0
- package/dist/coverage-rust.js +20 -0
- package/dist/coverage.d.ts +27 -3
- package/dist/coverage.js +68 -15
- package/dist/daemon.js +13 -10
- package/dist/harness/component-images.d.ts +5 -0
- package/dist/harness/component-images.js +15 -2
- package/dist/harness/coverage-target.d.ts +2 -0
- package/dist/harness/coverage-target.js +3 -2
- package/dist/harness/reach.d.ts +6 -1
- package/dist/harness/reach.js +8 -34
- package/dist/harness/shadow-builder.d.ts +2 -1
- package/dist/harness/shadow-builder.js +9 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/vendor/rrweb-record.min.js +2 -17
- package/package.json +1 -1
- package/src/browser-coverage-navigation.test.ts +10 -2
- package/src/browser-image-loading.test.ts +51 -0
- package/src/browser.ts +4 -2
- package/src/change-rules.fixtures.json +35 -0
- package/src/change-rules.test.ts +125 -0
- package/src/change-rules.ts +147 -0
- package/src/components/k3s-coverage.test.ts +32 -0
- package/src/components/k3s-coverage.ts +4 -4
- package/src/components/supabase-coverage.ts +12 -0
- package/src/components/supabase-dir.test.ts +26 -0
- package/src/components/supabase-functions-reach.test.ts +41 -1
- package/src/components/supabase-migrations.test.ts +1 -1
- package/src/components/supabase.ts +26 -12
- package/src/coverage-bun.ts +2 -1
- package/src/coverage-next.ts +29 -3
- package/src/coverage-rust.test.ts +34 -0
- package/src/coverage-rust.ts +22 -0
- package/src/coverage.test.ts +2 -1
- package/src/coverage.ts +76 -7
- package/src/daemon.ts +14 -9
- package/src/harness/component-images.test.ts +42 -0
- package/src/harness/component-images.ts +20 -3
- package/src/harness/coverage-target.ts +5 -2
- package/src/harness/reach.test.ts +40 -6
- package/src/harness/reach.ts +13 -3
- package/src/harness/shadow-builder.test.ts +12 -1
- package/src/harness/shadow-builder.ts +10 -3
- package/src/index.ts +11 -0
- package/src/vendor/rrweb-record.min.js +2 -17
package/dist/browser.js
CHANGED
|
@@ -289,7 +289,8 @@ async function applySafeAreaInsets(cdp, d) {
|
|
|
289
289
|
// ────────────────────────────────────────────────────────────────────────
|
|
290
290
|
// Vendored `@rrweb/record` UMD bundle (rrweb 2.x), BUILT FROM OUR FORK by
|
|
291
291
|
// `scripts/rrweb-record/build.sh` (specific-dev/rrweb, branch `spectest` =
|
|
292
|
-
// the `rrweb@2.1.0` tag + upstream PR 1802
|
|
292
|
+
// the `rrweb@2.1.0` tag + upstream PR 1802 + non-mutating image capture).
|
|
293
|
+
// Never copy the npm dist over
|
|
293
294
|
// it: upstream's `@rrweb/utils` lists `querySelector` as an accessor, so its
|
|
294
295
|
// pristine-prototype test always fails and `record()` appends an <iframe>
|
|
295
296
|
// to every document — measured at ~1 s per navigation (see the top-frame
|
|
@@ -380,7 +381,8 @@ const RRWEB_BOOTSTRAP = `
|
|
|
380
381
|
// CSS (what next/font emits), so it's a no-op for most apps; the
|
|
381
382
|
// url()-based fonts are handled by the inliner below instead. All
|
|
382
383
|
// degrade gracefully — an asset that can't be read is skipped,
|
|
383
|
-
// never fatal to recording.
|
|
384
|
+
// never fatal to recording. Our fork never changes a live image's
|
|
385
|
+
// crossorigin attribute to retry a failed canvas read.
|
|
384
386
|
inlineStylesheet: true,
|
|
385
387
|
collectFonts: true,
|
|
386
388
|
inlineImages: true,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Portable source-change policy, evaluated identically by the SDK and controller.
|
|
2
|
+
* Globs match whole root-relative paths: *, ?, [], [!], ** and backslash escapes.
|
|
3
|
+
* Braces/extglobs have no special meaning. Directory rules explicitly use /**. */
|
|
4
|
+
export type ChangeAction = "coverage" | "full" | "ignore";
|
|
5
|
+
export interface ChangeRule {
|
|
6
|
+
root?: string;
|
|
7
|
+
pattern: string;
|
|
8
|
+
action: ChangeAction;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedChangeRule extends ChangeRule {
|
|
11
|
+
root: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ChangePolicy {
|
|
14
|
+
defaults: readonly (readonly ResolvedChangeRule[])[];
|
|
15
|
+
overrides: readonly ResolvedChangeRule[];
|
|
16
|
+
}
|
|
17
|
+
export interface ChangeRuleAdapter {
|
|
18
|
+
changeRules?: readonly ChangeRule[];
|
|
19
|
+
/** Wrappers preserve independent nested adapter policies, including roots. */
|
|
20
|
+
changeRuleGroups?: readonly (readonly ChangeRule[])[];
|
|
21
|
+
sourceOptions?: {
|
|
22
|
+
sourceRoot?: string;
|
|
23
|
+
} | ((...args: any[]) => unknown);
|
|
24
|
+
}
|
|
25
|
+
export interface ChangeRuleService {
|
|
26
|
+
/** Explicit rules override all defaults. false disables defaults. */
|
|
27
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
28
|
+
coverage?: boolean | {
|
|
29
|
+
sourceRoot?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function compileChangeGlob(pattern: string): RegExp;
|
|
33
|
+
export declare function matchesChangeGlob(pattern: string, path: string): boolean;
|
|
34
|
+
export declare function validateChangeRules(value: unknown): asserts value is readonly ChangeRule[];
|
|
35
|
+
export declare const COMMON_CHANGE_RULES: readonly ChangeRule[];
|
|
36
|
+
export declare const JAVASCRIPT_CHANGE_RULES: readonly ChangeRule[];
|
|
37
|
+
export declare const NODE_CHANGE_RULES: readonly ChangeRule[];
|
|
38
|
+
export declare const BROWSER_CHANGE_RULES: readonly ChangeRule[];
|
|
39
|
+
export declare const BUN_CHANGE_RULES: readonly ChangeRule[];
|
|
40
|
+
export declare const NEXT_CHANGE_RULES: readonly ChangeRule[];
|
|
41
|
+
export declare const RUST_CHANGE_RULES: readonly ChangeRule[];
|
|
42
|
+
export declare function resolveChangePolicy(service: ChangeRuleService, adapters?: readonly ChangeRuleAdapter[]): ChangePolicy;
|
|
43
|
+
/** Compile once for scans over a source tree; regexes carry no mutable flags. */
|
|
44
|
+
export declare function compileChangePolicy(policy: ChangePolicy): (path: string) => ChangeAction | undefined;
|
|
45
|
+
export declare function evaluateChangePolicy(policy: ChangePolicy, path: string): ChangeAction | undefined;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const literal = (c) => /[\\^$.*+?()[\]{}|/]/u.test(c) ? `\\${c}` : c;
|
|
2
|
+
const classLiteral = (c) => ["\\", "[", "]", "^", "-"].includes(c) ? `\\${c}` : c;
|
|
3
|
+
export function compileChangeGlob(pattern) {
|
|
4
|
+
if (!pattern || pattern.includes("\0") || pattern.startsWith("/") || pattern.split("/").some(p => p === "" || p === "." || p === ".."))
|
|
5
|
+
throw new Error("change-rule pattern must be a nonempty relative glob without traversal");
|
|
6
|
+
const chars = [...pattern];
|
|
7
|
+
let out = "";
|
|
8
|
+
for (let i = 0; i < chars.length; i++) {
|
|
9
|
+
const c = chars[i];
|
|
10
|
+
if (c === "\\") {
|
|
11
|
+
const next = chars[++i];
|
|
12
|
+
if (next === undefined || next === "/")
|
|
13
|
+
throw new Error("invalid glob escape");
|
|
14
|
+
out += literal(next);
|
|
15
|
+
}
|
|
16
|
+
else if (c === "*") {
|
|
17
|
+
if (chars[i + 1] === "*") {
|
|
18
|
+
i++;
|
|
19
|
+
if ((i > 1 && chars[i - 2] !== "/") || (i + 1 < chars.length && chars[i + 1] !== "/"))
|
|
20
|
+
throw new Error("** must occupy a whole path segment");
|
|
21
|
+
if (chars[i + 1] === "/") {
|
|
22
|
+
i++;
|
|
23
|
+
out += "(?:[\\s\\S]*/)?";
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
out += "[\\s\\S]*";
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
out += "[^/]*";
|
|
30
|
+
}
|
|
31
|
+
else if (c === "?")
|
|
32
|
+
out += "[^/]";
|
|
33
|
+
else if (c === "[") {
|
|
34
|
+
let negate = false;
|
|
35
|
+
if (chars[i + 1] === "!") {
|
|
36
|
+
negate = true;
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
const members = [];
|
|
40
|
+
let closed = false;
|
|
41
|
+
while (++i < chars.length) {
|
|
42
|
+
let ch = chars[i];
|
|
43
|
+
if (ch === "]") {
|
|
44
|
+
closed = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
let escaped = false;
|
|
48
|
+
if (ch === "\\") {
|
|
49
|
+
ch = chars[++i];
|
|
50
|
+
escaped = true;
|
|
51
|
+
if (ch === undefined)
|
|
52
|
+
throw new Error("invalid class escape");
|
|
53
|
+
}
|
|
54
|
+
if (ch === "/")
|
|
55
|
+
throw new Error("glob classes cannot match separators");
|
|
56
|
+
members.push({ c: ch, escaped });
|
|
57
|
+
}
|
|
58
|
+
if (!closed || !members.length)
|
|
59
|
+
throw new Error("unterminated or empty glob class");
|
|
60
|
+
let body = "";
|
|
61
|
+
for (let j = 0; j < members.length; j++) {
|
|
62
|
+
const m = members[j];
|
|
63
|
+
if (members[j + 1]?.c === "-" && !members[j + 1].escaped && members[j + 2]) {
|
|
64
|
+
const end = members[j + 2];
|
|
65
|
+
if (m.c.codePointAt(0) > end.c.codePointAt(0) || (m.c.codePointAt(0) <= 47 && end.c.codePointAt(0) >= 47))
|
|
66
|
+
throw new Error("invalid glob range");
|
|
67
|
+
body += `${classLiteral(m.c)}-${classLiteral(end.c)}`;
|
|
68
|
+
j += 2;
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
body += classLiteral(m.c);
|
|
72
|
+
}
|
|
73
|
+
// Prefix slash in a negated class so it remains segment-scoped.
|
|
74
|
+
out += `[${negate ? "^/" : ""}${body}]`;
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
out += literal(c);
|
|
78
|
+
}
|
|
79
|
+
return new RegExp(`^(?:${out})$(?![\\s\\S])`, "u");
|
|
80
|
+
}
|
|
81
|
+
export function matchesChangeGlob(pattern, path) { return compileChangeGlob(pattern).test(path); }
|
|
82
|
+
export function validateChangeRules(value) {
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
throw new Error("changeRules must be an array or false");
|
|
85
|
+
for (const rule of value) {
|
|
86
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule) || !Object.keys(rule).every(k => ["root", "pattern", "action"].includes(k)) || typeof rule.pattern !== "string" || !["coverage", "full", "ignore"].includes(rule.action))
|
|
87
|
+
throw new Error("invalid change rule");
|
|
88
|
+
if (rule.root !== undefined && (typeof rule.root !== "string" || (rule.root !== "." && (!rule.root || rule.root.includes("\\") || rule.root.includes("\0") || rule.root.split("/").some((p) => !p || p === "." || p === "..")))))
|
|
89
|
+
throw new Error("change-rule root must be repository-relative");
|
|
90
|
+
compileChangeGlob(rule.pattern);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const freeze = (rules) => Object.freeze(rules.map(r => Object.freeze(r)));
|
|
94
|
+
export const COMMON_CHANGE_RULES = freeze(["**/*.md", "**/*.markdown", "**/*.MD", "**/*.MARKDOWN"].map(pattern => ({ pattern, action: "ignore" })));
|
|
95
|
+
const extensions = ["js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts"];
|
|
96
|
+
const full = (pattern) => ({ pattern, action: "full" });
|
|
97
|
+
const ignore = (pattern) => ({ pattern, action: "ignore" });
|
|
98
|
+
const coverage = (pattern) => ({ pattern, action: "coverage" });
|
|
99
|
+
export const JAVASCRIPT_CHANGE_RULES = freeze([
|
|
100
|
+
...extensions.map(ext => coverage(`**/*.${ext}`)),
|
|
101
|
+
full("**/migrations/**"),
|
|
102
|
+
...extensions.flatMap(ext => [full(`**/*.config.${ext}`), full(`**/.babelrc.${ext}`), full(`**/.pnp.${ext}`), full(`**/.pnp.loader.${ext}`)]),
|
|
103
|
+
...["**/tsconfig*.json", "**/jsconfig*.json", "**/.babelrc", "**/.babelrc.json", "**/.swcrc", "**/.env", "**/.env.*"].map(full),
|
|
104
|
+
...extensions.flatMap(ext => [ignore(`**/test.${ext}`), ignore(`**/spec.${ext}`)]),
|
|
105
|
+
...extensions.flatMap(ext => [".test", ".spec", "_test", "_spec", ".test-d", ".spec-d", ".cy"].map(suffix => ignore(`**/*${suffix}.${ext}`))),
|
|
106
|
+
...["**/__tests__/**", "**/__mocks__/**", "**/__snapshots__/**", "test/**", "tests/**", "e2e/**", "cypress/**", "playwright/**"].map(ignore),
|
|
107
|
+
...[...extensions, "json"].flatMap(ext => ["jest", "vitest", "playwright", "cypress"].map(tool => ignore(`**/${tool}.config.${ext}`))),
|
|
108
|
+
...COMMON_CHANGE_RULES,
|
|
109
|
+
]);
|
|
110
|
+
export const NODE_CHANGE_RULES = JAVASCRIPT_CHANGE_RULES;
|
|
111
|
+
export const BROWSER_CHANGE_RULES = JAVASCRIPT_CHANGE_RULES;
|
|
112
|
+
export const BUN_CHANGE_RULES = freeze([...NODE_CHANGE_RULES, full("**/bunfig.toml")]);
|
|
113
|
+
const sharedNext = ["layout", "template", "error", "global-error", "loading", "not-found", "default", "forbidden", "unauthorized", "middleware", "proxy", "instrumentation", "instrumentation-client", "mdx-components", "_app", "_document", "_error"];
|
|
114
|
+
export const NEXT_CHANGE_RULES = freeze([
|
|
115
|
+
...JAVASCRIPT_CHANGE_RULES,
|
|
116
|
+
// Pages-router files are routes even if their names resemble unit tests.
|
|
117
|
+
...["pages", "src/pages"].flatMap(root => extensions.map(ext => coverage(`${root}/**/*.${ext}`))),
|
|
118
|
+
...["app", "src/app"].flatMap(root => extensions.flatMap(ext => [coverage(`${root}/**/page.${ext}`), coverage(`${root}/**/route.${ext}`)])),
|
|
119
|
+
full("**/*.mdx"), full("**/*.MDX"),
|
|
120
|
+
...extensions.flatMap(ext => sharedNext.map(name => full(`**/${name}.${ext}`))),
|
|
121
|
+
]);
|
|
122
|
+
export const RUST_CHANGE_RULES = freeze([
|
|
123
|
+
coverage("**/*.rs"), ignore("tests/**"), ignore("benches/**"),
|
|
124
|
+
...["**/Cargo.toml", "**/Cargo.lock", "**/.cargo/config", "**/.cargo/config.toml", "**/rust-toolchain", "**/rust-toolchain.toml", "**/build.rs", "src/bin/**"].map(full),
|
|
125
|
+
...COMMON_CHANGE_RULES,
|
|
126
|
+
]);
|
|
127
|
+
export function resolveChangePolicy(service, adapters = []) {
|
|
128
|
+
if (service.changeRules !== undefined && service.changeRules !== false)
|
|
129
|
+
validateChangeRules(service.changeRules);
|
|
130
|
+
const serviceRoot = typeof service.coverage === "object" ? service.coverage.sourceRoot ?? "." : ".";
|
|
131
|
+
const resolve = (rules, root) => {
|
|
132
|
+
validateChangeRules(rules);
|
|
133
|
+
const out = rules.map(r => ({ ...r, root: r.root ?? root }));
|
|
134
|
+
validateChangeRules(out);
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
const defaults = service.changeRules === false ? [] : [resolve([{ pattern: "spectest/**", action: "full" }, { pattern: "spectest/tests/**", action: "coverage" }, ...COMMON_CHANGE_RULES], ".")];
|
|
138
|
+
if (service.changeRules !== false)
|
|
139
|
+
for (const adapter of adapters) {
|
|
140
|
+
const groups = [...(adapter.changeRules ? [adapter.changeRules] : []), ...(adapter.changeRuleGroups ?? [])];
|
|
141
|
+
const root = typeof adapter.sourceOptions === "object" ? adapter.sourceOptions.sourceRoot ?? serviceRoot : serviceRoot;
|
|
142
|
+
for (const group of groups) {
|
|
143
|
+
if (typeof adapter.sourceOptions === "function" && group.some(r => r.root === undefined))
|
|
144
|
+
throw new Error("dynamic sourceOptions requires explicit change-rule roots");
|
|
145
|
+
defaults.push(resolve(group, root));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { defaults, overrides: resolve(service.changeRules || [], serviceRoot) };
|
|
149
|
+
}
|
|
150
|
+
/** Compile once for scans over a source tree; regexes carry no mutable flags. */
|
|
151
|
+
export function compileChangePolicy(policy) {
|
|
152
|
+
const compile = (rules) => rules.map(r => ({ ...r, regex: compileChangeGlob(r.pattern) }));
|
|
153
|
+
const defaults = policy.defaults.map(compile), overrides = compile(policy.overrides);
|
|
154
|
+
const groupAction = (rules, path) => {
|
|
155
|
+
let result;
|
|
156
|
+
for (const r of rules) {
|
|
157
|
+
const rel = r.root === "." ? path : path === r.root ? "" : path.startsWith(r.root + "/") ? path.slice(r.root.length + 1) : undefined;
|
|
158
|
+
if (rel !== undefined && r.regex.test(rel))
|
|
159
|
+
result = r.action;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
};
|
|
163
|
+
const rank = { ignore: 1, coverage: 2, full: 3 };
|
|
164
|
+
return path => {
|
|
165
|
+
const override = groupAction(overrides, path);
|
|
166
|
+
if (override)
|
|
167
|
+
return override;
|
|
168
|
+
let result;
|
|
169
|
+
for (const rules of defaults) {
|
|
170
|
+
const action = groupAction(rules, path);
|
|
171
|
+
if (action && (!result || rank[action] > rank[result]))
|
|
172
|
+
result = action;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function evaluateChangePolicy(policy, path) {
|
|
178
|
+
return compileChangePolicy(policy)(path);
|
|
179
|
+
}
|
|
@@ -13,13 +13,13 @@ const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
|
13
13
|
/** One component-owned bridge; runtime adapters remain unaware of Kubernetes. */
|
|
14
14
|
export function k3sCoverage(images) {
|
|
15
15
|
const declarations = images.flatMap(image => typeof image === "string" || image.coverage === undefined ? [] : [{
|
|
16
|
-
reference: canonicalImageReference(image.reference), coverage: image.coverage,
|
|
16
|
+
reference: canonicalImageReference(image.reference), coverage: image.coverage, changeRules: image.changeRules,
|
|
17
17
|
}]);
|
|
18
18
|
for (const image of declarations) {
|
|
19
19
|
const adapters = coverageAdapters(image.coverage);
|
|
20
20
|
if (adapters.some(adapter => adapter.load || adapter.collect))
|
|
21
21
|
throw new Error("k3s image coverage accepts container adapters; put browser coverage on the cluster service");
|
|
22
|
-
applyCoverageAdapters(image.reference, { image: { type: "registry", reference: image.reference }, coverage: image.coverage });
|
|
22
|
+
applyCoverageAdapters(image.reference, { image: { type: "registry", reference: image.reference }, coverage: image.coverage, changeRules: image.changeRules });
|
|
23
23
|
}
|
|
24
24
|
const metadata = new Map();
|
|
25
25
|
const service = (cluster, reference) => `${cluster}/image-${hash(reference)}`;
|
|
@@ -136,7 +136,7 @@ export function k3sCoverage(images) {
|
|
|
136
136
|
toJSON() {
|
|
137
137
|
return { adapter: "k3s-containers", images: declarations.map(image => ({
|
|
138
138
|
reference: image.reference,
|
|
139
|
-
config: applyCoverageAdapters(image.reference, { image: { type: "registry", reference: image.reference }, coverage: image.coverage }),
|
|
139
|
+
config: applyCoverageAdapters(image.reference, { image: { type: "registry", reference: image.reference }, coverage: image.coverage, changeRules: image.changeRules }),
|
|
140
140
|
})) };
|
|
141
141
|
},
|
|
142
142
|
async collect(ctx, baseline) {
|
|
@@ -191,7 +191,7 @@ export function k3sCoverage(images) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
const captured = await captureCoverageTarget({
|
|
194
|
-
coverage: declaration.coverage, root: instance.root, baseline,
|
|
194
|
+
coverage: declaration.coverage, changeRules: declaration.changeRules ?? ctx.changeRules, root: instance.root, baseline,
|
|
195
195
|
inventoryAvailable: baseline && adapters.some(a => a.inventory) ? !!await livePod() : false,
|
|
196
196
|
counterKey: `${service(ctx.service, instance.image)}/${id}`,
|
|
197
197
|
context: { ...ctx, service: service(ctx.service, instance.image), reportDir: dir,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { COMMON_CHANGE_RULES, NODE_CHANGE_RULES } from "../change-rules.js";
|
|
4
5
|
import { rawFetch } from "../harness/raw-fetch.js";
|
|
5
6
|
// Drain all work even on failure: no writes may outlive the capture boundary.
|
|
6
7
|
async function finishAll(pending) {
|
|
@@ -19,6 +20,17 @@ export function supabaseFunctionsCoverage(functionRoot) {
|
|
|
19
20
|
name: "supabase-functions",
|
|
20
21
|
reports: "delta",
|
|
21
22
|
sourceOptions: { sourceRoot: "." },
|
|
23
|
+
// Ordinary modules follow callers, including shared modules outside the
|
|
24
|
+
// functions directory. Concrete reach still limits which paths this
|
|
25
|
+
// service owns. Directory-discovered entrypoints remain in the inventory.
|
|
26
|
+
changeRules: [
|
|
27
|
+
...["**/deno.json", "**/deno.jsonc", "**/deno.lock", "**/import_map.json"].map(pattern => ({ pattern, action: "full" })),
|
|
28
|
+
...NODE_CHANGE_RULES,
|
|
29
|
+
...["config.toml", "config.json"].map(pattern => ({ root: path.posix.dirname(functionRoot), pattern, action: "full" })),
|
|
30
|
+
// Migration directory conventions in the JS preset must not make
|
|
31
|
+
// incidental Markdown defeat the shared documentation default.
|
|
32
|
+
...COMMON_CHANGE_RULES,
|
|
33
|
+
],
|
|
22
34
|
ownsReport: name => name.startsWith("edge-") || name.startsWith("edge-scripts/"),
|
|
23
35
|
async inventory(ctx) {
|
|
24
36
|
// Functions are discovered by directory, not by an import in user code.
|
|
@@ -67,6 +67,7 @@ import { SUPABASE_WEBHOOKS_SQL } from "./supabase-webhooks.js";
|
|
|
67
67
|
import { postgresTls } from "./postgres-tls.js";
|
|
68
68
|
import { supabasePoolerService } from "./supabase-pooler.js";
|
|
69
69
|
import { SUPABASE_COVERAGE_COLLECTOR, supabaseFunctionsCoverage } from "./supabase-coverage.js";
|
|
70
|
+
import { COMMON_CHANGE_RULES } from "../change-rules.js";
|
|
70
71
|
import { mapSupabaseConfig, parseSupabaseConfigFile, } from "./supabase-config.js";
|
|
71
72
|
// ──────────────────────────────────────────────────────────────────────────
|
|
72
73
|
// Digest-pinned upstream images. Update BUILTIN_IMAGES together for compatible
|
|
@@ -1449,20 +1450,32 @@ export function supabase(opts = {}) {
|
|
|
1449
1450
|
const realtimeHost = `realtime-dev.${g.key("realtime")}`;
|
|
1450
1451
|
const parts = {};
|
|
1451
1452
|
// ── db ──────────────────────────────────────────────────────────
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
];
|
|
1453
|
+
// Track actual setup inputs, including currently absent conventional
|
|
1454
|
+
// paths so newly added config/migrations still select the full suite.
|
|
1455
|
+
// Functions own their module graph separately; giving the database the
|
|
1456
|
+
// entire folder would veto every function's source-selection policy.
|
|
1457
|
+
const projectRelative = (absolute) => absolute === root ? "." : absolute.startsWith(root + "/") ? absolute.slice(root.length + 1) : undefined;
|
|
1458
|
+
const inputs = [...new Set([
|
|
1459
|
+
...(dirPath === null ? [] : ["config.toml", "config.json", "migrations", "seed.sql"].map(p => projectRelative(join(dirPath, p)))),
|
|
1460
|
+
...templateBodies.map(([, spec]) => projectRelative(absProjectPath(spec.path))),
|
|
1461
|
+
].filter((p) => p !== undefined))];
|
|
1462
|
+
const literalGlob = (p) => p.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1463
1463
|
const dbDef = {
|
|
1464
1464
|
image: { type: "registry", reference: opts.db?.image ?? IMAGES.db },
|
|
1465
1465
|
...(inputs.length > 0 ? { inputs } : {}),
|
|
1466
|
+
changeRules: [
|
|
1467
|
+
...inputs.flatMap(p => [
|
|
1468
|
+
{ pattern: literalGlob(p), action: "full" },
|
|
1469
|
+
{ pattern: `${literalGlob(p)}/**`, action: "full" },
|
|
1470
|
+
]),
|
|
1471
|
+
...COMMON_CHANGE_RULES,
|
|
1472
|
+
// A configured template is runtime content even if its extension
|
|
1473
|
+
// is Markdown. Its exact declaration overrides the common ignore.
|
|
1474
|
+
...templateBodies.flatMap(([, spec]) => {
|
|
1475
|
+
const p = projectRelative(absProjectPath(spec.path));
|
|
1476
|
+
return p === undefined ? [] : [{ pattern: literalGlob(p), action: "full" }];
|
|
1477
|
+
}),
|
|
1478
|
+
],
|
|
1466
1479
|
// `args` (not `command`): keep the image's docker-entrypoint.sh so it
|
|
1467
1480
|
// runs initdb + its baked init-scripts before starting.
|
|
1468
1481
|
args: [
|
|
@@ -1488,7 +1501,7 @@ export function supabase(opts = {}) {
|
|
|
1488
1501
|
// Read SQL directly, including large seeds, without copying it through
|
|
1489
1502
|
// the harness and docker exec stdin. Keep this out of PGDATA.
|
|
1490
1503
|
...(migrationsPath === null ? {} : {
|
|
1491
|
-
volumes: [{ source: dirname(migrationsPath), target: SQL_MOUNT, readOnly: true }],
|
|
1504
|
+
volumes: [{ source: dirname(migrationsPath), target: SQL_MOUNT, readOnly: true, reachExempt: true }],
|
|
1492
1505
|
}),
|
|
1493
1506
|
ports: [5432],
|
|
1494
1507
|
// `pg_isready` succeeds only once the image finishes initdb + its
|
package/dist/coverage-bun.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { BUN_CHANGE_RULES } from "./change-rules.js";
|
|
1
2
|
import { appendEnvFlag, node, NODE_COVERAGE_HOOK, NODE_COVERAGE_HOOK_PATH } from "./coverage.js";
|
|
2
3
|
import { access } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
export function bun(options = {}) {
|
|
5
|
-
const adapter = node(options);
|
|
6
|
+
const adapter = node({ ...options, changeRules: options.changeRules === undefined ? BUN_CHANGE_RULES : options.changeRules });
|
|
6
7
|
return {
|
|
7
8
|
...adapter,
|
|
8
9
|
name: "bun",
|
package/dist/coverage-next.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type ChangeRule } from "./change-rules.js";
|
|
1
2
|
import { type CoverageAdapter, type CoverageSourceOptions } from "./coverage.js";
|
|
2
3
|
export interface NextCoverageOptions extends CoverageSourceOptions {
|
|
4
|
+
/** Replace this adapter's defaults, or false to disable them. */
|
|
5
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
3
6
|
/** Repository-relative directory represented by the build's source-map root.
|
|
4
7
|
* Defaults to "." (repository root). Set explicitly for a nested build root. */
|
|
5
8
|
sourceRoot?: string;
|
|
@@ -14,6 +17,8 @@ export interface NextBuildMetadata {
|
|
|
14
17
|
generated: string | null;
|
|
15
18
|
serverMaps: number;
|
|
16
19
|
browserMaps: number;
|
|
20
|
+
pageExtensions?: string[];
|
|
21
|
+
appRoot?: string;
|
|
17
22
|
}
|
|
18
23
|
/** Server + browser coverage for a built Next app. Build-time maps are required;
|
|
19
24
|
* enable native Next source maps and retain them in the runtime image.
|
package/dist/coverage-next.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Next policy lives in the SDK. The collector/indexer only see ordinary
|
|
2
2
|
// Node/browser reports and generic, report-scoped source metadata.
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { NEXT_CHANGE_RULES } from "./change-rules.js";
|
|
4
5
|
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
5
6
|
import { node, browser } from "./coverage.js";
|
|
6
7
|
export const NEXT_COVERAGE_HELPER_PATH = "/spectest/next-coverage.cjs";
|
|
@@ -96,7 +97,10 @@ if (require.main === module) {
|
|
|
96
97
|
walk(path.join(dist, "server"), true);
|
|
97
98
|
walk(path.join(dist, "static"), false);
|
|
98
99
|
if (!serverMaps || !browserMaps) throw new Error("Next coverage needs server and browser .js.map files in the runtime image. Enable productionBrowserSourceMaps and experimental.serverSourceMaps in Next config, and copy .next/server and .next/static maps into the runtime image, including Edge maps.");
|
|
99
|
-
|
|
100
|
+
const appRoot = typeof m.relativeAppDir === "string" ? m.relativeAppDir :
|
|
101
|
+
(typeof m.appDir === "string" && typeof m.config.turbopack?.root === "string" ? path.relative(m.config.turbopack.root, m.appDir) : "");
|
|
102
|
+
process.stdout.write(JSON.stringify({generated:launcher(m), serverMaps, browserMaps,
|
|
103
|
+
pageExtensions:m.config.pageExtensions || ["tsx","ts","jsx","js"], appRoot}));
|
|
100
104
|
} catch (e) { console.error("Next coverage: " + e.message); process.exitCode = 1; }
|
|
101
105
|
}
|
|
102
106
|
`;
|
|
@@ -120,7 +124,7 @@ export function next(options = {}) {
|
|
|
120
124
|
if (value !== undefined && (!value || path.posix.isAbsolute(value) || value.split("/").some(p => p === "..") || /[\\\0\r\n]/.test(value)))
|
|
121
125
|
throw new Error(`coverage.next ${key} must be a relative runtime directory`);
|
|
122
126
|
}
|
|
123
|
-
const server = node(), client = browser();
|
|
127
|
+
const server = node({ sourceRoot, changeRules: options.changeRules === undefined ? NEXT_CHANGE_RULES : options.changeRules }), client = browser();
|
|
124
128
|
const cache = new Map();
|
|
125
129
|
const metadata = (ctx) => {
|
|
126
130
|
let value = cache.get(ctx.service);
|
|
@@ -133,7 +137,27 @@ export function next(options = {}) {
|
|
|
133
137
|
};
|
|
134
138
|
return {
|
|
135
139
|
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
136
|
-
name: "next", reports: "delta", explains: server.explains,
|
|
140
|
+
name: "next", reports: "delta", explains: server.explains, changeRules: server.changeRules?.map(rule => ({ ...rule, root: rule.root ?? sourceRoot })),
|
|
141
|
+
...(options.changeRules === undefined ? { async resolveChangeRules(ctx) {
|
|
142
|
+
const m = await metadata(ctx);
|
|
143
|
+
const extensions = m.pageExtensions ?? ["tsx", "ts", "jsx", "js"];
|
|
144
|
+
if (!Array.isArray(extensions) || extensions.some(ext => typeof ext !== "string" || !ext || /[^a-zA-Z0-9._-]/.test(ext)))
|
|
145
|
+
throw new Error("invalid Next pageExtensions metadata");
|
|
146
|
+
const appRoot = m.appRoot || ".";
|
|
147
|
+
if (appRoot !== "." && (appRoot.includes("\\") || appRoot.split("/").some(part => !part || part === "." || part === "..")))
|
|
148
|
+
throw new Error("invalid Next app root metadata");
|
|
149
|
+
const root = path.posix.join(sourceRoot, appRoot);
|
|
150
|
+
// Remove the static conventional route allowance; the build's extensions
|
|
151
|
+
// determine which files are actually routes (including custom test-like names).
|
|
152
|
+
const rules = NEXT_CHANGE_RULES.filter(rule => !/^(pages|src\/pages|app|src\/app)\//.test(rule.pattern));
|
|
153
|
+
const routes = extensions.flatMap(ext => [
|
|
154
|
+
...["pages", "src/pages"].map(dir => ({ root, pattern: `${dir}/**/*.${ext}`, action: "coverage" })),
|
|
155
|
+
...["app", "src/app"].flatMap(dir => ["page", "route"].map(name => ({ root, pattern: `${dir}/**/${name}.${ext}`, action: "coverage" }))),
|
|
156
|
+
]);
|
|
157
|
+
// Shared entries still require full runs, even when they are also routes.
|
|
158
|
+
const full = rules.filter(rule => rule.action === "full" && rule.pattern !== "**/migrations/**");
|
|
159
|
+
return [...rules.map(rule => ({ ...rule, root: rule.root ?? sourceRoot })), ...routes, ...full.map(rule => ({ ...rule, root: rule.root ?? sourceRoot }))];
|
|
160
|
+
} } : {}),
|
|
137
161
|
ownsReport: name => server.ownsReport(name) || client.ownsReport(name),
|
|
138
162
|
configure(svc, info) {
|
|
139
163
|
const configured = server.configure(svc, info);
|
package/dist/coverage-rust.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type ChangeRule } from "./change-rules.js";
|
|
1
2
|
import type { CoverageAdapter, CoverageSourceOptions } from "./coverage.js";
|
|
2
3
|
export interface RustCoverageOptions extends CoverageSourceOptions {
|
|
4
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
3
5
|
/** Only for idle command containers where no Rust process has run yet. */
|
|
4
6
|
allowEmpty?: boolean;
|
|
5
7
|
/** Compiler workspace directory, when different from the runtime workdir.
|
package/dist/coverage-rust.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RUST_CHANGE_RULES, COMMON_CHANGE_RULES, validateChangeRules } from "./change-rules.js";
|
|
1
2
|
import { promises as fs, readFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { connect } from "node:net";
|
|
@@ -36,6 +37,9 @@ export async function flushCoverageSockets(dir, prefix, signal) {
|
|
|
36
37
|
* The helper exports cumulative per-process LCOV before exit and on request;
|
|
37
38
|
* no compiler tools or binaries need to be copied out of terminated pods. */
|
|
38
39
|
export function rust(options = {}) {
|
|
40
|
+
const rules = options.changeRules === undefined ? RUST_CHANGE_RULES : options.changeRules;
|
|
41
|
+
if (rules !== false)
|
|
42
|
+
validateChangeRules(rules);
|
|
39
43
|
const { sourceRoot, excludeSources, llvmCov, llvmProfdata, ignoreFilenameRegex, buildRoot } = options;
|
|
40
44
|
if (buildRoot !== undefined && (!buildRoot.startsWith("/") || buildRoot === "/" || /[\0\r\n]/.test(buildRoot) || buildRoot.split("/").includes("..")))
|
|
41
45
|
throw new Error("Rust buildRoot must be an absolute compiler workspace directory");
|
|
@@ -45,6 +49,22 @@ export function rust(options = {}) {
|
|
|
45
49
|
}
|
|
46
50
|
return {
|
|
47
51
|
name: "rust", reports: "cumulative",
|
|
52
|
+
changeRules: rules === false ? [] : rules.map(rule => ({ ...rule })),
|
|
53
|
+
...(options.changeRules === undefined ? { async resolveChangeRules(ctx) {
|
|
54
|
+
const root = sourceRoot ?? ctx.sourceRoot ?? ".";
|
|
55
|
+
const prefix = root === "." ? "" : root + "/";
|
|
56
|
+
const files = await ctx.projectFiles();
|
|
57
|
+
const crates = [...new Set(files.filter(file => file.startsWith(prefix) && (file === "Cargo.toml" || file.endsWith("/Cargo.toml"))).map(file => path.posix.dirname(file)))].sort();
|
|
58
|
+
// A workspace can contain many packages; their canonical test directories
|
|
59
|
+
// are relative to each Cargo.toml, not just the adapter's source root.
|
|
60
|
+
// Test fixture packages stay ignored even when they contain binary targets.
|
|
61
|
+
return [
|
|
62
|
+
...RUST_CHANGE_RULES.map(rule => ({ ...rule, root: rule.root ?? root })),
|
|
63
|
+
...crates.map(root => ({ root, pattern: "src/bin/**", action: "full" })),
|
|
64
|
+
...crates.flatMap(root => ["tests/**", "benches/**"].map(pattern => ({ root, pattern, action: "ignore" }))),
|
|
65
|
+
...COMMON_CHANGE_RULES.map(rule => ({ ...rule, root })),
|
|
66
|
+
];
|
|
67
|
+
} } : {}),
|
|
48
68
|
sourceOptions: { ...(sourceRoot !== undefined ? { sourceRoot } : {}), ...(excludeSources ? { excludeSources } : {}) },
|
|
49
69
|
ownsReport: name => /^rust-.*\.lcov$/.test(name),
|
|
50
70
|
configure: service => ({ ...service, env: { ...service.env,
|
package/dist/coverage.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ChangeRule, type ChangePolicy } from "./change-rules.js";
|
|
2
|
+
export * from "./change-rules.js";
|
|
1
3
|
import type { ServiceConfig } from "./index.js";
|
|
2
4
|
import { COVERAGE_CONTAINER_DIR, type CoverageReportsMode } from "./harness/coverage.js";
|
|
3
5
|
export { COVERAGE_CONTAINER_DIR, type CoverageReportsMode };
|
|
@@ -39,6 +41,10 @@ export interface CoverageLoadContext {
|
|
|
39
41
|
* of the manifests that declare them (see {@link CoverageAdapter.inventory}).
|
|
40
42
|
*/
|
|
41
43
|
export interface CoverageInventoryContext {
|
|
44
|
+
/** Effective repository-relative source root for build-specific policy resolution. */
|
|
45
|
+
sourceRoot?: string;
|
|
46
|
+
/** Selection overrides of the owning service, forwarded by container wrappers. */
|
|
47
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
42
48
|
/** The services-map key. */
|
|
43
49
|
service: string;
|
|
44
50
|
/** Run a shell command (`sh -c`) inside the container. Rejects on a
|
|
@@ -47,8 +53,8 @@ export interface CoverageInventoryContext {
|
|
|
47
53
|
stdout: string;
|
|
48
54
|
stderr: string;
|
|
49
55
|
}>;
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
56
|
+
/** Project-relative files; inventory calls omit paths ignored by effective
|
|
57
|
+
* change policy, as well as node_modules, .git and other upload exclusions. */
|
|
52
58
|
projectFiles(): Promise<string[]>;
|
|
53
59
|
/** One project file's text (project-relative path); `null` when absent. */
|
|
54
60
|
readProjectFile(path: string): Promise<string | null>;
|
|
@@ -88,6 +94,12 @@ export type CoverageSourceContext = CoverageInventoryContext;
|
|
|
88
94
|
export interface CoverageAdapter {
|
|
89
95
|
/** Names the adapter in errors (`coverage adapter "node" on service …`). */
|
|
90
96
|
name: string;
|
|
97
|
+
/** Ordered default rules; service rules are explicit overrides. */
|
|
98
|
+
changeRules?: readonly ChangeRule[];
|
|
99
|
+
/** Resolve build-specific rules once evidence is available; replaces static defaults. */
|
|
100
|
+
resolveChangeRules?(ctx: CoverageSourceContext): Promise<readonly ChangeRule[]>;
|
|
101
|
+
/** Wrappers retain separate policy groups for each nested adapter. */
|
|
102
|
+
changeRuleGroups?: readonly (readonly ChangeRule[])[];
|
|
91
103
|
/** Archived on this adapter's reports; other adapters retain their own policy. */
|
|
92
104
|
sourceOptions?: CoverageSourceOptions | ((ctx: CoverageSourceContext) => Promise<CoverageSourceOptions>);
|
|
93
105
|
/** Generated scaffolding excluded in addition to explicit or legacy source policy. */
|
|
@@ -178,6 +190,13 @@ export declare function coverageAdapters(cov: ServiceCoverage | undefined): read
|
|
|
178
190
|
* adapter names (`toJSON`), never as functions.
|
|
179
191
|
*/
|
|
180
192
|
export declare function applyCoverageAdapters<S extends ServiceConfig>(key: string, svc: S): S;
|
|
193
|
+
export declare function resolveAdapterChangeRules(adapter: CoverageAdapter, ctx: CoverageSourceContext): Promise<void>;
|
|
194
|
+
export declare function serviceChangePolicy(svc: Pick<ServiceConfig, "coverage" | "changeRules">, options?: {
|
|
195
|
+
requireResolved?: boolean;
|
|
196
|
+
}): ChangePolicy;
|
|
197
|
+
/** Inventory fingerprints must not turn ignored fixture manifests into changes.
|
|
198
|
+
* Installed runtime dependency inventories remain independent of project files. */
|
|
199
|
+
export declare function inventoryProjectFiles(svc: Pick<ServiceConfig, "coverage" | "changeRules">, files: readonly string[]): string[];
|
|
181
200
|
/** Resolve adapter policies once per capture. Invalid/overlapping ownership is
|
|
182
201
|
* an error, so one adapter cannot silently reinterpret another's reports. */
|
|
183
202
|
export declare function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext, fallback?: CoverageSourceOptions): Promise<{
|
|
@@ -305,6 +324,8 @@ export declare function npmManifestUnit(projectPath: string, text: string): stri
|
|
|
305
324
|
*/
|
|
306
325
|
export declare const NODE_INVENTORY_SCRIPT = "\"use strict\";\nconst fs = require(\"node:fs\"), path = require(\"node:path\");\nconst out = new Set();\nfunction isRealDir(p) { try { return fs.lstatSync(p).isDirectory(); } catch (e) { return false; } }\nfunction readPkg(d) {\n try {\n const j = JSON.parse(fs.readFileSync(path.join(d, \"package.json\"), \"utf8\"));\n if (j && typeof j.name === \"string\" && j.name) out.add(\"npm:\" + j.name + (typeof j.version === \"string\" && j.version ? \"@\" + j.version : \"\"));\n } catch (e) {}\n}\nfunction visit(d, depth) {\n readPkg(d);\n if (depth < 3) { const nested = path.join(d, \"node_modules\"); if (isRealDir(nested)) walkNm(nested, depth + 1); }\n}\nfunction walkNm(nm, depth) {\n let names; try { names = fs.readdirSync(nm); } catch (e) { return; }\n for (const n of names) {\n if (n === \".bin\" || n === \".cache\" || n === \".modules.yaml\") continue;\n if (n === \".pnpm\") {\n let ps; try { ps = fs.readdirSync(path.join(nm, \".pnpm\")); } catch (e) { continue; }\n for (const p of ps) { const inner = path.join(nm, \".pnpm\", p, \"node_modules\"); if (isRealDir(inner)) walkNm(inner, depth + 1); }\n continue;\n }\n if (n.startsWith(\"@\")) {\n let ss; try { ss = fs.readdirSync(path.join(nm, n)); } catch (e) { continue; }\n for (const s of ss) { const d = path.join(nm, n, s); if (isRealDir(d)) visit(d, depth); }\n } else { const d = path.join(nm, n); if (isRealDir(d)) visit(d, depth); }\n }\n}\nlet d = process.cwd();\nfor (;;) {\n const nm = path.join(d, \"node_modules\");\n if (isRealDir(nm)) walkNm(nm, 0);\n const up = path.dirname(d);\n if (up === d) break;\n d = up;\n}\nprocess.stdout.write(Array.from(out).sort().join(\"\\n\") + (out.size ? \"\\n\" : \"\"));\n";
|
|
307
326
|
export interface NodeCoverageOptions extends CoverageSourceOptions {
|
|
327
|
+
/** Replace this adapter's defaults, or false to disable them. */
|
|
328
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
308
329
|
/** Generated runtime entrypoints, relative to the service working directory.
|
|
309
330
|
* Track their exact contents as units rather than inventing repository lines.
|
|
310
331
|
* A content change changes the inventory and invalidates dependent tests. */
|
|
@@ -348,7 +369,10 @@ export declare function collectNodeReports(dir: string): Promise<NodeReportStats
|
|
|
348
369
|
* harvested first, and only this service's totals are taken and cleared. Nothing ran ⇒
|
|
349
370
|
* an lcov with no records, which is a report saying so.
|
|
350
371
|
*/
|
|
351
|
-
export
|
|
372
|
+
export interface BrowserCoverageOptions extends CoverageSourceOptions {
|
|
373
|
+
changeRules?: readonly ChangeRule[] | false;
|
|
374
|
+
}
|
|
375
|
+
export declare function browser(options?: BrowserCoverageOptions): CoverageAdapter;
|
|
352
376
|
/**
|
|
353
377
|
* Run `command` (via `sh -c`) in the container at every capture, so the
|
|
354
378
|
* service writes a fresh report into `/spectest/coverage/`. The escape
|