@specific.dev/spectest 0.79.2 → 0.81.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/coverage.d.ts +111 -69
- package/dist/coverage.js +280 -171
- package/dist/daemon.js +24 -14
- package/package.json +1 -1
- package/src/coverage.test.ts +198 -131
- package/src/coverage.ts +320 -177
- package/src/daemon.ts +20 -16
package/src/coverage.test.ts
CHANGED
|
@@ -4,27 +4,27 @@ import { promises as fs } from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import {
|
|
7
|
-
NODE_COVERAGE_C8_BIN,
|
|
8
|
-
NODE_COVERAGE_DUMPS_SUBDIR,
|
|
9
7
|
NODE_COVERAGE_HOOK,
|
|
10
8
|
NODE_COVERAGE_EMPTY_REPORT,
|
|
11
|
-
NODE_COVERAGE_LCOV,
|
|
12
|
-
NODE_COVERAGE_TOOLS_CONTAINER_DIR,
|
|
13
9
|
NODE_COVERAGE_HOOK_PATH,
|
|
14
10
|
NODE_COVERAGE_SOCKET_PREFIX,
|
|
15
|
-
|
|
11
|
+
SOURCE_MAP_HASH_KEY,
|
|
12
|
+
SOURCE_MAP_REF_KEY,
|
|
16
13
|
applyCoverageAdapters,
|
|
17
14
|
appendEnvFlag,
|
|
15
|
+
attachSourceMaps,
|
|
16
|
+
lineLengthsOf,
|
|
17
|
+
resolveSourceMapFromContainer,
|
|
18
|
+
sourceMappingUrlOf,
|
|
18
19
|
browser,
|
|
19
20
|
command,
|
|
20
21
|
coverageReportsMode,
|
|
21
22
|
compactV8Document,
|
|
22
|
-
compactV8Reports,
|
|
23
|
-
COVERAGE_CONTAINER_DIR,
|
|
24
23
|
node,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
prepareV8Reports,
|
|
25
|
+
removeShippedV8Reports,
|
|
26
|
+
resetNodeCoverageMemory,
|
|
27
|
+
scriptExecuted,
|
|
28
28
|
validateCoverage,
|
|
29
29
|
type CoverageCaptureContext,
|
|
30
30
|
} from "./coverage.js";
|
|
@@ -99,58 +99,89 @@ describe("applyCoverageAdapters", () => {
|
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
const ran = { functions: [{ functionName: "", ranges: [{ startOffset: 0, endOffset: 10, count: 1 }] }] };
|
|
103
|
+
const idle = { functions: [{ functionName: "", ranges: [{ startOffset: 0, endOffset: 10, count: 0 }] }] };
|
|
104
|
+
|
|
102
105
|
describe("compactV8Document", () => {
|
|
103
106
|
const doc = {
|
|
104
107
|
result: [
|
|
105
|
-
{ scriptId: "1", url: "file:///app/src/a.ts",
|
|
106
|
-
{ scriptId: "2", url: "file:///app/node_modules/x/index.js",
|
|
107
|
-
{ scriptId: "3", url: "node:fs",
|
|
108
|
-
{ scriptId: "5", url: "file:///root/.cache/node/corepack/v1/pnpm/10.0.0/dist/pnpm.cjs",
|
|
109
|
-
{ scriptId: "
|
|
108
|
+
{ scriptId: "1", url: "file:///app/src/a.ts", ...ran },
|
|
109
|
+
{ scriptId: "2", url: "file:///app/node_modules/x/index.js", ...ran },
|
|
110
|
+
{ scriptId: "3", url: "node:fs", ...ran },
|
|
111
|
+
{ scriptId: "5", url: "file:///root/.cache/node/corepack/v1/pnpm/10.0.0/dist/pnpm.cjs", ...ran },
|
|
112
|
+
{ scriptId: "6", url: "file:///app/src/idle.ts", ...idle },
|
|
113
|
+
{ scriptId: "4", url: "", ...ran },
|
|
110
114
|
],
|
|
111
115
|
"source-map-cache": {
|
|
112
116
|
"file:///app/src/a.ts": { lineLengths: [1], data: { mappings: "AAAA", sources: ["a.ts"], sourcesContent: ["x"] }, url: null },
|
|
117
|
+
"file:///app/src/idle.ts": { lineLengths: [1], data: { mappings: "AAAA", sources: ["idle.ts"] }, url: null },
|
|
113
118
|
"file:///app/node_modules/x/index.js": { lineLengths: [1], data: { mappings: "AAAA" }, url: null },
|
|
114
119
|
},
|
|
115
120
|
};
|
|
116
|
-
|
|
121
|
+
type Cache = Record<string, Record<string, unknown>>;
|
|
122
|
+
|
|
123
|
+
test("scriptExecuted: any range count above zero", () => {
|
|
124
|
+
expect(scriptExecuted(ran)).toBe(true);
|
|
125
|
+
expect(scriptExecuted(idle)).toBe(false);
|
|
126
|
+
expect(scriptExecuted({})).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
test("keeps app scripts that ran and their maps, drops the rest and sourcesContent", () => {
|
|
117
129
|
const out = compactV8Document(doc) as typeof doc;
|
|
118
130
|
expect(out.result.map((s) => s.url)).toEqual(["file:///app/src/a.ts"]);
|
|
119
|
-
|
|
120
|
-
expect(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
expect(out["source-map-cache"]["file:///app/src/a.ts"].data).toEqual({
|
|
125
|
-
mappings: "AAAA",
|
|
126
|
-
sources: ["a.ts"],
|
|
127
|
-
sourcesContent: ["x"],
|
|
128
|
-
});
|
|
131
|
+
const cache = out["source-map-cache"] as unknown as Cache;
|
|
132
|
+
expect(Object.keys(cache)).toEqual(["file:///app/src/a.ts"]);
|
|
133
|
+
expect(cache["file:///app/src/a.ts"].data).toEqual({ mappings: "AAAA", sources: ["a.ts"] });
|
|
134
|
+
expect(cache["file:///app/src/a.ts"][SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
135
|
+
expect(cache["file:///app/src/a.ts"][SOURCE_MAP_REF_KEY]).toBeUndefined();
|
|
129
136
|
});
|
|
130
|
-
test("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
expect(
|
|
135
|
-
|
|
137
|
+
test("a map shipped earlier on the branch becomes a stub that refers to its hash", () => {
|
|
138
|
+
const shipped = new Map<string, string>();
|
|
139
|
+
const first = compactV8Document(doc, shipped) as typeof doc;
|
|
140
|
+
const hash = (first["source-map-cache"] as unknown as Cache)["file:///app/src/a.ts"][SOURCE_MAP_HASH_KEY];
|
|
141
|
+
expect(shipped.get("file:///app/src/a.ts")).toBe(hash as string);
|
|
142
|
+
const second = compactV8Document(doc, shipped) as typeof doc;
|
|
143
|
+
expect((second["source-map-cache"] as unknown as Cache)["file:///app/src/a.ts"]).toEqual({ [SOURCE_MAP_REF_KEY]: hash });
|
|
144
|
+
// The hash is over the compacted entry, so the same map with its
|
|
145
|
+
// sources dropped is the same map; a changed map ships again.
|
|
146
|
+
const changed = {
|
|
147
|
+
...doc,
|
|
148
|
+
"source-map-cache": { "file:///app/src/a.ts": { lineLengths: [2], data: { mappings: "AAAC", sources: ["a.ts"] }, url: null } },
|
|
149
|
+
};
|
|
150
|
+
const third = compactV8Document(changed, shipped) as typeof doc;
|
|
151
|
+
const entry = (third["source-map-cache"] as unknown as Cache)["file:///app/src/a.ts"];
|
|
152
|
+
expect(entry[SOURCE_MAP_REF_KEY]).toBeUndefined();
|
|
153
|
+
expect(entry[SOURCE_MAP_HASH_KEY]).not.toBe(hash);
|
|
136
154
|
});
|
|
137
155
|
test("a dump with no app script keeps an empty result and no cache", () => {
|
|
138
156
|
const out = compactV8Document({ result: [doc.result[2]], "source-map-cache": doc["source-map-cache"] });
|
|
139
157
|
expect(out.result).toEqual([]);
|
|
140
158
|
expect("source-map-cache" in out).toBe(false);
|
|
141
159
|
});
|
|
142
|
-
test("
|
|
160
|
+
test("prepareV8Reports compacts coverage-*.json once, ships maps once, and the next capture removes them", async () => {
|
|
161
|
+
resetNodeCoverageMemory();
|
|
143
162
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-cov-"));
|
|
144
163
|
await fs.writeFile(path.join(dir, "coverage-1-2-0.json"), JSON.stringify(doc));
|
|
145
164
|
await fs.writeFile(path.join(dir, "browser.lcov"), "TN:\n");
|
|
146
|
-
await
|
|
147
|
-
|
|
165
|
+
const stats = await prepareV8Reports(dir);
|
|
166
|
+
expect(stats.dumps).toBe(1);
|
|
167
|
+
expect(stats.bytesOut).toBeLessThan(stats.bytesIn);
|
|
168
|
+
const back = JSON.parse(await fs.readFile(path.join(dir, "coverage-1-2-0.json"), "utf8")) as typeof doc;
|
|
148
169
|
expect(back.result.length).toBe(1);
|
|
170
|
+
expect((back["source-map-cache"] as unknown as Cache)["file:///app/src/a.ts"][SOURCE_MAP_HASH_KEY]).toBeDefined();
|
|
149
171
|
expect(await fs.readFile(path.join(dir, "browser.lcov"), "utf8")).toBe("TN:\n");
|
|
150
|
-
//
|
|
172
|
+
// Shipped: a second pass leaves it alone, even when it is not JSON any more.
|
|
151
173
|
await fs.writeFile(path.join(dir, "coverage-1-2-0.json"), "not json");
|
|
152
|
-
await
|
|
174
|
+
expect((await prepareV8Reports(dir)).dumps).toBe(0);
|
|
153
175
|
expect(await fs.readFile(path.join(dir, "coverage-1-2-0.json"), "utf8")).toBe("not json");
|
|
176
|
+
// A later dump of the same script carries a stub for the map.
|
|
177
|
+
await fs.writeFile(path.join(dir, "coverage-1-3-0.json"), JSON.stringify(doc));
|
|
178
|
+
expect((await prepareV8Reports(dir)).dumps).toBe(1);
|
|
179
|
+
const later = JSON.parse(await fs.readFile(path.join(dir, "coverage-1-3-0.json"), "utf8")) as typeof doc;
|
|
180
|
+
expect(Object.keys((later["source-map-cache"] as unknown as Cache)["file:///app/src/a.ts"])).toEqual([SOURCE_MAP_REF_KEY]);
|
|
181
|
+
// The next capture starts by removing what shipped; other files stay.
|
|
182
|
+
await removeShippedV8Reports(dir);
|
|
183
|
+
expect((await fs.readdir(dir)).sort()).toEqual(["browser.lcov"]);
|
|
184
|
+
resetNodeCoverageMemory();
|
|
154
185
|
});
|
|
155
186
|
});
|
|
156
187
|
|
|
@@ -166,9 +197,99 @@ function fakeCtx(dir: string, signal = new AbortController().signal): CoverageCa
|
|
|
166
197
|
async writeReport(name, content) {
|
|
167
198
|
await fs.writeFile(path.join(dir, name), content);
|
|
168
199
|
},
|
|
200
|
+
async readContainerFile(p) {
|
|
201
|
+
return fs.readFile(p).catch(() => null);
|
|
202
|
+
},
|
|
169
203
|
};
|
|
170
204
|
}
|
|
171
205
|
|
|
206
|
+
describe("source maps by reference", () => {
|
|
207
|
+
test("lineLengthsOf matches Node's shape: \\n splits, \\r stays, last line counted", () => {
|
|
208
|
+
expect(lineLengthsOf("ab\ncde\r\n\nf")).toEqual([2, 4, 0, 1]);
|
|
209
|
+
expect(lineLengthsOf("")).toEqual([0]);
|
|
210
|
+
expect(lineLengthsOf("x\n")).toEqual([1, 0]);
|
|
211
|
+
});
|
|
212
|
+
test("sourceMappingUrlOf takes the last comment", () => {
|
|
213
|
+
expect(sourceMappingUrlOf("a\n//# sourceMappingURL=a.js.map\nb\n//# sourceMappingURL=b.js.map\n")).toBe("b.js.map");
|
|
214
|
+
expect(sourceMappingUrlOf("//@ sourceMappingURL=old.map")).toBe("old.map");
|
|
215
|
+
expect(sourceMappingUrlOf("no map here")).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
test("resolves the map a comment names, drops sourcesContent, computes line lengths", async () => {
|
|
218
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
219
|
+
await fs.mkdir(path.join(dir, "maps"));
|
|
220
|
+
await fs.writeFile(path.join(dir, "cli.js"), "const a = 1;\nconsole.log(a);\n//# sourceMappingURL=maps/cli.map\n");
|
|
221
|
+
await fs.writeFile(
|
|
222
|
+
path.join(dir, "maps", "cli.map"),
|
|
223
|
+
JSON.stringify({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [], sourcesContent: ["x"] }),
|
|
224
|
+
);
|
|
225
|
+
const entry = await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "cli.js")}`);
|
|
226
|
+
expect(entry).not.toBeNull();
|
|
227
|
+
expect(entry!.url).toBe(`file://${path.join(dir, "maps", "cli.map")}`);
|
|
228
|
+
expect(entry!.data).toEqual({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [] });
|
|
229
|
+
expect(entry!.lineLengths).toEqual([12, 15, 33, 0]);
|
|
230
|
+
});
|
|
231
|
+
test("with no comment, <script>.map next to the script is the map", async () => {
|
|
232
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
233
|
+
await fs.writeFile(path.join(dir, "cli.js"), "x();\n");
|
|
234
|
+
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA" }));
|
|
235
|
+
const entry = await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "cli.js")}`);
|
|
236
|
+
expect(entry!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
|
|
237
|
+
expect(entry!.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA" });
|
|
238
|
+
});
|
|
239
|
+
test("resolves an inline data: URL map; no map, no file, or a foreign URL is null", async () => {
|
|
240
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
241
|
+
const map = JSON.stringify({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
242
|
+
await fs.writeFile(
|
|
243
|
+
path.join(dir, "inline.js"),
|
|
244
|
+
`x();\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(map).toString("base64")}\n`,
|
|
245
|
+
);
|
|
246
|
+
const inline = await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "inline.js")}`);
|
|
247
|
+
expect(inline!.url).toBeNull();
|
|
248
|
+
expect(inline!.data).toEqual({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
249
|
+
await fs.writeFile(path.join(dir, "bare.js"), "x();\n");
|
|
250
|
+
expect(await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "bare.js")}`)).toBeNull();
|
|
251
|
+
await fs.writeFile(path.join(dir, "gone.js"), "x();\n//# sourceMappingURL=gone.js.map\n");
|
|
252
|
+
expect(await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "gone.js")}`)).toBeNull();
|
|
253
|
+
await fs.writeFile(path.join(dir, "http.js"), "x();\n//# sourceMappingURL=https://cdn.test/x.map\n");
|
|
254
|
+
expect(await resolveSourceMapFromContainer(fakeCtx(dir), `file://${path.join(dir, "http.js")}`)).toBeNull();
|
|
255
|
+
expect(await resolveSourceMapFromContainer(fakeCtx(dir), "node:fs")).toBeNull();
|
|
256
|
+
const noRead = { ...fakeCtx(dir), readContainerFile: undefined };
|
|
257
|
+
expect(await resolveSourceMapFromContainer(noRead, `file://${path.join(dir, "inline.js")}`)).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
test("attachSourceMaps: a map once, a stub after, a missing map looked up once", async () => {
|
|
260
|
+
const calls: string[] = [];
|
|
261
|
+
const entry = { url: null, data: { version: 3, sources: ["a.ts"], mappings: "AAAA" }, lineLengths: [4] };
|
|
262
|
+
const resolve = async (url: string) => {
|
|
263
|
+
calls.push(url);
|
|
264
|
+
return url.endsWith("/a.js") ? entry : null;
|
|
265
|
+
};
|
|
266
|
+
const shipped = new Map<string, string>();
|
|
267
|
+
const missing = new Set<string>();
|
|
268
|
+
const dump = () => ({
|
|
269
|
+
result: [
|
|
270
|
+
{ url: "file:///app/a.js", ...ran },
|
|
271
|
+
{ url: "file:///app/b.js", ...ran },
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
const first = dump() as Record<string, unknown>;
|
|
275
|
+
expect(await attachSourceMaps(first, resolve, shipped, missing)).toBe(1);
|
|
276
|
+
const c1 = first["source-map-cache"] as Record<string, Record<string, unknown>>;
|
|
277
|
+
expect(c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:/);
|
|
278
|
+
expect(c1["file:///app/a.js"].lineLengths).toEqual([4]);
|
|
279
|
+
expect("file:///app/b.js" in c1).toBe(false);
|
|
280
|
+
expect(calls).toEqual(["file:///app/a.js", "file:///app/b.js"]);
|
|
281
|
+
const second = dump() as Record<string, unknown>;
|
|
282
|
+
expect(await attachSourceMaps(second, resolve, shipped, missing)).toBe(0);
|
|
283
|
+
const c2 = second["source-map-cache"] as Record<string, Record<string, unknown>>;
|
|
284
|
+
expect(c2["file:///app/a.js"]).toEqual({ [SOURCE_MAP_REF_KEY]: c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY] });
|
|
285
|
+
expect(calls.length).toBe(2); // b.js is not asked again
|
|
286
|
+
// A dump that carries its own entry is left alone.
|
|
287
|
+
const own = { ...dump(), "source-map-cache": { "file:///app/b.js": { data: { mappings: "" }, lineLengths: [1] } } } as Record<string, unknown>;
|
|
288
|
+
expect(await attachSourceMaps(own, resolve, shipped, missing)).toBe(0);
|
|
289
|
+
expect(calls.length).toBe(2);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
172
293
|
describe("browserCoverage", () => {
|
|
173
294
|
test("writes an empty lcov when nothing ran for the service", async () => {
|
|
174
295
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-cov-"));
|
|
@@ -257,8 +378,12 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
|
257
378
|
expect((await socketsIn(dir)).length).toBe(1); // its socket is gone with it
|
|
258
379
|
expect((await reportsIn(dir)).length).toBe(2); // its exit-time report is there
|
|
259
380
|
|
|
381
|
+
// The next capture ships only what is new: the first dump goes,
|
|
382
|
+
// the short process's exit dump and the server's fresh dump stay.
|
|
260
383
|
await node().capture!(fakeCtx(dir));
|
|
261
|
-
|
|
384
|
+
const next = await reportsIn(dir);
|
|
385
|
+
expect(next.length).toBe(2);
|
|
386
|
+
expect(next).not.toContain(reports[0]!);
|
|
262
387
|
} finally {
|
|
263
388
|
server.kill("SIGKILL");
|
|
264
389
|
}
|
|
@@ -360,106 +485,48 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
|
360
485
|
}
|
|
361
486
|
});
|
|
362
487
|
|
|
363
|
-
test("
|
|
488
|
+
test("a CLI built with an external map and no comment gets its map from the file next to the script, once per branch", async () => {
|
|
489
|
+
// Node caches a map it finds through a sourceMappingURL comment into
|
|
490
|
+
// every dump under NODE_V8_COVERAGE, flag or no flag (checked: the
|
|
491
|
+
// entry then carries Node's resolved `sources` and `sourceRoot`).
|
|
492
|
+
// Without the comment the dump has no map and the adapter reads
|
|
493
|
+
// `cli.js.map` from the container.
|
|
494
|
+
resetNodeCoverageMemory();
|
|
364
495
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covhook-"));
|
|
365
|
-
await node().capture!(fakeCtx(dir));
|
|
366
|
-
await node().capture!(fakeCtx(dir));
|
|
367
|
-
expect(await reportsIn(dir)).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// The conversion to lcov against a real `node` and a real `c8` tree.
|
|
372
|
-
// Golden ships the tree at /opt/spectest/coverage-tools; on a dev box
|
|
373
|
-
// point SPECTEST_C8_TEST_DIR at one (`bun install --linker=hoisted` of
|
|
374
|
-
// `c8` in a scratch dir). Skipped where either is missing. The exec here
|
|
375
|
-
// is the container's `sh -c` with the container paths rewritten to this
|
|
376
|
-
// directory: the command is exactly what a container runs.
|
|
377
|
-
const c8TestDir = process.env.SPECTEST_C8_TEST_DIR || "/opt/spectest/coverage-tools";
|
|
378
|
-
const hasC8 = await fs
|
|
379
|
-
.access(path.join(c8TestDir, NODE_COVERAGE_C8_BIN))
|
|
380
|
-
.then(() => true)
|
|
381
|
-
.catch(() => false);
|
|
382
|
-
|
|
383
|
-
describe.if(hasNode && hasC8)("nodeCoverage → lcov (real node + c8)", () => {
|
|
384
|
-
function c8Ctx(dir: string): CoverageCaptureContext {
|
|
385
|
-
return {
|
|
386
|
-
...fakeCtx(dir),
|
|
387
|
-
async exec(command: string) {
|
|
388
|
-
const local = command
|
|
389
|
-
.replaceAll(NODE_COVERAGE_TOOLS_CONTAINER_DIR, c8TestDir)
|
|
390
|
-
.replaceAll(COVERAGE_CONTAINER_DIR, dir);
|
|
391
|
-
return new Promise((resolve, reject) => {
|
|
392
|
-
const p = spawn("sh", ["-c", local], { stdio: ["ignore", "pipe", "pipe"] });
|
|
393
|
-
let out = "";
|
|
394
|
-
p.stdout!.on("data", (b: Buffer) => (out += b));
|
|
395
|
-
p.stderr!.on("data", (b: Buffer) => (out += b));
|
|
396
|
-
p.on("exit", (code) => (code === 0 ? resolve({ stdout: out, stderr: "" }) : reject(new Error(out))));
|
|
397
|
-
});
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
async function withTools<T>(f: () => Promise<T>): Promise<T> {
|
|
402
|
-
const prior = process.env.SPECTEST_COVERAGE_TOOLS_DIR;
|
|
403
|
-
process.env.SPECTEST_COVERAGE_TOOLS_DIR = c8TestDir;
|
|
404
|
-
try {
|
|
405
|
-
return await f();
|
|
406
|
-
} finally {
|
|
407
|
-
if (prior === undefined) delete process.env.SPECTEST_COVERAGE_TOOLS_DIR;
|
|
408
|
-
else process.env.SPECTEST_COVERAGE_TOOLS_DIR = prior;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
test("the tools dir is found through the override, and the command is the container's", async () => {
|
|
413
|
-
await withTools(async () => {
|
|
414
|
-
expect(nodeCoverageToolsDir()).toBe(c8TestDir);
|
|
415
|
-
expect(await nodeCoverageToolsAvailable()).toBe(true);
|
|
416
|
-
});
|
|
417
|
-
expect(nodeCoverageConvertCommand()).toBe(
|
|
418
|
-
`cd / && env -u NODE_V8_COVERAGE -u NODE_OPTIONS node ${NODE_COVERAGE_TOOLS_CONTAINER_DIR}/${NODE_COVERAGE_C8_BIN} report --temp-directory ${COVERAGE_CONTAINER_DIR}/${NODE_COVERAGE_DUMPS_SUBDIR} --reporter=lcovonly --reports-dir ${COVERAGE_CONTAINER_DIR}/.c8`,
|
|
419
|
-
);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test("dumps are converted to node.lcov, the delta of this capture; the raw dumps never ship", async () => {
|
|
423
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covc8-"));
|
|
424
496
|
const hook = path.join(dir, "coverage-hook.cjs");
|
|
425
497
|
await fs.writeFile(hook, NODE_COVERAGE_HOOK);
|
|
426
|
-
await fs.writeFile(path.join(dir, "cli.js"), "function used() { return 1 }\
|
|
427
|
-
await
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
expect(await fs.readFile(path.join(dir, NODE_COVERAGE_LCOV), "utf8")).toBe("TN:\n");
|
|
498
|
+
await fs.writeFile(path.join(dir, "cli.js"), "function used() { return 1 }\nused();\n");
|
|
499
|
+
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA", sourcesContent: ["src"] }));
|
|
500
|
+
const url = `file://${path.join(dir, "cli.js")}`;
|
|
501
|
+
for (let i = 0; i < 2; i++) {
|
|
431
502
|
const cli = spawn("node", [path.join(dir, "cli.js")], {
|
|
432
503
|
env: { ...process.env, NODE_V8_COVERAGE: dir, NODE_OPTIONS: `--require ${hook}` },
|
|
433
504
|
stdio: "ignore",
|
|
434
505
|
});
|
|
435
506
|
expect(await new Promise<number | null>((r) => cli.on("exit", r))).toBe(0);
|
|
436
|
-
await node().capture!(
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
expect(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const lcov2 = await fs.readFile(path.join(dir, NODE_COVERAGE_LCOV), "utf8");
|
|
461
|
-
expect(lcov2).toMatch(/FNDA:1,used/);
|
|
462
|
-
expect((await fs.readdir(path.join(dir, NODE_COVERAGE_DUMPS_SUBDIR))).filter((n) => n.endsWith(".json")).length).toBe(1);
|
|
463
|
-
});
|
|
507
|
+
await node().capture!(fakeCtx(dir));
|
|
508
|
+
const [name] = (await reportsIn(dir)).filter((n) => n !== NODE_COVERAGE_EMPTY_REPORT);
|
|
509
|
+
const doc = JSON.parse(await fs.readFile(path.join(dir, name!), "utf8")) as {
|
|
510
|
+
result: { url: string }[];
|
|
511
|
+
"source-map-cache": Record<string, Record<string, unknown>>;
|
|
512
|
+
};
|
|
513
|
+
expect(doc.result.map((x) => x.url)).toEqual([url]);
|
|
514
|
+
const entry = doc["source-map-cache"][url]!;
|
|
515
|
+
if (i === 0) {
|
|
516
|
+
expect(entry[SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:/);
|
|
517
|
+
expect(entry.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA" });
|
|
518
|
+
expect(entry.lineLengths).toEqual([28, 7, 0]);
|
|
519
|
+
} else {
|
|
520
|
+
expect(Object.keys(entry)).toEqual([SOURCE_MAP_REF_KEY]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
resetNodeCoverageMemory();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("no process and no dump: an empty report is written, once", async () => {
|
|
527
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covhook-"));
|
|
528
|
+
await node().capture!(fakeCtx(dir));
|
|
529
|
+
await node().capture!(fakeCtx(dir));
|
|
530
|
+
expect(await reportsIn(dir)).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
464
531
|
});
|
|
465
532
|
});
|