@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.
@@ -8,22 +8,11 @@ export declare const NODE_COVERAGE_EMPTY_REPORT = "coverage-spectest-empty.json"
8
8
  /** Prefix of the per-process sockets the node hook answers on, relative
9
9
  * to the coverage dir: `.ctl-<pid>`. */
10
10
  export declare const NODE_COVERAGE_SOCKET_PREFIX = ".ctl-";
11
- /** Where golden keeps the conversion tools (`c8` and its dependencies),
12
- * seen from the VM. `SPECTEST_COVERAGE_TOOLS_DIR` overrides it (tests).
13
- * The daemon bind-mounts it read-only into every covered container at
14
- * {@link NODE_COVERAGE_TOOLS_CONTAINER_DIR} when it exists. */
15
- export declare const NODE_COVERAGE_TOOLS_DIR = "/opt/spectest/coverage-tools";
16
- /** Where the container sees {@link NODE_COVERAGE_TOOLS_DIR}. */
17
- export declare const NODE_COVERAGE_TOOLS_CONTAINER_DIR = "/spectest/coverage-tools";
18
- /** The `c8` entry point, relative to the tools dir. */
19
- export declare const NODE_COVERAGE_C8_BIN = "node_modules/c8/bin/c8.js";
20
- /** The hidden subdirectory of the coverage dir where `node()` parks the
21
- * V8 dumps it converts at one capture. Hidden, so the harness never
22
- * ships them: the lcov is the report. Emptied at every capture — the
23
- * lcov is this capture's delta, see {@link convertV8ReportsToLcov}. */
24
- export declare const NODE_COVERAGE_DUMPS_SUBDIR = ".v8";
25
- /** The lcov `node()` writes: the dumps of this capture, merged. */
26
- export declare const NODE_COVERAGE_LCOV = "node.lcov";
11
+ /** Key of the content hash (`sha256:<hex>`) a shipped `source-map-cache`
12
+ * entry carries, and key of the stub that stands in for an entry that
13
+ * shipped earlier on the same branch. See {@link compactV8Document}. */
14
+ export declare const SOURCE_MAP_HASH_KEY = "spectestHash";
15
+ export declare const SOURCE_MAP_REF_KEY = "spectestRef";
27
16
  /** What `configure` learns about the service it rewrites. */
28
17
  export interface CoverageConfigureInfo {
29
18
  /** The services-map key. */
@@ -64,6 +53,9 @@ export interface CoverageCaptureContext {
64
53
  }>;
65
54
  /** Write one report file (name relative to the directory). */
66
55
  writeReport(name: string, content: string): Promise<void>;
56
+ /** Read one file out of the container (`docker cp`). `null` when it
57
+ * does not exist. Absent on a context that has no container. */
58
+ readContainerFile?(containerPath: string): Promise<Buffer | null>;
67
59
  /** Aborts when the per-service capture budget runs out. */
68
60
  signal: AbortSignal;
69
61
  }
@@ -146,50 +138,28 @@ export declare const NODE_COVERAGE_HOOK: string;
146
138
  * capture time — the server, and any wrapper it sits behind (`pnpm exec`,
147
139
  * `tsx`). Nothing for the app to write.
148
140
  *
149
- * At capture the dumps are **converted to lcov inside the container**
150
- * with `c8` (`c8 report`), and `node.lcov` is the one report that
151
- * ships. It holds **what ran since the previous capture**: V8 resets its
152
- * counters at every `takeCoverage()`, so a live server's dump after a
153
- * test is that test's own execution — the boot dump (everything loaded,
154
- * which at file level is everything "ran") lands in the bring-up
155
- * capture and nowhere else. A run's total is the union over its cases
156
- * on the server; a test's set is the test's own. Merging every dump on
157
- * the branch instead was tried first and would have made a long-lived
158
- * server's per-test set the boot set, every time. The conversion runs
159
- * in the container, not in the harness, because `v8-to-istanbul` needs
160
- * the script text (for byte offsets → lines) and the source maps
161
- * (`--enable-source-maps`, or a `sourceMappingURL` next to each file),
162
- * and only the container has them. `c8` comes from golden
163
- * ({@link NODE_COVERAGE_TOOLS_DIR}); on a guest without it the raw V8
164
- * documents ship instead, compacted ({@link compactV8Document}), as they
165
- * did before SDK 0.60.
141
+ * What ships is the V8 documents themselves, compacted
142
+ * ({@link compactV8Document}): the app's own scripts that ran, and each
143
+ * script's source map once per branch. A map comes from Node's own
144
+ * `source-map-cache` when Node found one through the script's
145
+ * `sourceMappingURL` (it caches maps under NODE_V8_COVERAGE with or
146
+ * without `--enable-source-maps`), else from `<script>.map` next to the
147
+ * script, read out of the container once
148
+ * ({@link resolveSourceMapFromContainer}). The second way is the one to
149
+ * build a short-lived bundled process in: a map Node finds is serialized
150
+ * whole into every exit dump — 12 MB per invocation for a bundled CLI,
151
+ * inside the test's own time — and parsed again by the harness; a map
152
+ * with no comment costs nothing there. Nothing is converted in the VM.
153
+ * A dump is a **delta** by construction: V8 resets its counters at every
154
+ * `takeCoverage()`, so a live server's dump after a test is that test's
155
+ * own execution, and the boot dump (everything loaded) lands in the
156
+ * bring-up capture and nowhere else. The control plane derives lines
157
+ * from the stored documents, off the test run. Converting to lcov in
158
+ * the container (`c8`, SDK 0.60 to 0.79) took seconds per capture per
159
+ * service on a real project, inside the test run; a real project turned
160
+ * coverage off because of it, and the conversion went.
166
161
  */
167
162
  export declare function node(): CoverageAdapter;
168
- /** The VM-side tools dir, after the test override. */
169
- export declare function nodeCoverageToolsDir(): string;
170
- /** True when golden (or the override) ships `c8`. The daemon mounts the
171
- * directory into covered containers on the same check, so what the VM
172
- * has is what the container sees. */
173
- export declare function nodeCoverageToolsAvailable(): Promise<boolean>;
174
- /** The command `convertV8ReportsToLcov` runs in the container. From `/`,
175
- * so lcov paths come out relative to the root (made absolute after). */
176
- export declare function nodeCoverageConvertCommand(): string;
177
- /**
178
- * Convert the V8 dumps written since the previous capture to one lcov,
179
- * `node.lcov`. New dumps at the root of the dir are compacted to the
180
- * app's scripts (their maps and `sourcesContent` kept — `v8-to-istanbul`
181
- * reads the original sources from there when they are not on disk, the
182
- * usual shape of a multi-stage image) and moved into `.v8/`, which is
183
- * emptied first; `c8 report` merges that directory. So the lcov is this
184
- * capture's delta: what the live processes ran since their last take,
185
- * plus every process that exited since. No new dump ⇒ `TN:` alone,
186
- * nothing ran. The `SF:` paths come out relative to `/` and are made
187
- * absolute, so they read as container paths like every other tool's.
188
- */
189
- export declare function convertV8ReportsToLcov(ctx: CoverageCaptureContext): Promise<void>;
190
- /** `SF:` records relative to `/` (what `c8` run from `/` writes) made
191
- * absolute. An lcov with nothing in it becomes the empty report. */
192
- export declare function absoluteLcovPaths(lcov: string): string;
193
163
  /**
194
164
  * Ask every node process that holds a hook socket in `dir` for a dump.
195
165
  * A socket nobody answers (its process died without unlinking — SIGKILL,
@@ -197,27 +167,99 @@ export declare function absoluteLcovPaths(lcov: string): string;
197
167
  * answered with an error throws.
198
168
  */
199
169
  export declare function dumpAllNodeProcesses(dir: string, signal: AbortSignal): Promise<number>;
170
+ /** Forget the branch memory (tests). */
171
+ export declare function resetNodeCoverageMemory(): void;
172
+ /** Remove from `dir` the reports an earlier capture shipped. */
173
+ export declare function removeShippedV8Reports(dir: string): Promise<void>;
200
174
  /** A script is the app's own when it is a file outside node_modules —
201
175
  * and not our hook, which every dump would otherwise carry. */
202
176
  export declare function isAppScriptUrl(url: string): boolean;
177
+ interface V8Range {
178
+ count?: unknown;
179
+ }
180
+ interface V8Function {
181
+ ranges?: V8Range[];
182
+ }
183
+ interface V8Script {
184
+ url?: unknown;
185
+ functions?: V8Function[];
186
+ }
187
+ /** True when any range of any function of the script has a count above
188
+ * zero: something in it ran since the previous take. A script whose
189
+ * every count is zero is one V8 still lists after a reset; it says
190
+ * nothing and is dropped. */
191
+ export declare function scriptExecuted(script: V8Script): boolean;
203
192
  /**
204
- * Compact one V8 coverage document to the app's own scripts. What Node
205
- * writes is everything the process loaded: `node:` internals, every
193
+ * Compact one V8 coverage document to what the derivation needs. What
194
+ * Node writes is everything the process loaded: `node:` internals, every
206
195
  * `node_modules` file, and — under `--enable-source-maps` — a
207
196
  * `source-map-cache` with each file's full map **and its sources**,
208
197
  * repeated in every dump. Measured on a real project: a 12–20 MiB dump
209
198
  * per capture, of which the app's own coverage was under 0.5 MiB, and a
210
199
  * suite that hit the 64 MiB cap on its third test. Kept: `file://`
211
- * scripts outside `node_modules`, the map entries of exactly those
212
- * scripts, minus `sourcesContent` (the sources are the repo) unless
213
- * `keepSourcesContent` — the lcov conversion reads original sources from
214
- * there. Still a V8 document — nothing is converted here.
200
+ * scripts outside `node_modules` in which something ran, and the map
201
+ * entries of exactly those scripts, minus `sourcesContent` (the sources
202
+ * are the repo). Each kept map entry carries its content hash under
203
+ * {@link SOURCE_MAP_HASH_KEY}; when `shipped` holds the same hash for the
204
+ * URL — the map went out with an earlier dump on this branch — a stub
205
+ * `{ [SOURCE_MAP_REF_KEY]: hash }` stands in for it, and the reader finds
206
+ * the map in an earlier capture of the branch. `shipped` is updated in
207
+ * place. Still a V8 document — nothing is converted here.
208
+ */
209
+ export declare function compactV8Document(doc: Record<string, unknown>, shipped?: Map<string, string>): Record<string, unknown>;
210
+ /** What {@link prepareV8Reports} did, for the capture's log line. */
211
+ export interface PrepareV8Stats {
212
+ dumps: number;
213
+ scripts: number;
214
+ bytesIn: number;
215
+ bytesOut: number;
216
+ /** Source maps read out of the container this capture. */
217
+ mapsFromContainer: number;
218
+ }
219
+ /** One `source-map-cache` entry in the shape Node writes: the map (its
220
+ * `sourcesContent` dropped), the generated script's line lengths, and
221
+ * the map's own URL. */
222
+ export interface SourceMapEntry {
223
+ url: string | null;
224
+ data: Record<string, unknown>;
225
+ lineLengths: number[];
226
+ }
227
+ /** Finds the source map of a script the dump carries no map for.
228
+ * `null` when there is none. */
229
+ export type SourceMapResolver = (scriptUrl: string) => Promise<SourceMapEntry | null>;
230
+ /** The length of every line of `text`, the way Node computes it for
231
+ * `source-map-cache`: split on `\n` (and U+2028/2029), `\r` kept, the
232
+ * last line included. Lengths in UTF-16 units, which is what V8's byte
233
+ * offsets count in. */
234
+ export declare function lineLengthsOf(text: string): number[];
235
+ /** The last `sourceMappingURL` comment of a script, or `null`. */
236
+ export declare function sourceMappingUrlOf(script: string): string | null;
237
+ /**
238
+ * The source map of `scriptUrl`, read out of the container: the script
239
+ * itself (for its `sourceMappingURL` and its line lengths), then the map
240
+ * — inline as a `data:` URL, a file the comment names, or, with no
241
+ * comment, `<script>.map` next to it. `null` when the script is not a
242
+ * `file://` URL, cannot be read, or the map is not there or not JSON; a
243
+ * map's `sourcesContent` is dropped.
244
+ * One read per script per branch: the caller remembers the outcome.
245
+ */
246
+ export declare function resolveSourceMapFromContainer(ctx: CoverageCaptureContext, scriptUrl: string): Promise<SourceMapEntry | null>;
247
+ /**
248
+ * Give every kept script of a compacted document a `source-map-cache`
249
+ * entry it lacks: a stub when the map shipped earlier on this branch,
250
+ * else the map `resolve` finds (shipped whole, with its hash, and
251
+ * remembered), else nothing — and that outcome is remembered too, so a
252
+ * script with no map is looked up once per branch. Returns how many maps
253
+ * `resolve` supplied.
254
+ */
255
+ export declare function attachSourceMaps(doc: Record<string, unknown>, resolve: SourceMapResolver, shipped?: Map<string, string>, missing?: Set<string>): Promise<number>;
256
+ /**
257
+ * Compact, in place, every `coverage-*.json` in `dir` that no earlier
258
+ * capture shipped, attach the maps the dump lacks through `resolve`
259
+ * ({@link attachSourceMaps}), and mark it shipped. A dump mid-write (not
260
+ * yet valid JSON) is left for the next capture.
215
261
  */
216
- export declare function compactV8Document(doc: Record<string, unknown>, opts?: {
217
- keepSourcesContent?: boolean;
218
- }): Record<string, unknown>;
219
- /** Compact every not-yet-compacted V8 document in `dir`, in place. */
220
- export declare function compactV8Reports(dir: string): Promise<void>;
262
+ export declare function prepareV8Reports(dir: string, resolve?: SourceMapResolver): Promise<PrepareV8Stats>;
221
263
  /**
222
264
  * Coverage for the frontend a service serves. The code runs in the guest
223
265
  * browser — spectest's own process — so no report can be written by the