@specific.dev/spectest 0.81.1 → 0.83.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 +90 -153
- package/dist/coverage.js +339 -419
- package/dist/daemon.js +11 -24
- package/dist/harness/coverage.d.ts +10 -4
- package/dist/harness/coverage.js +15 -6
- package/package.json +1 -1
- package/src/coverage.test.ts +175 -358
- package/src/coverage.ts +350 -449
- package/src/daemon.ts +11 -19
- package/src/harness/coverage.ts +16 -6
package/src/coverage.test.ts
CHANGED
|
@@ -4,27 +4,21 @@ 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_DIR_ENV,
|
|
7
8
|
NODE_COVERAGE_HOOK,
|
|
8
9
|
NODE_COVERAGE_EMPTY_REPORT,
|
|
9
10
|
NODE_COVERAGE_HOOK_PATH,
|
|
11
|
+
NODE_COVERAGE_SCRIPTS_SUBDIR,
|
|
12
|
+
NODE_COVERAGE_MARKERS_SUBDIR,
|
|
10
13
|
NODE_COVERAGE_SOCKET_PREFIX,
|
|
11
|
-
SOURCE_MAP_HASH_KEY,
|
|
12
|
-
SOURCE_MAP_REF_KEY,
|
|
13
14
|
applyCoverageAdapters,
|
|
14
15
|
appendEnvFlag,
|
|
15
|
-
attachSourceMaps,
|
|
16
|
-
lineLengthsOf,
|
|
17
|
-
resolveSourceMapsFromContainer,
|
|
18
|
-
sourceMapProbeCommand,
|
|
19
16
|
browser,
|
|
20
17
|
command,
|
|
21
18
|
coverageReportsMode,
|
|
22
|
-
|
|
19
|
+
isNodeDumpName,
|
|
23
20
|
node,
|
|
24
|
-
prepareV8Reports,
|
|
25
|
-
removeShippedV8Reports,
|
|
26
21
|
resetNodeCoverageMemory,
|
|
27
|
-
scriptExecuted,
|
|
28
22
|
validateCoverage,
|
|
29
23
|
type CoverageCaptureContext,
|
|
30
24
|
} from "./coverage.js";
|
|
@@ -68,10 +62,11 @@ describe("validateCoverage", () => {
|
|
|
68
62
|
});
|
|
69
63
|
|
|
70
64
|
describe("applyCoverageAdapters", () => {
|
|
71
|
-
test("node: appends NODE_OPTIONS, sets
|
|
65
|
+
test("node: appends NODE_OPTIONS, sets the coverage dir env, mounts the hook — never NODE_V8_COVERAGE", () => {
|
|
72
66
|
const out = applyCoverageAdapters("api", { ...base, coverage: { adapters: [node()] } });
|
|
73
67
|
expect(out.env?.NODE_OPTIONS).toBe(`--max-old-space-size=512 --require ${NODE_COVERAGE_HOOK_PATH}`);
|
|
74
|
-
expect(out.env?.
|
|
68
|
+
expect(out.env?.[NODE_COVERAGE_DIR_ENV]).toBe("/spectest/coverage");
|
|
69
|
+
expect(out.env?.NODE_V8_COVERAGE).toBeUndefined();
|
|
75
70
|
expect(out.files?.map((f) => f.path)).toEqual([NODE_COVERAGE_HOOK_PATH]);
|
|
76
71
|
expect(out.files?.[0]?.content).toBe(NODE_COVERAGE_HOOK);
|
|
77
72
|
});
|
|
@@ -99,217 +94,21 @@ describe("applyCoverageAdapters", () => {
|
|
|
99
94
|
});
|
|
100
95
|
});
|
|
101
96
|
|
|
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
|
-
|
|
105
|
-
describe("compactV8Document", () => {
|
|
106
|
-
const doc = {
|
|
107
|
-
result: [
|
|
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 },
|
|
114
|
-
],
|
|
115
|
-
"source-map-cache": {
|
|
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 },
|
|
118
|
-
"file:///app/node_modules/x/index.js": { lineLengths: [1], data: { mappings: "AAAA" }, url: null },
|
|
119
|
-
},
|
|
120
|
-
};
|
|
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", () => {
|
|
129
|
-
const out = compactV8Document(doc) as typeof doc;
|
|
130
|
-
expect(out.result.map((s) => s.url)).toEqual(["file:///app/src/a.ts"]);
|
|
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();
|
|
136
|
-
});
|
|
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);
|
|
154
|
-
});
|
|
155
|
-
test("a dump with no app script keeps an empty result and no cache", () => {
|
|
156
|
-
const out = compactV8Document({ result: [doc.result[2]], "source-map-cache": doc["source-map-cache"] });
|
|
157
|
-
expect(out.result).toEqual([]);
|
|
158
|
-
expect("source-map-cache" in out).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
test("prepareV8Reports compacts coverage-*.json once, ships maps once, and the next capture removes them", async () => {
|
|
161
|
-
resetNodeCoverageMemory();
|
|
162
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-cov-"));
|
|
163
|
-
await fs.writeFile(path.join(dir, "coverage-1-2-0.json"), JSON.stringify(doc));
|
|
164
|
-
await fs.writeFile(path.join(dir, "browser.lcov"), "TN:\n");
|
|
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;
|
|
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();
|
|
171
|
-
expect(await fs.readFile(path.join(dir, "browser.lcov"), "utf8")).toBe("TN:\n");
|
|
172
|
-
// Shipped: a second pass leaves it alone, even when it is not JSON any more.
|
|
173
|
-
await fs.writeFile(path.join(dir, "coverage-1-2-0.json"), "not json");
|
|
174
|
-
expect((await prepareV8Reports(dir)).dumps).toBe(0);
|
|
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();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
97
|
/** A capture context over a real directory, no container. */
|
|
189
98
|
function fakeCtx(dir: string, signal = new AbortController().signal): CoverageCaptureContext {
|
|
190
99
|
return {
|
|
191
100
|
service: "api",
|
|
192
101
|
reportDir: dir,
|
|
193
102
|
signal,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
async exec(command: string) {
|
|
197
|
-
return new Promise((resolve, reject) => {
|
|
198
|
-
const p = spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] });
|
|
199
|
-
let stdout = "";
|
|
200
|
-
let stderr = "";
|
|
201
|
-
p.stdout!.on("data", (b: Buffer) => (stdout += b));
|
|
202
|
-
p.stderr!.on("data", (b: Buffer) => (stderr += b));
|
|
203
|
-
p.on("exit", (code) => (code === 0 ? resolve({ stdout, stderr }) : reject(new Error(stderr || `exit ${code}`))));
|
|
204
|
-
});
|
|
103
|
+
async exec() {
|
|
104
|
+
throw new Error("no container");
|
|
205
105
|
},
|
|
206
106
|
async writeReport(name, content) {
|
|
207
107
|
await fs.writeFile(path.join(dir, name), content);
|
|
208
108
|
},
|
|
209
|
-
async readContainerFile(p) {
|
|
210
|
-
return fs.readFile(p).catch(() => null);
|
|
211
|
-
},
|
|
212
109
|
};
|
|
213
110
|
}
|
|
214
111
|
|
|
215
|
-
describe("source maps by reference", () => {
|
|
216
|
-
test("lineLengthsOf matches Node's shape: \\n splits, \\r stays, last line counted", () => {
|
|
217
|
-
expect(lineLengthsOf("ab\ncde\r\n\nf")).toEqual([2, 4, 0, 1]);
|
|
218
|
-
expect(lineLengthsOf("")).toEqual([0]);
|
|
219
|
-
expect(lineLengthsOf("x\n")).toEqual([1, 0]);
|
|
220
|
-
});
|
|
221
|
-
test("the probe: one exec, one line per script — the last comment's ref, the .map next to it, or none", async () => {
|
|
222
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covprobe-"));
|
|
223
|
-
await fs.writeFile(path.join(dir, "a.js"), "x\n//# sourceMappingURL=old.map\ny\n//# sourceMappingURL=maps/a.map\n");
|
|
224
|
-
await fs.writeFile(path.join(dir, "b.js"), "x();\n");
|
|
225
|
-
await fs.writeFile(path.join(dir, "b.js.map"), "{}");
|
|
226
|
-
await fs.writeFile(path.join(dir, "it's.js"), "x();\n");
|
|
227
|
-
const r = await fakeCtx(dir).exec(
|
|
228
|
-
sourceMapProbeCommand([path.join(dir, "a.js"), path.join(dir, "b.js"), path.join(dir, "it's.js"), path.join(dir, "gone.js")]),
|
|
229
|
-
);
|
|
230
|
-
expect(r.stdout).toBe(
|
|
231
|
-
`${path.join(dir, "a.js")}\tmaps/a.map\n` +
|
|
232
|
-
`${path.join(dir, "b.js")}\t${path.join(dir, "b.js.map")}\n` +
|
|
233
|
-
`${path.join(dir, "it's.js")}\t-\n` +
|
|
234
|
-
`${path.join(dir, "gone.js")}\t!\n`,
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
test("resolves the map a comment names, drops sourcesContent, computes line lengths", async () => {
|
|
238
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
239
|
-
await fs.mkdir(path.join(dir, "maps"));
|
|
240
|
-
await fs.writeFile(path.join(dir, "cli.js"), "const a = 1;\nconsole.log(a);\n//# sourceMappingURL=maps/cli.map\n");
|
|
241
|
-
await fs.writeFile(
|
|
242
|
-
path.join(dir, "maps", "cli.map"),
|
|
243
|
-
JSON.stringify({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [], sourcesContent: ["x"] }),
|
|
244
|
-
);
|
|
245
|
-
const url = `file://${path.join(dir, "cli.js")}`;
|
|
246
|
-
const out = await resolveSourceMapsFromContainer(fakeCtx(dir), [url, "node:fs"]);
|
|
247
|
-
expect([...out.keys()]).toEqual([url]);
|
|
248
|
-
const entry = out.get(url)!;
|
|
249
|
-
expect(entry.url).toBe(`file://${path.join(dir, "maps", "cli.map")}`);
|
|
250
|
-
expect(entry.data).toEqual({ version: 3, sources: ["../src/cli.ts"], mappings: "AAAA;AACA", names: [] });
|
|
251
|
-
expect(entry.lineLengths).toEqual([12, 15, 33, 0]);
|
|
252
|
-
});
|
|
253
|
-
test("with no comment, <script>.map next to the script is the map; inline data: maps decode; the rest is absent", async () => {
|
|
254
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covmap-"));
|
|
255
|
-
await fs.writeFile(path.join(dir, "cli.js"), "x();\n");
|
|
256
|
-
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA" }));
|
|
257
|
-
const map = JSON.stringify({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
258
|
-
await fs.writeFile(
|
|
259
|
-
path.join(dir, "inline.js"),
|
|
260
|
-
`x();\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(map).toString("base64")}\n`,
|
|
261
|
-
);
|
|
262
|
-
await fs.writeFile(path.join(dir, "bare.js"), "x();\n");
|
|
263
|
-
await fs.writeFile(path.join(dir, "gone.js"), "x();\n//# sourceMappingURL=gone.js.map\n");
|
|
264
|
-
await fs.writeFile(path.join(dir, "http.js"), "x();\n//# sourceMappingURL=https://cdn.test/x.map\n");
|
|
265
|
-
await fs.writeFile(path.join(dir, "notjson.js"), "x();\n");
|
|
266
|
-
await fs.writeFile(path.join(dir, "notjson.js.map"), "nope");
|
|
267
|
-
const u = (n: string) => `file://${path.join(dir, n)}`;
|
|
268
|
-
const out = await resolveSourceMapsFromContainer(fakeCtx(dir), [
|
|
269
|
-
u("cli.js"), u("inline.js"), u("bare.js"), u("gone.js"), u("http.js"), u("notjson.js"), u("missing.js"),
|
|
270
|
-
]);
|
|
271
|
-
expect([...out.keys()].sort()).toEqual([u("cli.js"), u("inline.js")].sort());
|
|
272
|
-
expect(out.get(u("cli.js"))!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
|
|
273
|
-
expect(out.get(u("cli.js"))!.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA" });
|
|
274
|
-
expect(out.get(u("inline.js"))!.url).toBeNull();
|
|
275
|
-
expect(out.get(u("inline.js"))!.data).toEqual({ version: 3, sources: ["a.ts"], mappings: "AAAA" });
|
|
276
|
-
const noRead = { ...fakeCtx(dir), readContainerFile: undefined };
|
|
277
|
-
expect((await resolveSourceMapsFromContainer(noRead, [u("cli.js")])).size).toBe(0);
|
|
278
|
-
});
|
|
279
|
-
test("attachSourceMaps: one batched lookup, a map once, a stub after, a missing map looked up once", async () => {
|
|
280
|
-
const calls: string[][] = [];
|
|
281
|
-
const entry = { url: null, data: { version: 3, sources: ["a.ts"], mappings: "AAAA" }, lineLengths: [4] };
|
|
282
|
-
const resolve = async (urls: string[]) => {
|
|
283
|
-
calls.push(urls);
|
|
284
|
-
return new Map(urls.filter((u) => u.endsWith("/a.js")).map((u) => [u, entry]));
|
|
285
|
-
};
|
|
286
|
-
const shipped = new Map<string, string>();
|
|
287
|
-
const missing = new Set<string>();
|
|
288
|
-
const dump = () => ({
|
|
289
|
-
result: [
|
|
290
|
-
{ url: "file:///app/a.js", ...ran },
|
|
291
|
-
{ url: "file:///app/b.js", ...ran },
|
|
292
|
-
],
|
|
293
|
-
});
|
|
294
|
-
const first = dump() as Record<string, unknown>;
|
|
295
|
-
expect(await attachSourceMaps(first, resolve, shipped, missing)).toBe(1);
|
|
296
|
-
const c1 = first["source-map-cache"] as Record<string, Record<string, unknown>>;
|
|
297
|
-
expect(c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:/);
|
|
298
|
-
expect(c1["file:///app/a.js"].lineLengths).toEqual([4]);
|
|
299
|
-
expect("file:///app/b.js" in c1).toBe(false);
|
|
300
|
-
expect(calls).toEqual([["file:///app/a.js", "file:///app/b.js"]]);
|
|
301
|
-
const second = dump() as Record<string, unknown>;
|
|
302
|
-
expect(await attachSourceMaps(second, resolve, shipped, missing)).toBe(0);
|
|
303
|
-
const c2 = second["source-map-cache"] as Record<string, Record<string, unknown>>;
|
|
304
|
-
expect(c2["file:///app/a.js"]).toEqual({ [SOURCE_MAP_REF_KEY]: c1["file:///app/a.js"][SOURCE_MAP_HASH_KEY] });
|
|
305
|
-
expect(calls.length).toBe(1); // b.js is not asked again
|
|
306
|
-
// A dump that carries its own entry is left alone.
|
|
307
|
-
const own = { ...dump(), "source-map-cache": { "file:///app/b.js": { data: { mappings: "" }, lineLengths: [1] } } } as Record<string, unknown>;
|
|
308
|
-
expect(await attachSourceMaps(own, resolve, shipped, missing)).toBe(0);
|
|
309
|
-
expect(calls.length).toBe(1);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
112
|
describe("browserCoverage", () => {
|
|
314
113
|
test("writes an empty lcov when nothing ran for the service", async () => {
|
|
315
114
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-cov-"));
|
|
@@ -334,219 +133,237 @@ describe("browserCoverage", () => {
|
|
|
334
133
|
});
|
|
335
134
|
|
|
336
135
|
// The node hook against a real `node` (the box has one; skipped where it
|
|
337
|
-
// does not).
|
|
338
|
-
// socket with a fresh
|
|
339
|
-
// wrapper process, the pnpm/tsx shape
|
|
340
|
-
//
|
|
341
|
-
//
|
|
136
|
+
// does not). The hook is the collector: every process armed with it
|
|
137
|
+
// must answer its own socket with a fresh dump — including a server
|
|
138
|
+
// behind a wrapper process, the pnpm/tsx shape — and write one at exit;
|
|
139
|
+
// a worker thread writes its own; a script record appears once per
|
|
140
|
+
// branch with the source's line lengths, every function, and the map
|
|
141
|
+
// found by comment, inline, or next to the script; stale sockets are
|
|
142
|
+
// removed; NODE_V8_COVERAGE is never set.
|
|
342
143
|
const hasNode = await new Promise<boolean>((resolve) => {
|
|
343
144
|
const p = spawn("node", ["--version"]);
|
|
344
145
|
p.on("error", () => resolve(false));
|
|
345
146
|
p.on("exit", (code) => resolve(code === 0));
|
|
346
147
|
});
|
|
347
148
|
|
|
348
|
-
const SERVER_JS = "
|
|
149
|
+
const SERVER_JS = "function tick() { return 1 }\nsetInterval(tick, 5); process.stdout.write('up\\n');\n";
|
|
349
150
|
// A wrapper that spawns the real server as a child node process and
|
|
350
151
|
// waits on it — what `pnpm exec`, `npm run` and the `tsx` binary do.
|
|
152
|
+
// It takes its child down with it on SIGTERM, so the test leaves no
|
|
153
|
+
// orphan holding the test runner's stdout pipe open.
|
|
351
154
|
const WRAPPER_JS =
|
|
352
155
|
"const { spawn } = require('node:child_process');\n" +
|
|
353
156
|
"const c = spawn(process.execPath, [require('node:path').join(__dirname, 'server.js')], { stdio: 'inherit' });\n" +
|
|
354
|
-
"c.on('exit', (code) => process.exit(code ?? 1));\n"
|
|
157
|
+
"c.on('exit', (code) => process.exit(code ?? 1));\n" +
|
|
158
|
+
"process.on('SIGTERM', () => { c.kill('SIGKILL'); process.exit(1); });\n";
|
|
159
|
+
|
|
160
|
+
interface Dump {
|
|
161
|
+
spectestVersion: number;
|
|
162
|
+
result: { url: string; spectestScript: string; functions: number[][] }[];
|
|
163
|
+
}
|
|
164
|
+
interface ScriptRecord {
|
|
165
|
+
spectestScript: {
|
|
166
|
+
url: string;
|
|
167
|
+
lineLengths: number[] | null;
|
|
168
|
+
functions: [string, number, number][];
|
|
169
|
+
sourceMap: { url: string | null; data: Record<string, unknown> } | null;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
355
172
|
|
|
356
173
|
describe.if(hasNode)("nodeCoverage hook (real node)", () => {
|
|
174
|
+
const hookEnv = (dir: string) => ({ ...process.env, [NODE_COVERAGE_DIR_ENV]: dir, NODE_OPTIONS: `--require ${path.join(dir, "hook.cjs")}` });
|
|
357
175
|
async function socketsIn(dir: string): Promise<string[]> {
|
|
358
176
|
return (await fs.readdir(dir)).filter((n) => n.startsWith(NODE_COVERAGE_SOCKET_PREFIX)).sort();
|
|
359
177
|
}
|
|
360
|
-
async function
|
|
361
|
-
return (await fs.readdir(dir)).filter(
|
|
178
|
+
async function dumpsIn(dir: string): Promise<string[]> {
|
|
179
|
+
return (await fs.readdir(dir)).filter(isNodeDumpName).sort();
|
|
180
|
+
}
|
|
181
|
+
async function readDump(dir: string, name: string): Promise<Dump> {
|
|
182
|
+
return JSON.parse(await fs.readFile(path.join(dir, name), "utf8")) as Dump;
|
|
183
|
+
}
|
|
184
|
+
async function records(dir: string): Promise<Map<string, ScriptRecord["spectestScript"]>> {
|
|
185
|
+
const out = new Map<string, ScriptRecord["spectestScript"]>();
|
|
186
|
+
const d = path.join(dir, NODE_COVERAGE_SCRIPTS_SUBDIR);
|
|
187
|
+
for (const n of (await fs.readdir(d).catch(() => [] as string[])).filter((n) => n.endsWith(".json"))) {
|
|
188
|
+
const r = JSON.parse(await fs.readFile(path.join(d, n), "utf8")) as ScriptRecord;
|
|
189
|
+
out.set(r.spectestScript.url, r.spectestScript);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
/** The names of a dump's ran functions, joined to the record on the extent. */
|
|
194
|
+
function ranNames(dump: Dump, rec: ScriptRecord["spectestScript"]): string[] {
|
|
195
|
+
const byExtent = new Map(rec.functions.map(([name, start, end]) => [`${start}-${end}`, name]));
|
|
196
|
+
return dump.result[0]!.functions.map((f) => byExtent.get(`${f[0]}-${f[0]! + f[1]!}`) ?? "?");
|
|
197
|
+
}
|
|
198
|
+
async function scratch(): Promise<string> {
|
|
199
|
+
resetNodeCoverageMemory();
|
|
200
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covhook-"));
|
|
201
|
+
await fs.writeFile(path.join(dir, "hook.cjs"), NODE_COVERAGE_HOOK);
|
|
202
|
+
await fs.mkdir(path.join(dir, NODE_COVERAGE_SCRIPTS_SUBDIR));
|
|
203
|
+
await fs.mkdir(path.join(dir, NODE_COVERAGE_MARKERS_SUBDIR), { recursive: true });
|
|
204
|
+
return dir;
|
|
362
205
|
}
|
|
363
206
|
async function start(dir: string, entry: string, expectSockets: number): Promise<ChildProcess> {
|
|
364
|
-
const hook = path.join(dir, "hook.cjs");
|
|
365
|
-
await fs.writeFile(hook, NODE_COVERAGE_HOOK);
|
|
366
207
|
await fs.writeFile(path.join(dir, "server.js"), SERVER_JS);
|
|
367
208
|
await fs.writeFile(path.join(dir, "wrapper.js"), WRAPPER_JS);
|
|
368
|
-
const child = spawn("node", [path.join(dir, entry)], {
|
|
369
|
-
env: { ...process.env, NODE_V8_COVERAGE: dir, NODE_OPTIONS: `--require ${hook}` },
|
|
370
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
371
|
-
});
|
|
209
|
+
const child = spawn("node", [path.join(dir, entry)], { env: hookEnv(dir), stdio: ["ignore", "pipe", "inherit"] });
|
|
372
210
|
await new Promise<void>((resolve) => child.stdout!.once("data", () => resolve()));
|
|
373
211
|
for (let i = 0; i < 100 && (await socketsIn(dir)).length < expectSockets; i++) {
|
|
374
212
|
await new Promise((r) => setTimeout(r, 20));
|
|
375
213
|
}
|
|
376
214
|
return child;
|
|
377
215
|
}
|
|
216
|
+
async function runOnce(dir: string, entry: string): Promise<number | null> {
|
|
217
|
+
const p = spawn("node", [path.join(dir, entry)], { env: hookEnv(dir), stdio: "ignore" });
|
|
218
|
+
return new Promise<number | null>((r) => p.on("exit", r));
|
|
219
|
+
}
|
|
378
220
|
|
|
379
|
-
test("capture asks the live server
|
|
380
|
-
const dir = await
|
|
221
|
+
test("capture asks the live server: a dump of what ran, a script record once, and only the app's code", async () => {
|
|
222
|
+
const dir = await scratch();
|
|
381
223
|
const server = await start(dir, "server.js", 1);
|
|
382
224
|
try {
|
|
383
|
-
expect(await
|
|
225
|
+
expect(await dumpsIn(dir)).toEqual([]);
|
|
384
226
|
await node().capture!(fakeCtx(dir));
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
expect(
|
|
398
|
-
expect(
|
|
399
|
-
|
|
227
|
+
const [name] = await dumpsIn(dir);
|
|
228
|
+
const dump = await readDump(dir, name!);
|
|
229
|
+
expect(dump.spectestVersion).toBe(2);
|
|
230
|
+
expect(dump.result.map((s) => s.url)).toEqual([`file://${path.join(dir, "server.js")}`]);
|
|
231
|
+
const fns = dump.result[0]!.functions;
|
|
232
|
+
// Triples: the extent, then blocks relative to it; some count > 0.
|
|
233
|
+
expect(fns.every((f) => f.length >= 3 && f.length % 3 === 0)).toBe(true);
|
|
234
|
+
expect(fns.every((f) => f.some((_, i) => i % 3 === 2 && f[i]! > 0))).toBe(true);
|
|
235
|
+
const rec = (await records(dir)).get(`file://${path.join(dir, "server.js")}`)!;
|
|
236
|
+
expect(ranNames(dump, rec).sort()).toEqual(["", "tick"]);
|
|
237
|
+
expect(rec.lineLengths).toEqual([28, 51, 0]);
|
|
238
|
+
expect(rec.functions.map((f) => f[0])).toEqual(["", "tick"]);
|
|
239
|
+
expect(rec.sourceMap).toBeNull();
|
|
240
|
+
expect(dump.result[0]!.spectestScript).toBeDefined();
|
|
241
|
+
const markers = await fs.readdir(path.join(dir, NODE_COVERAGE_MARKERS_SUBDIR));
|
|
242
|
+
expect(markers).toEqual([dump.result[0]!.spectestScript]);
|
|
400
243
|
|
|
401
|
-
// The next capture
|
|
402
|
-
//
|
|
244
|
+
// The next capture: the first dump is gone, a new one describes
|
|
245
|
+
// only what ran since (tick), and the record is not written again.
|
|
246
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
403
247
|
await node().capture!(fakeCtx(dir));
|
|
404
|
-
const
|
|
405
|
-
expect(
|
|
406
|
-
expect(
|
|
248
|
+
const names = await dumpsIn(dir);
|
|
249
|
+
expect(names.length).toBe(1);
|
|
250
|
+
expect(names[0]).not.toBe(name);
|
|
251
|
+
const second = await readDump(dir, names[0]!);
|
|
252
|
+
expect(ranNames(second, rec)).toEqual(["tick"]);
|
|
253
|
+
expect((await fs.readdir(path.join(dir, NODE_COVERAGE_SCRIPTS_SUBDIR))).length).toBe(0);
|
|
407
254
|
} finally {
|
|
408
255
|
server.kill("SIGKILL");
|
|
409
256
|
}
|
|
410
257
|
});
|
|
411
258
|
|
|
412
259
|
test("a server behind a wrapper process is dumped too (pnpm exec / tsx shape)", async () => {
|
|
413
|
-
const dir = await
|
|
260
|
+
const dir = await scratch();
|
|
414
261
|
const wrapper = await start(dir, "wrapper.js", 2);
|
|
415
262
|
try {
|
|
416
263
|
expect((await socketsIn(dir)).length).toBe(2);
|
|
417
264
|
await node().capture!(fakeCtx(dir));
|
|
418
|
-
const reports = await reportsIn(dir);
|
|
419
|
-
expect(reports.length).toBe(2);
|
|
420
265
|
const urls = new Set<string>();
|
|
421
|
-
for (const
|
|
422
|
-
const doc = JSON.parse(await fs.readFile(path.join(dir, r), "utf8")) as { result: { url: string }[] };
|
|
423
|
-
for (const s of doc.result) urls.add(s.url);
|
|
424
|
-
}
|
|
266
|
+
for (const n of await dumpsIn(dir)) for (const s of (await readDump(dir, n)).result) urls.add(s.url);
|
|
425
267
|
expect([...urls].some((u) => u.endsWith("/server.js"))).toBe(true);
|
|
426
268
|
expect([...urls].some((u) => u.endsWith("/wrapper.js"))).toBe(true);
|
|
427
269
|
} finally {
|
|
428
|
-
wrapper.kill("
|
|
429
|
-
|
|
430
|
-
const pids = (await socketsIn(dir)).map((n) => Number(n.slice(NODE_COVERAGE_SOCKET_PREFIX.length)));
|
|
431
|
-
for (const pid of pids) {
|
|
432
|
-
try {
|
|
433
|
-
process.kill(pid, "SIGKILL");
|
|
434
|
-
} catch {}
|
|
435
|
-
}
|
|
270
|
+
wrapper.kill("SIGTERM");
|
|
271
|
+
await new Promise<void>((r) => wrapper.on("exit", () => r()));
|
|
436
272
|
}
|
|
437
273
|
});
|
|
438
274
|
|
|
439
|
-
test("short-lived CLI processes
|
|
440
|
-
const dir = await
|
|
441
|
-
|
|
442
|
-
await fs.writeFile(
|
|
443
|
-
await fs.writeFile(path.join(dir, "cli.js"), "process.stdout.write('v1');\n");
|
|
275
|
+
test("short-lived CLI processes write at exit; the map next to the script is recorded once, no NODE_V8_COVERAGE", async () => {
|
|
276
|
+
const dir = await scratch();
|
|
277
|
+
await fs.writeFile(path.join(dir, "cli.js"), "function used() { return 1 }\nfunction unused() { return 2 }\nused();\n");
|
|
278
|
+
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA", sourcesContent: ["src"] }));
|
|
444
279
|
// Nothing has run yet: an empty report, not a failure.
|
|
445
280
|
await node().capture!(fakeCtx(dir));
|
|
446
|
-
expect(await
|
|
447
|
-
|
|
448
|
-
for (let i = 0; i < 2; i++) {
|
|
449
|
-
const cli = spawn("node", [path.join(dir, "cli.js")], {
|
|
450
|
-
env: { ...process.env, NODE_V8_COVERAGE: dir, NODE_OPTIONS: `--require ${hook}` },
|
|
451
|
-
stdio: "ignore",
|
|
452
|
-
});
|
|
453
|
-
expect(await new Promise<number | null>((r) => cli.on("exit", r))).toBe(0);
|
|
454
|
-
}
|
|
281
|
+
expect(await fs.readdir(dir).then((n) => n.filter((x) => x === NODE_COVERAGE_EMPTY_REPORT))).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
282
|
+
for (let i = 0; i < 2; i++) expect(await runOnce(dir, "cli.js")).toBe(0);
|
|
455
283
|
expect((await socketsIn(dir)).length).toBe(0);
|
|
284
|
+
expect(await fs.readdir(dir).then((n) => n.filter((x) => x.startsWith("coverage-") && x !== NODE_COVERAGE_EMPTY_REPORT))).toEqual([]); // no NODE_V8_COVERAGE dumps
|
|
285
|
+
await node().capture!(fakeCtx(dir));
|
|
286
|
+
const names = await dumpsIn(dir);
|
|
287
|
+
expect(names.length).toBe(2);
|
|
288
|
+
const recs = await records(dir);
|
|
289
|
+
expect(recs.size).toBe(1);
|
|
290
|
+
const rec = recs.get(`file://${path.join(dir, "cli.js")}`)!;
|
|
291
|
+
for (const n of names) {
|
|
292
|
+
const d = await readDump(dir, n);
|
|
293
|
+
expect(d.result.map((s) => s.url)).toEqual([`file://${path.join(dir, "cli.js")}`]);
|
|
294
|
+
expect(ranNames(d, rec).sort()).toEqual(["", "used"]);
|
|
295
|
+
}
|
|
296
|
+
expect(rec.functions.map((f) => f[0])).toEqual(["", "used", "unused"]);
|
|
297
|
+
expect(rec.lineLengths).toEqual([28, 30, 7, 0]);
|
|
298
|
+
expect(rec.sourceMap!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
|
|
299
|
+
expect(rec.sourceMap!.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA" });
|
|
300
|
+
// The next capture removes what shipped; a third run is a new dump
|
|
301
|
+
// and still no new record.
|
|
302
|
+
expect(await runOnce(dir, "cli.js")).toBe(0);
|
|
456
303
|
await node().capture!(fakeCtx(dir));
|
|
457
|
-
|
|
458
|
-
expect(
|
|
459
|
-
const doc = JSON.parse(await fs.readFile(path.join(dir, reports[0]!), "utf8")) as { result: { url: string }[] };
|
|
460
|
-
expect(doc.result.map((x) => x.url)).toEqual([`file://${path.join(dir, "cli.js")}`]); // compacted
|
|
304
|
+
expect((await dumpsIn(dir)).length).toBe(1);
|
|
305
|
+
expect((await records(dir)).size).toBe(0);
|
|
461
306
|
});
|
|
462
307
|
|
|
463
|
-
test("a
|
|
464
|
-
const dir = await
|
|
465
|
-
|
|
466
|
-
await fs.writeFile(
|
|
467
|
-
await fs.writeFile(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
308
|
+
test("maps: the file a comment names, and an inline data: map; a comment Node would have cached costs nothing", async () => {
|
|
309
|
+
const dir = await scratch();
|
|
310
|
+
await fs.mkdir(path.join(dir, "maps"));
|
|
311
|
+
await fs.writeFile(path.join(dir, "a.js"), "x = 1;\n//# sourceMappingURL=maps/a.map\n");
|
|
312
|
+
await fs.writeFile(path.join(dir, "maps", "a.map"), JSON.stringify({ version: 3, sources: ["../a.ts"], mappings: "AAAA" }));
|
|
313
|
+
const inline = JSON.stringify({ version: 3, sources: ["b.ts"], mappings: "AACA" });
|
|
314
|
+
await fs.writeFile(path.join(dir, "b.js"), `y = 2;\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(inline).toString("base64")}\n`);
|
|
315
|
+
await fs.writeFile(path.join(dir, "c.js"), "z = 3;\n//# sourceMappingURL=https://cdn.test/c.map\n");
|
|
316
|
+
await fs.writeFile(path.join(dir, "main.js"), "require('./a.js'); require('./b.js'); require('./c.js');\n");
|
|
317
|
+
expect(await runOnce(dir, "main.js")).toBe(0);
|
|
318
|
+
await node().capture!(fakeCtx(dir));
|
|
319
|
+
const recs = await records(dir);
|
|
320
|
+
expect(recs.get(`file://${path.join(dir, "a.js")}`)!.sourceMap).toEqual({
|
|
321
|
+
url: `file://${path.join(dir, "maps", "a.map")}`,
|
|
322
|
+
data: { version: 3, sources: ["../a.ts"], mappings: "AAAA" },
|
|
476
323
|
});
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
expect((await socketsIn(dir)).length).toBe(1);
|
|
481
|
-
await node().capture!(fakeCtx(dir));
|
|
482
|
-
const reports = (await reportsIn(dir)).filter((n) => n !== NODE_COVERAGE_EMPTY_REPORT);
|
|
483
|
-
expect(reports.length).toBe(1);
|
|
484
|
-
const doc = JSON.parse(await fs.readFile(path.join(dir, reports[0]!), "utf8")) as { result: { url: string }[] };
|
|
485
|
-
expect(doc.result.some((s) => s.url.endsWith("/server.js"))).toBe(true);
|
|
486
|
-
} finally {
|
|
487
|
-
child.kill("SIGKILL");
|
|
488
|
-
}
|
|
324
|
+
expect(recs.get(`file://${path.join(dir, "b.js")}`)!.sourceMap).toEqual({ url: null, data: { version: 3, sources: ["b.ts"], mappings: "AACA" } });
|
|
325
|
+
expect(recs.get(`file://${path.join(dir, "c.js")}`)!.sourceMap).toBeNull();
|
|
326
|
+
expect(recs.get(`file://${path.join(dir, "main.js")}`)!.sourceMap).toBeNull();
|
|
489
327
|
});
|
|
490
328
|
|
|
491
|
-
test("a
|
|
492
|
-
const dir = await
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
329
|
+
test("a worker thread writes its own dump at exit; only the main thread binds the socket", async () => {
|
|
330
|
+
const dir = await scratch();
|
|
331
|
+
await fs.writeFile(
|
|
332
|
+
path.join(dir, "threads.js"),
|
|
333
|
+
"const { Worker, isMainThread } = require('node:worker_threads');\n" +
|
|
334
|
+
"function inWorker() { return 1 }\n" +
|
|
335
|
+
"if (isMainThread) { new Worker(__filename); } else { inWorker(); }\n",
|
|
336
|
+
);
|
|
337
|
+
expect(await runOnce(dir, "threads.js")).toBe(0);
|
|
338
|
+
expect((await socketsIn(dir)).length).toBe(0);
|
|
497
339
|
await node().capture!(fakeCtx(dir));
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
340
|
+
const names = await dumpsIn(dir);
|
|
341
|
+
expect(names.length).toBe(2);
|
|
342
|
+
const byThread = new Map<string, Dump>();
|
|
343
|
+
for (const n of names) byThread.set(n.split("-")[2]!, await readDump(dir, n));
|
|
344
|
+
const recs = await records(dir);
|
|
345
|
+
expect(recs.size).toBe(1); // one script, one record, whichever thread came first
|
|
346
|
+
const rec = recs.get(`file://${path.join(dir, "threads.js")}`)!;
|
|
347
|
+
const worker = [...byThread.entries()].find(([t]) => t !== "0")![1];
|
|
348
|
+
expect(ranNames(worker, rec)).toContain("inWorker");
|
|
349
|
+
const main = byThread.get("0")!;
|
|
350
|
+
expect(ranNames(main, rec)).not.toContain("inWorker");
|
|
506
351
|
});
|
|
507
352
|
|
|
508
|
-
test("a
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const hook = path.join(dir, "coverage-hook.cjs");
|
|
517
|
-
await fs.writeFile(hook, NODE_COVERAGE_HOOK);
|
|
518
|
-
await fs.writeFile(path.join(dir, "cli.js"), "function used() { return 1 }\nused();\n");
|
|
519
|
-
await fs.writeFile(path.join(dir, "cli.js.map"), JSON.stringify({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA", sourcesContent: ["src"] }));
|
|
520
|
-
const url = `file://${path.join(dir, "cli.js")}`;
|
|
521
|
-
for (let i = 0; i < 2; i++) {
|
|
522
|
-
const cli = spawn("node", [path.join(dir, "cli.js")], {
|
|
523
|
-
env: { ...process.env, NODE_V8_COVERAGE: dir, NODE_OPTIONS: `--require ${hook}` },
|
|
524
|
-
stdio: "ignore",
|
|
525
|
-
});
|
|
526
|
-
expect(await new Promise<number | null>((r) => cli.on("exit", r))).toBe(0);
|
|
527
|
-
await node().capture!(fakeCtx(dir));
|
|
528
|
-
const [name] = (await reportsIn(dir)).filter((n) => n !== NODE_COVERAGE_EMPTY_REPORT);
|
|
529
|
-
const doc = JSON.parse(await fs.readFile(path.join(dir, name!), "utf8")) as {
|
|
530
|
-
result: { url: string }[];
|
|
531
|
-
"source-map-cache": Record<string, Record<string, unknown>>;
|
|
532
|
-
};
|
|
533
|
-
expect(doc.result.map((x) => x.url)).toEqual([url]);
|
|
534
|
-
const entry = doc["source-map-cache"][url]!;
|
|
535
|
-
if (i === 0) {
|
|
536
|
-
expect(entry[SOURCE_MAP_HASH_KEY]).toMatch(/^sha256:/);
|
|
537
|
-
expect(entry.data).toEqual({ version: 3, sources: ["cli.ts"], mappings: "AAAA;AACA" });
|
|
538
|
-
expect(entry.lineLengths).toEqual([28, 7, 0]);
|
|
539
|
-
} else {
|
|
540
|
-
expect(Object.keys(entry)).toEqual([SOURCE_MAP_REF_KEY]);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
resetNodeCoverageMemory();
|
|
353
|
+
test("a stale socket left by a killed process is removed at capture", async () => {
|
|
354
|
+
const dir = await scratch();
|
|
355
|
+
const server = await start(dir, "server.js", 1);
|
|
356
|
+
server.kill("SIGKILL");
|
|
357
|
+
await new Promise<void>((r) => server.on("exit", () => r()));
|
|
358
|
+
expect((await socketsIn(dir)).length).toBe(1); // SIGKILL: no exit handler, socket left behind
|
|
359
|
+
await node().capture!(fakeCtx(dir));
|
|
360
|
+
expect((await socketsIn(dir)).length).toBe(0);
|
|
544
361
|
});
|
|
545
362
|
|
|
546
363
|
test("no process and no dump: an empty report is written, once", async () => {
|
|
547
|
-
const dir = await
|
|
364
|
+
const dir = await scratch();
|
|
548
365
|
await node().capture!(fakeCtx(dir));
|
|
549
366
|
await node().capture!(fakeCtx(dir));
|
|
550
|
-
expect(await
|
|
367
|
+
expect((await fs.readdir(dir)).filter((n) => n.endsWith(".json"))).toEqual([NODE_COVERAGE_EMPTY_REPORT]);
|
|
551
368
|
});
|
|
552
369
|
});
|