@specific.dev/spectest 0.82.0 → 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.
@@ -144,9 +144,12 @@ export declare const NODE_COVERAGE_DUMP_PREFIX = "node-";
144
144
  * - Writes a **dump** `node-<pid>-<thread>-<ms>-<n>.json`: the app's own
145
145
  * scripts (`file://`, outside `node_modules` and `.cache`, not the
146
146
  * hook) in which some range count is above zero, and of each only the
147
- * functions that ran, ranges and counts as V8 reported them. Nothing
148
- * when nothing ran. Still a V8 coverage document (`result`), plus
149
- * `spectestScript` on each script naming its record.
147
+ * functions that ran, each one flat array: `[start, length, count]`
148
+ * for its extent (V8's first range), then `[offset from start,
149
+ * length, count]` per block range in V8's order. Name and block flag
150
+ * are in the record, joined on the extent. Nothing when nothing ran.
151
+ * `{ spectestVersion: 2, result: [{ url, spectestScript, functions:
152
+ * [[s, l, c, o, l, c, …], …] }] }`.
150
153
  * - Writes, **once per script per branch**, a **script record**
151
154
  * `scripts/<id>.json` (`id` = sha256 of the URL): the length of every
152
155
  * line of the source V8 compiled (`Debugger.getScriptSource`, so a
package/dist/coverage.js CHANGED
@@ -156,9 +156,12 @@ export const NODE_COVERAGE_DUMP_PREFIX = "node-";
156
156
  * - Writes a **dump** `node-<pid>-<thread>-<ms>-<n>.json`: the app's own
157
157
  * scripts (`file://`, outside `node_modules` and `.cache`, not the
158
158
  * hook) in which some range count is above zero, and of each only the
159
- * functions that ran, ranges and counts as V8 reported them. Nothing
160
- * when nothing ran. Still a V8 coverage document (`result`), plus
161
- * `spectestScript` on each script naming its record.
159
+ * functions that ran, each one flat array: `[start, length, count]`
160
+ * for its extent (V8's first range), then `[offset from start,
161
+ * length, count]` per block range in V8's order. Name and block flag
162
+ * are in the record, joined on the extent. Nothing when nothing ran.
163
+ * `{ spectestVersion: 2, result: [{ url, spectestScript, functions:
164
+ * [[s, l, c, o, l, c, …], …] }] }`.
162
165
  * - Writes, **once per script per branch**, a **script record**
163
166
  * `scripts/<id>.json` (`id` = sha256 of the URL): the length of every
164
167
  * line of the source V8 compiled (`Debugger.getScriptSource`, so a
@@ -331,10 +334,27 @@ export const NODE_COVERAGE_HOOK = `"use strict";
331
334
  }
332
335
  post("Debugger.disable");
333
336
  }
337
+ // A ran function is one flat array: [start, length, count] for its
338
+ // extent (V8's first range), then [offset from start, length, count]
339
+ // per block range in V8's order. Small numbers, so the dump gzips to
340
+ // less than half of V8's own JSON; name and block flag live in the
341
+ // script record, joined on the extent.
334
342
  const doc = {
335
- spectestVersion: 1,
343
+ spectestVersion: 2,
336
344
  result: kept.map(function (s) {
337
- return { url: s.url, scriptId: s.scriptId, spectestScript: idOf(s.url), functions: s.functions.filter(ran) };
345
+ return {
346
+ url: s.url,
347
+ spectestScript: idOf(s.url),
348
+ functions: s.functions.filter(ran).map(function (f) {
349
+ const first = f.ranges[0];
350
+ const out = [first.startOffset, first.endOffset - first.startOffset, first.count];
351
+ for (let i = 1; i < f.ranges.length; i++) {
352
+ const r = f.ranges[i];
353
+ out.push(r.startOffset - first.startOffset, r.endOffset - r.startOffset, r.count);
354
+ }
355
+ return out;
356
+ }),
357
+ };
338
358
  }),
339
359
  };
340
360
  const name = ${JSON.stringify(NODE_COVERAGE_DUMP_PREFIX)} + process.pid + "-" + wt.threadId + "-" + Date.now() + "-" + seq++ + ".json";
@@ -57,9 +57,10 @@ export declare function hasLcovSourceFile(text: string): boolean;
57
57
  export declare function isLcovShaped(text: string): boolean;
58
58
  /**
59
59
  * True when `text` looks like a V8 coverage JSON document — what
60
- * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write: an object with a
61
- * `result` array of scripts. A cheap shape check, not a parse (a report
62
- * can be megabytes); the control plane parses for real.
60
+ * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write, and the shape the
61
+ * `node()` hook's dumps keep: an object with a `result` array of
62
+ * scripts. A cheap shape check, not a parse (a report can be
63
+ * megabytes); the control plane parses for real.
63
64
  */
64
65
  export declare function isV8CoverageJson(text: string): boolean;
65
66
  /** True when `text` is a script record the `node()` hook writes
@@ -56,9 +56,10 @@ export function isLcovShaped(text) {
56
56
  }
57
57
  /**
58
58
  * True when `text` looks like a V8 coverage JSON document — what
59
- * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write: an object with a
60
- * `result` array of scripts. A cheap shape check, not a parse (a report
61
- * can be megabytes); the control plane parses for real.
59
+ * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write, and the shape the
60
+ * `node()` hook's dumps keep: an object with a `result` array of
61
+ * scripts. A cheap shape check, not a parse (a report can be
62
+ * megabytes); the control plane parses for real.
62
63
  */
63
64
  export function isV8CoverageJson(text) {
64
65
  return /^\s*\{/.test(text) && /"result"\s*:\s*\[/.test(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -159,7 +159,7 @@ const WRAPPER_JS =
159
159
 
160
160
  interface Dump {
161
161
  spectestVersion: number;
162
- result: { url: string; spectestScript: string; functions: { functionName: string; ranges: { count: number }[] }[] }[];
162
+ result: { url: string; spectestScript: string; functions: number[][] }[];
163
163
  }
164
164
  interface ScriptRecord {
165
165
  spectestScript: {
@@ -190,6 +190,11 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
190
190
  }
191
191
  return out;
192
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
+ }
193
198
  async function scratch(): Promise<string> {
194
199
  resetNodeCoverageMemory();
195
200
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-covhook-"));
@@ -221,11 +226,14 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
221
226
  await node().capture!(fakeCtx(dir));
222
227
  const [name] = await dumpsIn(dir);
223
228
  const dump = await readDump(dir, name!);
224
- expect(dump.spectestVersion).toBe(1);
229
+ expect(dump.spectestVersion).toBe(2);
225
230
  expect(dump.result.map((s) => s.url)).toEqual([`file://${path.join(dir, "server.js")}`]);
226
231
  const fns = dump.result[0]!.functions;
227
- expect(fns.every((f) => f.ranges.some((r) => r.count > 0))).toBe(true);
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);
228
235
  const rec = (await records(dir)).get(`file://${path.join(dir, "server.js")}`)!;
236
+ expect(ranNames(dump, rec).sort()).toEqual(["", "tick"]);
229
237
  expect(rec.lineLengths).toEqual([28, 51, 0]);
230
238
  expect(rec.functions.map((f) => f[0])).toEqual(["", "tick"]);
231
239
  expect(rec.sourceMap).toBeNull();
@@ -241,7 +249,7 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
241
249
  expect(names.length).toBe(1);
242
250
  expect(names[0]).not.toBe(name);
243
251
  const second = await readDump(dir, names[0]!);
244
- expect(second.result[0]!.functions.map((f) => f.functionName)).toEqual(["tick"]);
252
+ expect(ranNames(second, rec)).toEqual(["tick"]);
245
253
  expect((await fs.readdir(path.join(dir, NODE_COVERAGE_SCRIPTS_SUBDIR))).length).toBe(0);
246
254
  } finally {
247
255
  server.kill("SIGKILL");
@@ -277,14 +285,14 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
277
285
  await node().capture!(fakeCtx(dir));
278
286
  const names = await dumpsIn(dir);
279
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")}`)!;
280
291
  for (const n of names) {
281
292
  const d = await readDump(dir, n);
282
293
  expect(d.result.map((s) => s.url)).toEqual([`file://${path.join(dir, "cli.js")}`]);
283
- expect(d.result[0]!.functions.map((f) => f.functionName).sort()).toEqual(["", "used"]);
294
+ expect(ranNames(d, rec).sort()).toEqual(["", "used"]);
284
295
  }
285
- const recs = await records(dir);
286
- expect(recs.size).toBe(1);
287
- const rec = recs.get(`file://${path.join(dir, "cli.js")}`)!;
288
296
  expect(rec.functions.map((f) => f[0])).toEqual(["", "used", "unused"]);
289
297
  expect(rec.lineLengths).toEqual([28, 30, 7, 0]);
290
298
  expect(rec.sourceMap!.url).toBe(`file://${path.join(dir, "cli.js.map")}`);
@@ -333,11 +341,13 @@ describe.if(hasNode)("nodeCoverage hook (real node)", () => {
333
341
  expect(names.length).toBe(2);
334
342
  const byThread = new Map<string, Dump>();
335
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")}`)!;
336
347
  const worker = [...byThread.entries()].find(([t]) => t !== "0")![1];
337
- expect(worker.result[0]!.functions.some((f) => f.functionName === "inWorker")).toBe(true);
348
+ expect(ranNames(worker, rec)).toContain("inWorker");
338
349
  const main = byThread.get("0")!;
339
- expect(main.result[0]!.functions.some((f) => f.functionName === "inWorker")).toBe(false);
340
- expect((await records(dir)).size).toBe(1); // one script, one record, whichever thread came first
350
+ expect(ranNames(main, rec)).not.toContain("inWorker");
341
351
  });
342
352
 
343
353
  test("a stale socket left by a killed process is removed at capture", async () => {
package/src/coverage.ts CHANGED
@@ -258,9 +258,12 @@ export const NODE_COVERAGE_DUMP_PREFIX = "node-";
258
258
  * - Writes a **dump** `node-<pid>-<thread>-<ms>-<n>.json`: the app's own
259
259
  * scripts (`file://`, outside `node_modules` and `.cache`, not the
260
260
  * hook) in which some range count is above zero, and of each only the
261
- * functions that ran, ranges and counts as V8 reported them. Nothing
262
- * when nothing ran. Still a V8 coverage document (`result`), plus
263
- * `spectestScript` on each script naming its record.
261
+ * functions that ran, each one flat array: `[start, length, count]`
262
+ * for its extent (V8's first range), then `[offset from start,
263
+ * length, count]` per block range in V8's order. Name and block flag
264
+ * are in the record, joined on the extent. Nothing when nothing ran.
265
+ * `{ spectestVersion: 2, result: [{ url, spectestScript, functions:
266
+ * [[s, l, c, o, l, c, …], …] }] }`.
264
267
  * - Writes, **once per script per branch**, a **script record**
265
268
  * `scripts/<id>.json` (`id` = sha256 of the URL): the length of every
266
269
  * line of the source V8 compiled (`Debugger.getScriptSource`, so a
@@ -433,10 +436,27 @@ export const NODE_COVERAGE_HOOK = `"use strict";
433
436
  }
434
437
  post("Debugger.disable");
435
438
  }
439
+ // A ran function is one flat array: [start, length, count] for its
440
+ // extent (V8's first range), then [offset from start, length, count]
441
+ // per block range in V8's order. Small numbers, so the dump gzips to
442
+ // less than half of V8's own JSON; name and block flag live in the
443
+ // script record, joined on the extent.
436
444
  const doc = {
437
- spectestVersion: 1,
445
+ spectestVersion: 2,
438
446
  result: kept.map(function (s) {
439
- return { url: s.url, scriptId: s.scriptId, spectestScript: idOf(s.url), functions: s.functions.filter(ran) };
447
+ return {
448
+ url: s.url,
449
+ spectestScript: idOf(s.url),
450
+ functions: s.functions.filter(ran).map(function (f) {
451
+ const first = f.ranges[0];
452
+ const out = [first.startOffset, first.endOffset - first.startOffset, first.count];
453
+ for (let i = 1; i < f.ranges.length; i++) {
454
+ const r = f.ranges[i];
455
+ out.push(r.startOffset - first.startOffset, r.endOffset - r.startOffset, r.count);
456
+ }
457
+ return out;
458
+ }),
459
+ };
440
460
  }),
441
461
  };
442
462
  const name = ${JSON.stringify(NODE_COVERAGE_DUMP_PREFIX)} + process.pid + "-" + wt.threadId + "-" + Date.now() + "-" + seq++ + ".json";
@@ -108,9 +108,10 @@ export function isLcovShaped(text: string): boolean {
108
108
 
109
109
  /**
110
110
  * True when `text` looks like a V8 coverage JSON document — what
111
- * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write: an object with a
112
- * `result` array of scripts. A cheap shape check, not a parse (a report
113
- * can be megabytes); the control plane parses for real.
111
+ * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write, and the shape the
112
+ * `node()` hook's dumps keep: an object with a `result` array of
113
+ * scripts. A cheap shape check, not a parse (a report can be
114
+ * megabytes); the control plane parses for real.
114
115
  */
115
116
  export function isV8CoverageJson(text: string): boolean {
116
117
  return /^\s*\{/.test(text) && /"result"\s*:\s*\[/.test(text);