@specific.dev/spectest 0.120.0 → 0.121.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/components/k3s-nodes.d.ts +5 -0
- package/dist/components/k3s-nodes.js +37 -0
- package/dist/components/k3s.d.ts +7 -41
- package/dist/components/k3s.js +260 -215
- package/dist/coverage-javascript.d.ts +1 -0
- package/dist/coverage-javascript.js +11 -0
- package/dist/coverage-next.js +2 -3
- package/dist/coverage.d.ts +4 -2
- package/dist/coverage.js +21 -5
- package/dist/daemon.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -3
- package/package.json +1 -1
- package/src/components/k3s-concurrency.test.ts +76 -3
- package/src/components/k3s-docker.test.ts +140 -0
- package/src/components/k3s-nodes.test.ts +73 -0
- package/src/components/k3s-nodes.ts +35 -0
- package/src/components/k3s-registry.test.ts +13 -8
- package/src/components/k3s.ts +268 -223
- package/src/coverage-javascript.ts +11 -0
- package/src/coverage-next.test.ts +3 -0
- package/src/coverage-next.ts +2 -3
- package/src/coverage.test.ts +47 -1
- package/src/coverage.ts +22 -6
- package/src/daemon.ts +4 -1
- package/src/index.ts +11 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Bundler-generated source identities shared by browser and Node coverage.
|
|
2
|
+
// Do not match arbitrary webpack:// sources: those include authored modules.
|
|
3
|
+
export const JAVASCRIPT_GENERATED_SOURCES = [
|
|
4
|
+
"webpack/bootstrap",
|
|
5
|
+
"*/webpack/bootstrap",
|
|
6
|
+
"webpack/runtime/*",
|
|
7
|
+
"*/webpack/runtime/*",
|
|
8
|
+
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
9
|
+
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
10
|
+
"webpack://*/external *", // Webpack external-module wrappers.
|
|
11
|
+
] as const;
|
|
@@ -29,6 +29,9 @@ describe("Next adapter", () => {
|
|
|
29
29
|
expect(browserLoaded).toBe(true);
|
|
30
30
|
const policies = await resolveCoverageSourceOptions([adapter],context());
|
|
31
31
|
expect(policies[0]!.options!.sourceRoot).toBe("packages");
|
|
32
|
+
expect(policies[0]!.options!.excludeSources).toContain("*/webpack/runtime/*");
|
|
33
|
+
expect(policies[0]!.options!.excludeSources).toContain("*/.next-internal/server/app/*/actions.js (server actions loader)");
|
|
34
|
+
expect(policies[0]!.options!.excludeSources!.some(p => p.includes("temporal"))).toBe(false);
|
|
32
35
|
const capture = attachCoverageSourceOptions({service:"web",reports:[
|
|
33
36
|
{name:"node-1.json",content:'{"result":[]}'}, {name:"browser.lcov",content:"TN:\n"},
|
|
34
37
|
{name:"other.lcov",content:"TN:\n"},
|
package/src/coverage-next.ts
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 { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
4
5
|
import { node, browser, type CoverageAdapter, type CoverageSourceContext, type CoverageSourceOptions } from "./coverage.js";
|
|
5
6
|
|
|
6
7
|
export interface NextCoverageOptions extends CoverageSourceOptions {
|
|
@@ -65,9 +66,6 @@ export interface NextBuildMetadata {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const GENERATED_SOURCES = [
|
|
68
|
-
"webpack://*/", // Loader-only modules with an empty source identity.
|
|
69
|
-
"webpack://*/?*", // Loader-only modules without a resource path.
|
|
70
|
-
"webpack://*/external *", // Webpack external-module wrappers.
|
|
71
69
|
"*/data:* <text/javascript>", // Turbopack virtual modules, including action proxies.
|
|
72
70
|
"*/.next-internal/server/app/*/actions.js (server actions loader)",
|
|
73
71
|
"*/app/*--route-entry.js",
|
|
@@ -96,6 +94,7 @@ export function next(options: NextCoverageOptions = {}): CoverageAdapter {
|
|
|
96
94
|
return value;
|
|
97
95
|
};
|
|
98
96
|
return {
|
|
97
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
99
98
|
name: "next", reports: "delta", explains: server.explains,
|
|
100
99
|
ownsReport: name => server.ownsReport!(name) || client.ownsReport!(name),
|
|
101
100
|
configure(svc, info) {
|
package/src/coverage.test.ts
CHANGED
|
@@ -22,9 +22,12 @@ import {
|
|
|
22
22
|
isNodeDumpName,
|
|
23
23
|
node,
|
|
24
24
|
resetNodeCoverageMemory,
|
|
25
|
+
resolveCoverageSourceOptions,
|
|
26
|
+
attachCoverageSourceOptions,
|
|
25
27
|
validateCoverage,
|
|
26
28
|
type CoverageCaptureContext,
|
|
27
29
|
} from "./coverage.js";
|
|
30
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
28
31
|
import type { ServiceConfig } from "./index.js";
|
|
29
32
|
import { foldBrowserCoverage } from "./browser-coverage.js";
|
|
30
33
|
|
|
@@ -109,7 +112,7 @@ describe("applyCoverageAdapters", () => {
|
|
|
109
112
|
expect(typeof list[0]?.capture).toBe("function");
|
|
110
113
|
expect(typeof list[1]?.load).toBe("function");
|
|
111
114
|
expect(JSON.parse(JSON.stringify(out)).coverage).toEqual({
|
|
112
|
-
adapters: [{ adapter: "node" }, { adapter: "browser" }],
|
|
115
|
+
adapters: [{ adapter: "node", defaultExcludeSources: [...JAVASCRIPT_GENERATED_SOURCES] }, { adapter: "browser", defaultExcludeSources: [...JAVASCRIPT_GENERATED_SOURCES] }],
|
|
113
116
|
explains: [...NODE_EXPLAINS],
|
|
114
117
|
});
|
|
115
118
|
});
|
|
@@ -614,3 +617,46 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
|
614
617
|
expect((await fs.readdir(dir)).filter((n) => n.endsWith(".json"))).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
615
618
|
});
|
|
616
619
|
});
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
describe("JavaScript generated-source defaults", () => {
|
|
623
|
+
const ctx = {
|
|
624
|
+
service: "app", signal: new AbortController().signal,
|
|
625
|
+
exec: async () => ({stdout: "", stderr: ""}),
|
|
626
|
+
projectFiles: async () => [], readProjectFile: async () => null,
|
|
627
|
+
};
|
|
628
|
+
test("defaults are archived only on owned reports and preserve legacy policy", async () => {
|
|
629
|
+
const legacy = {sourceRoot: "packages", excludeSources: ["virtual:///generated/*"]};
|
|
630
|
+
const policies = await resolveCoverageSourceOptions([node(), browser()], ctx, legacy);
|
|
631
|
+
const captured = attachCoverageSourceOptions({reports: [
|
|
632
|
+
{name: "node-1.json"}, {name: "browser.lcov"}, {name: "other.lcov"},
|
|
633
|
+
]}, policies);
|
|
634
|
+
for (const report of captured.reports.slice(0, 2) as any[]) {
|
|
635
|
+
expect(report.sourceOptions.sourceRoot).toBe("packages");
|
|
636
|
+
expect(report.sourceOptions.excludeSources).toContain("*/webpack/bootstrap");
|
|
637
|
+
expect(report.sourceOptions.excludeSources).toContain("*/webpack/runtime/*");
|
|
638
|
+
expect(report.sourceOptions.excludeSources).toContain("virtual:///generated/*");
|
|
639
|
+
expect(report.sourceOptions.excludeSources.some((p: string) => p.includes("temporal"))).toBe(false);
|
|
640
|
+
expect(report.sourceOptions.excludeSources.some((p: string) => p.includes("server actions loader"))).toBe(false);
|
|
641
|
+
}
|
|
642
|
+
expect((captured.reports[2] as any).sourceOptions).toBeUndefined();
|
|
643
|
+
});
|
|
644
|
+
test("explicit user policy replaces legacy policy and extends defaults without duplicates", async () => {
|
|
645
|
+
const user = {sourceRoot: "workflows", excludeSources: [
|
|
646
|
+
"*/webpack/runtime/*", "*/temporal/workflows/index-autogenerated-entrypoint.cjs",
|
|
647
|
+
]};
|
|
648
|
+
for (const adapter of [node(user), browser(user)]) {
|
|
649
|
+
const [policy] = await resolveCoverageSourceOptions([adapter], ctx, {
|
|
650
|
+
sourceRoot: "old", excludeSources: ["old/*"],
|
|
651
|
+
});
|
|
652
|
+
expect(policy!.options!.sourceRoot).toBe("workflows");
|
|
653
|
+
expect(policy!.options!.excludeSources).toContain(user.excludeSources[1]!);
|
|
654
|
+
expect(policy!.options!.excludeSources).not.toContain("old/*");
|
|
655
|
+
expect(policy!.options!.excludeSources!.filter(p => p === "*/webpack/runtime/*")).toHaveLength(1);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
test("default policy requires report ownership and valid source patterns", () => {
|
|
659
|
+
expect(() => validateCoverage("app", {adapters: [{name: "bad", defaultExcludeSources: ["generated/*"]}]})).toThrow();
|
|
660
|
+
expect(() => validateCoverage("app", {adapters: [{name: "bad", ownsReport: () => true, defaultExcludeSources: [""]}]})).toThrow();
|
|
661
|
+
});
|
|
662
|
+
});
|
package/src/coverage.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// coverage: { adapters: [coverage.node(), coverage.browser()] }
|
|
29
29
|
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
|
+
import { JAVASCRIPT_GENERATED_SOURCES } from "./coverage-javascript.js";
|
|
31
32
|
|
|
32
33
|
import type { ServiceConfig } from "./index.js";
|
|
33
34
|
import { harvestAllBrowserCoverage, takeBrowserCoverageReport } from "./browser-coverage.js";
|
|
@@ -126,7 +127,9 @@ export interface CoverageAdapter {
|
|
|
126
127
|
name: string;
|
|
127
128
|
/** Archived on this adapter's reports; other adapters retain their own policy. */
|
|
128
129
|
sourceOptions?: CoverageSourceOptions | ((ctx: CoverageSourceContext) => Promise<CoverageSourceOptions>);
|
|
129
|
-
/**
|
|
130
|
+
/** Generated scaffolding excluded in addition to explicit or legacy source policy. */
|
|
131
|
+
defaultExcludeSources?: readonly string[];
|
|
132
|
+
/** Required with sourceOptions/defaultExcludeSources, including runtime reports. */
|
|
130
133
|
ownsReport?(name: string): boolean;
|
|
131
134
|
/**
|
|
132
135
|
* How the reports this adapter writes (through `ctx.writeReport`)
|
|
@@ -248,14 +251,17 @@ export function validateCoverage(service: string, cov: unknown): void {
|
|
|
248
251
|
typeof (a as Record<string, unknown>)[k] === "function",
|
|
249
252
|
);
|
|
250
253
|
const sourceOptions = (a as CoverageAdapter | null)?.sourceOptions;
|
|
254
|
+
const defaults = (a as CoverageAdapter | null)?.defaultExcludeSources;
|
|
255
|
+
const defaultShape = defaults === undefined || typeof (a as CoverageAdapter).ownsReport === "function";
|
|
251
256
|
const sourceShape = sourceOptions === undefined || (typeof (a as CoverageAdapter).ownsReport === "function" &&
|
|
252
257
|
(typeof sourceOptions === "function" || (typeof sourceOptions === "object" && sourceOptions !== null && !Array.isArray(sourceOptions))));
|
|
253
|
-
if (!ok || !sourceShape) {
|
|
258
|
+
if (!ok || !sourceShape || !defaultShape) {
|
|
254
259
|
throw new Error(
|
|
255
260
|
`service "${service}" has an invalid coverage adapter — an adapter is \`{ name, reports?, explains?, configure?, load?, capture?, inventory? }\` (see \`defineAdapter\` in @specific.dev/spectest/coverage)`,
|
|
256
261
|
);
|
|
257
262
|
}
|
|
258
263
|
if (sourceOptions !== undefined && typeof sourceOptions !== "function") validateCoverageSourceOptions(service, sourceOptions);
|
|
264
|
+
if (defaults !== undefined) validateCoverageSourceOptions(service, { excludeSources: defaults });
|
|
259
265
|
}
|
|
260
266
|
return;
|
|
261
267
|
}
|
|
@@ -310,11 +316,19 @@ function validateCoverageSourceOptions(service: string, options: unknown): asser
|
|
|
310
316
|
|
|
311
317
|
/** Resolve adapter policies once per capture. Invalid/overlapping ownership is
|
|
312
318
|
* an error, so one adapter cannot silently reinterpret another's reports. */
|
|
313
|
-
export async function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext) {
|
|
314
|
-
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.ownsReport).map(async a => {
|
|
319
|
+
export async function resolveCoverageSourceOptions(adapters: readonly CoverageAdapter[], ctx: CoverageSourceContext, fallback?: CoverageSourceOptions) {
|
|
320
|
+
return Promise.all(adapters.filter(a => a.sourceOptions !== undefined || a.defaultExcludeSources !== undefined || a.ownsReport).map(async a => {
|
|
315
321
|
if (!a.ownsReport) throw new Error(`coverage adapter "${a.name}" needs ownsReport with sourceOptions`);
|
|
316
|
-
|
|
322
|
+
let options = typeof a.sourceOptions === "function" ? await a.sourceOptions(ctx) : a.sourceOptions;
|
|
317
323
|
if (a.sourceOptions !== undefined) validateCoverageSourceOptions(ctx.service, options);
|
|
324
|
+
if (a.defaultExcludeSources !== undefined) {
|
|
325
|
+
validateCoverageSourceOptions(ctx.service, { excludeSources: a.defaultExcludeSources });
|
|
326
|
+
// Defaults must not discard the source root/exclusions of older configs.
|
|
327
|
+
// Explicit adapter options still replace that service-level fallback.
|
|
328
|
+
options ??= fallback;
|
|
329
|
+
if (options !== undefined) validateCoverageSourceOptions(ctx.service, options);
|
|
330
|
+
options = { ...options, excludeSources: [...new Set([...a.defaultExcludeSources, ...(options?.excludeSources ?? [])])] };
|
|
331
|
+
}
|
|
318
332
|
return { adapter: a.name, ownsReport: a.ownsReport, options };
|
|
319
333
|
}));
|
|
320
334
|
}
|
|
@@ -333,7 +347,7 @@ export function attachCoverageSourceOptions<T extends { reports: { name: string;
|
|
|
333
347
|
function withWireName(a: CoverageAdapter): CoverageAdapter {
|
|
334
348
|
if (Object.prototype.hasOwnProperty.call(a, "toJSON")) return a;
|
|
335
349
|
return Object.assign(Object.create(Object.getPrototypeOf(a) as object | null), a, {
|
|
336
|
-
toJSON: () => ({ adapter: a.name, ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
350
|
+
toJSON: () => ({ adapter: a.name, ...(a.defaultExcludeSources ? { defaultExcludeSources: a.defaultExcludeSources } : {}), ...(a.sourceOptions && typeof a.sourceOptions !== "function" ? { sourceOptions: a.sourceOptions } : {}) }),
|
|
337
351
|
}) as CoverageAdapter;
|
|
338
352
|
}
|
|
339
353
|
|
|
@@ -909,6 +923,7 @@ export function node(options: NodeCoverageOptions = {}): CoverageAdapter {
|
|
|
909
923
|
}
|
|
910
924
|
return {
|
|
911
925
|
name: "node",
|
|
926
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
912
927
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: { sourceRoot: options.sourceRoot, excludeSources: options.excludeSources } } : {}),
|
|
913
928
|
ownsReport: name => isNodeDumpName(name) || name === NODE_COVERAGE_EMPTY_REPORT,
|
|
914
929
|
reports: "delta",
|
|
@@ -1121,6 +1136,7 @@ export async function collectNodeReports(dir: string): Promise<NodeReportStats>
|
|
|
1121
1136
|
export function browser(options: CoverageSourceOptions = {}): CoverageAdapter {
|
|
1122
1137
|
return {
|
|
1123
1138
|
name: "browser",
|
|
1139
|
+
defaultExcludeSources: JAVASCRIPT_GENERATED_SOURCES,
|
|
1124
1140
|
...(options.sourceRoot !== undefined || options.excludeSources !== undefined ? { sourceOptions: options } : {}),
|
|
1125
1141
|
ownsReport: name => name === "browser.lcov",
|
|
1126
1142
|
reports: "delta",
|
package/src/daemon.ts
CHANGED
|
@@ -3894,7 +3894,10 @@ async function captureServiceCoverage(baseline = false): Promise<ServiceCoverage
|
|
|
3894
3894
|
try { return await fs.readFile(resolveProjectPath(p, { record: false }), "utf8"); }
|
|
3895
3895
|
catch { return null; }
|
|
3896
3896
|
},
|
|
3897
|
-
}
|
|
3897
|
+
}, typeof svc.coverage === "object" ? {
|
|
3898
|
+
sourceRoot: svc.coverage.sourceRoot,
|
|
3899
|
+
excludeSources: svc.coverage.excludeSources,
|
|
3900
|
+
} : undefined);
|
|
3898
3901
|
} catch (err) {
|
|
3899
3902
|
return { service: svc.name, reports: [], error: `coverage source metadata: ${err instanceof Error ? err.message : String(err)}` };
|
|
3900
3903
|
}
|
package/src/index.ts
CHANGED
|
@@ -895,7 +895,11 @@ export interface ServiceGroupInput<
|
|
|
895
895
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
896
896
|
H extends Record<string, any> = Record<string, never>,
|
|
897
897
|
Primary extends keyof P & string = keyof P & string,
|
|
898
|
-
> {
|
|
898
|
+
> extends Partial<ServiceConfig> {
|
|
899
|
+
// ServiceConfig fields on the group override its primary container. This
|
|
900
|
+
// preserves the usual `{ ...component(), hostnames: [...] }` customization
|
|
901
|
+
// when a component expands to multiple services. Helpers/setup below are
|
|
902
|
+
// group hooks, not container overrides.
|
|
899
903
|
/**
|
|
900
904
|
* Build the group's member services, keyed by RELATIVE part name
|
|
901
905
|
* (`db`, `auth`, …). Called at expansion time with the final naming
|
|
@@ -1127,14 +1131,17 @@ function expandServiceGroups(input: InputServicesMap): ServicesMap {
|
|
|
1127
1131
|
);
|
|
1128
1132
|
}
|
|
1129
1133
|
}
|
|
1134
|
+
const { primary: _primary, services: _services, helpers: _helpers,
|
|
1135
|
+
setup: _setup, expectKey: _expectKey, [SERVICE_GROUP]: _marker,
|
|
1136
|
+
...primaryOverrides } = entry;
|
|
1130
1137
|
for (const [part, svc] of Object.entries(parts)) {
|
|
1131
1138
|
const finalKey = naming.key(part);
|
|
1132
1139
|
claim(finalKey, owner);
|
|
1133
1140
|
// Rewrite relative dependsOn to final keys; a spread keeps function
|
|
1134
1141
|
// fields (helpers/setup) and the enumerable-symbol ingress decls.
|
|
1135
|
-
const final: ServiceConfig = { ...svc };
|
|
1136
|
-
if (
|
|
1137
|
-
final.dependsOn =
|
|
1142
|
+
const final: ServiceConfig = { ...svc, ...(part === entry.primary ? primaryOverrides : {}) };
|
|
1143
|
+
if (final.dependsOn?.length) {
|
|
1144
|
+
final.dependsOn = final.dependsOn.map((d) =>
|
|
1138
1145
|
partKeys.has(d) ? naming.key(d) : d,
|
|
1139
1146
|
);
|
|
1140
1147
|
}
|