@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/dist/coverage.d.ts
CHANGED
|
@@ -8,11 +8,6 @@ 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
|
-
/** 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";
|
|
16
11
|
/** What `configure` learns about the service it rewrites. */
|
|
17
12
|
export interface CoverageConfigureInfo {
|
|
18
13
|
/** The services-map key. */
|
|
@@ -53,9 +48,6 @@ export interface CoverageCaptureContext {
|
|
|
53
48
|
}>;
|
|
54
49
|
/** Write one report file (name relative to the directory). */
|
|
55
50
|
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>;
|
|
59
51
|
/** Aborts when the per-service capture budget runs out. */
|
|
60
52
|
signal: AbortSignal;
|
|
61
53
|
}
|
|
@@ -109,55 +101,89 @@ export declare function coverageAdapters(cov: ServiceCoverage | undefined): read
|
|
|
109
101
|
export declare function applyCoverageAdapters<S extends ServiceConfig>(key: string, svc: S): S;
|
|
110
102
|
/** `env` with `value` appended to `name` (space-separated), or set. */
|
|
111
103
|
export declare function appendEnvFlag(env: Readonly<Record<string, string>> | undefined, name: string, value: string): Record<string, string>;
|
|
104
|
+
/** Env var the hook reads: the coverage directory inside the container. */
|
|
105
|
+
export declare const NODE_COVERAGE_DIR_ENV = "SPECTEST_COVERAGE_DIR";
|
|
106
|
+
/** Subdirectory of the coverage dir where the hook writes one record per
|
|
107
|
+
* script (`scripts/<id>.json`, {@link NODE_COVERAGE_HOOK}). Shipped
|
|
108
|
+
* once, like a dump. */
|
|
109
|
+
export declare const NODE_COVERAGE_SCRIPTS_SUBDIR = "scripts";
|
|
110
|
+
/** Hidden subdirectory holding one empty marker per script record ever
|
|
111
|
+
* written on this branch. The hook's "written already?" check, which
|
|
112
|
+
* must survive the harness shipping and removing the record; hidden,
|
|
113
|
+
* so the harness never reads it. */
|
|
114
|
+
export declare const NODE_COVERAGE_MARKERS_SUBDIR = ".spectest/scripts";
|
|
115
|
+
/** Name prefix of the dumps the hook writes: `node-<pid>-<thread>-<ms>-<n>.json`. */
|
|
116
|
+
export declare const NODE_COVERAGE_DUMP_PREFIX = "node-";
|
|
112
117
|
/**
|
|
113
|
-
* The hook `node()` mounts
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
118
|
+
* The hook `node()` mounts at {@link NODE_COVERAGE_HOOK_PATH} and
|
|
119
|
+
* `--require`s into every node process of the service. **It is the
|
|
120
|
+
* collector**: nothing outside the process reads, converts or looks
|
|
121
|
+
* anything up. Runs in every thread (a worker thread inherits
|
|
122
|
+
* `NODE_OPTIONS`); reads one env var, {@link NODE_COVERAGE_DIR_ENV}; uses
|
|
123
|
+
* node built-ins only, so it runs in any image that runs node.
|
|
124
|
+
*
|
|
125
|
+
* What it does:
|
|
126
|
+
*
|
|
127
|
+
* - Starts V8 precise (block, counted) coverage through the in-process
|
|
128
|
+
* inspector session. `NODE_V8_COVERAGE` is **not** set: Node would then
|
|
129
|
+
* write everything the process loaded at every exit — `node:`
|
|
130
|
+
* internals, every `node_modules` file, and the whole source map of
|
|
131
|
+
* every script that carries a `sourceMappingURL` comment, with or
|
|
132
|
+
* without `--enable-source-maps`. For a bundled CLI that was 12 MB per
|
|
133
|
+
* invocation, inside the test's own time, parsed again at capture.
|
|
134
|
+
* - **Takes** on request — the main thread binds a Unix socket
|
|
135
|
+
* `.ctl-<pid>` in the coverage directory and takes on `dump\n`; every
|
|
136
|
+
* process binds its own (the first node process is often a wrapper,
|
|
137
|
+
* `pnpm exec`, `tsx`, `npm run`) — and in every thread's `exit`
|
|
138
|
+
* handler, which is how a short-lived CLI and a worker thread report.
|
|
139
|
+
* The in-process session answers synchronously, so the exit handler
|
|
140
|
+
* has the result before the process is gone. A take resets V8's
|
|
141
|
+
* counters: every dump is the delta since the previous take. No
|
|
142
|
+
* signal is used: signals are claimed by frameworks (SIGUSR2 stops a
|
|
143
|
+
* Temporal worker, restarts nodemon), a socket is nobody's.
|
|
144
|
+
* - Writes a **dump** `node-<pid>-<thread>-<ms>-<n>.json`: the app's own
|
|
145
|
+
* scripts (`file://`, outside `node_modules` and `.cache`, not the
|
|
146
|
+
* hook) in which some range count is above zero, and of each only the
|
|
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, …], …] }] }`.
|
|
153
|
+
* - Writes, **once per script per branch**, a **script record**
|
|
154
|
+
* `scripts/<id>.json` (`id` = sha256 of the URL): the length of every
|
|
155
|
+
* line of the source V8 compiled (`Debugger.getScriptSource`, so a
|
|
156
|
+
* transpiled module — tsx, ts-node, a bundler's register hook — is
|
|
157
|
+
* described by the text V8 ran, not the file on disk), every function
|
|
158
|
+
* with its extent (the skeleton: what a dump's "ran" functions are a
|
|
159
|
+
* subset of, so a never-run function is known to exist), and the
|
|
160
|
+
* source map: the inline `data:` map in the text, the file the last
|
|
161
|
+
* `sourceMappingURL` comment names, or `<script>.map` next to the
|
|
162
|
+
* script; `sourcesContent` dropped (the sources are the repository).
|
|
163
|
+
* "Once per branch" is a marker file under `.spectest/scripts/`: the
|
|
164
|
+
* directory is shared by every process of the service and forks with
|
|
165
|
+
* the environment, so a record written at bring-up or by an ancestor
|
|
166
|
+
* is never written again, whichever process comes next.
|
|
167
|
+
*
|
|
168
|
+
* The harness ships new dumps and new records as they are. It parses
|
|
169
|
+
* nothing and touches no container; the control plane derives lines
|
|
170
|
+
* from the stored documents (`SUBSET_RUNS.md`).
|
|
130
171
|
*/
|
|
131
172
|
export declare const NODE_COVERAGE_HOOK: string;
|
|
132
173
|
/**
|
|
133
174
|
* Coverage for a Node service — a long-lived server, or a container whose
|
|
134
175
|
* node processes are short-lived CLIs run by `ctx.exec`. Sets
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* without `--enable-source-maps`), else from `<script>.map` next to the
|
|
147
|
-
* script, read out of the container once
|
|
148
|
-
* ({@link resolveSourceMapsFromContainer}). 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.
|
|
176
|
+
* {@link NODE_COVERAGE_DIR_ENV} to the coverage directory and
|
|
177
|
+
* `--require`s {@link NODE_COVERAGE_HOOK}, which collects everything in
|
|
178
|
+
* the process. Nothing for the app to write, and nothing the harness
|
|
179
|
+
* reads out of the container: at capture it asks every live process for
|
|
180
|
+
* a take over its socket and lists what appeared in the directory —
|
|
181
|
+
* dumps and script records — for the harness to ship as they are. A
|
|
182
|
+
* dump is a delta by construction (V8 resets its counters at every
|
|
183
|
+
* take), so the report after a test is that test's own execution and
|
|
184
|
+
* the boot dump lands in the bring-up capture alone. Each capture ships
|
|
185
|
+
* only its own files: what an earlier capture on this branch shipped is
|
|
186
|
+
* removed first (module memory, forks with the environment).
|
|
161
187
|
*/
|
|
162
188
|
export declare function node(): CoverageAdapter;
|
|
163
189
|
/**
|
|
@@ -169,113 +195,24 @@ export declare function node(): CoverageAdapter;
|
|
|
169
195
|
export declare function dumpAllNodeProcesses(dir: string, signal: AbortSignal): Promise<number>;
|
|
170
196
|
/** Forget the branch memory (tests). */
|
|
171
197
|
export declare function resetNodeCoverageMemory(): void;
|
|
172
|
-
/** Remove from `dir` the
|
|
173
|
-
export declare function
|
|
174
|
-
/**
|
|
175
|
-
|
|
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;
|
|
192
|
-
/**
|
|
193
|
-
* Compact one V8 coverage document to what the derivation needs. What
|
|
194
|
-
* Node writes is everything the process loaded: `node:` internals, every
|
|
195
|
-
* `node_modules` file, and — under `--enable-source-maps` — a
|
|
196
|
-
* `source-map-cache` with each file's full map **and its sources**,
|
|
197
|
-
* repeated in every dump. Measured on a real project: a 12–20 MiB dump
|
|
198
|
-
* per capture, of which the app's own coverage was under 0.5 MiB, and a
|
|
199
|
-
* suite that hit the 64 MiB cap on its third test. Kept: `file://`
|
|
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 {
|
|
198
|
+
/** Remove from `dir` the files an earlier capture shipped. */
|
|
199
|
+
export declare function removeShippedNodeReports(dir: string): Promise<void>;
|
|
200
|
+
/** What {@link collectNodeReports} found, for the capture's log line. */
|
|
201
|
+
export interface NodeReportStats {
|
|
212
202
|
dumps: number;
|
|
203
|
+
dumpBytes: number;
|
|
213
204
|
scripts: number;
|
|
214
|
-
|
|
215
|
-
bytesOut: number;
|
|
216
|
-
/** Source maps read out of the container this capture. */
|
|
217
|
-
mapsFromContainer: number;
|
|
205
|
+
scriptBytes: number;
|
|
218
206
|
}
|
|
219
|
-
/**
|
|
220
|
-
|
|
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 maps of the scripts a dump carries no map for, in
|
|
228
|
-
* one go. A script absent from the result has none. */
|
|
229
|
-
export type SourceMapResolver = (scriptUrls: string[]) => Promise<Map<string, SourceMapEntry>>;
|
|
230
|
-
/**
|
|
231
|
-
* The probe {@link resolveSourceMapsFromContainer} runs in the container:
|
|
232
|
-
* one `sh` loop over every script path, printing `<path>\t<ref>` — the
|
|
233
|
-
* last `sourceMappingURL` value in the file, else `<path>.map` when that
|
|
234
|
-
* file exists, else `-`; `!` for a script that is not there. One exec per
|
|
235
|
-
* capture per service, whatever the number of scripts: a Next.js server
|
|
236
|
-
* loads hundreds of chunk scripts with no map, and a `docker cp` per
|
|
237
|
-
* script (the first cut) took a capture past its budget on a busy host.
|
|
238
|
-
*/
|
|
239
|
-
export declare function sourceMapProbeCommand(scriptPaths: string[]): string;
|
|
240
|
-
/** The length of every line of `text`, the way Node computes it for
|
|
241
|
-
* `source-map-cache`: split on `\n` (and U+2028/2029), `\r` kept, the
|
|
242
|
-
* last line included. Lengths in UTF-16 units, which is what V8's byte
|
|
243
|
-
* offsets count in. */
|
|
244
|
-
export declare function lineLengthsOf(text: string): number[];
|
|
245
|
-
/**
|
|
246
|
-
* The source maps of `scriptUrls`, read out of the container. One probe
|
|
247
|
-
* exec ({@link sourceMapProbeCommand}) finds, per script, the map a
|
|
248
|
-
* `sourceMappingURL` comment names or `<script>.map` next to it; only a
|
|
249
|
-
* script that has one is then read (`docker cp`, the script for its line
|
|
250
|
-
* lengths and the map file, or the inline `data:` map decoded). A map's
|
|
251
|
-
* `sourcesContent` is dropped. Scripts that are not `file://` URLs, have
|
|
252
|
-
* no map, or whose map is not there or not JSON are absent from the
|
|
253
|
-
* result. The caller remembers every outcome, so this runs once per
|
|
254
|
-
* script per branch.
|
|
255
|
-
*
|
|
256
|
-
* With no comment the map next to the script is the shape to build a
|
|
257
|
-
* short-lived bundled process in: Node caches a map it finds through a
|
|
258
|
-
* comment into every dump it writes under NODE_V8_COVERAGE, flag or no
|
|
259
|
-
* flag, and a bundled CLI's map is tens of MB per exit; a map it does not
|
|
260
|
-
* find costs nothing there.
|
|
261
|
-
*/
|
|
262
|
-
export declare function resolveSourceMapsFromContainer(ctx: CoverageCaptureContext, scriptUrls: string[]): Promise<Map<string, SourceMapEntry>>;
|
|
263
|
-
/**
|
|
264
|
-
* Give every kept script of a compacted document a `source-map-cache`
|
|
265
|
-
* entry it lacks: a stub when the map shipped earlier on this branch,
|
|
266
|
-
* else the map `resolve` finds (asked once for all such scripts; shipped
|
|
267
|
-
* whole, with its hash, and remembered), else nothing — and that outcome
|
|
268
|
-
* is remembered too, so a script with no map is looked up once per
|
|
269
|
-
* branch. Returns how many maps `resolve` supplied.
|
|
270
|
-
*/
|
|
271
|
-
export declare function attachSourceMaps(doc: Record<string, unknown>, resolve: SourceMapResolver, shipped?: Map<string, string>, missing?: Set<string>): Promise<number>;
|
|
207
|
+
/** True for a dump the hook wrote (`node-*.json`). */
|
|
208
|
+
export declare function isNodeDumpName(name: string): boolean;
|
|
272
209
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
210
|
+
* Mark shipped every dump at the root of `dir` and every script record
|
|
211
|
+
* under `scripts/` that no earlier capture shipped, and count them.
|
|
212
|
+
* Nothing is read: the hook wrote each file whole (write + rename), and
|
|
213
|
+
* the harness ships them as they are.
|
|
277
214
|
*/
|
|
278
|
-
export declare function
|
|
215
|
+
export declare function collectNodeReports(dir: string): Promise<NodeReportStats>;
|
|
279
216
|
/**
|
|
280
217
|
* Coverage for the frontend a service serves. The code runs in the guest
|
|
281
218
|
* browser — spectest's own process — so no report can be written by the
|