supercov 0.0.3 → 0.0.4
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/README.md +58 -24
- package/dist/buildCache.d.ts +14 -0
- package/dist/buildCache.d.ts.map +1 -0
- package/dist/buildCache.js +84 -0
- package/dist/buildCache.js.map +1 -0
- package/dist/cli.js +128 -24
- package/dist/cli.js.map +1 -1
- package/dist/evidenceArchive.d.ts +35 -0
- package/dist/evidenceArchive.d.ts.map +1 -0
- package/dist/evidenceArchive.js +101 -0
- package/dist/evidenceArchive.js.map +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +28 -21
- package/dist/query.js.map +1 -1
- package/dist/reporter.d.ts +6 -13
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +21 -82
- package/dist/reporter.js.map +1 -1
- package/dist/runAnalysis.d.ts +19 -0
- package/dist/runAnalysis.d.ts.map +1 -0
- package/dist/runAnalysis.js +112 -0
- package/dist/runAnalysis.js.map +1 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +11 -5
- package/dist/transport.js.map +1 -1
- package/dist/vitePlugin.d.ts +1 -0
- package/dist/vitePlugin.d.ts.map +1 -1
- package/dist/vitePlugin.js +31 -0
- package/dist/vitePlugin.js.map +1 -1
- package/dist/workspace.d.ts +39 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +263 -46
- package/dist/workspace.js.map +1 -1
- package/docs/performance.md +107 -0
- package/docs/workspace-isolation.md +118 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -36,8 +36,8 @@ The coverage engine has seven independent release gates:
|
|
|
36
36
|
- release CI shards the pinned TC39 Test262 corpus across 16 workers, runs the
|
|
37
37
|
official Test262 harness on original and instrumented sources, and rejects
|
|
38
38
|
any scenario that passes originally but fails after transformation; and
|
|
39
|
-
- checked performance budgets cover transform latency,
|
|
40
|
-
runtime probe overhead.
|
|
39
|
+
- checked performance budgets cover transform latency, transactional workspace
|
|
40
|
+
preparation, output expansion, and runtime probe overhead.
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
43
|
npm test
|
|
@@ -51,7 +51,9 @@ computed logical assignments, defaults, `try`/`catch`/`finally`, iterator
|
|
|
51
51
|
closing, switch fallthrough, labeled loops, async functions, and generators.
|
|
52
52
|
The compatibility workflow additionally runs Node 22/24/25, Playwright
|
|
53
53
|
1.55/current, Vite 5/current, Vitest 2/current, Chromium, Firefox, WebKit, and
|
|
54
|
-
modern JavaScript/JSX/TypeScript/TSX syntax fixtures.
|
|
54
|
+
modern JavaScript/JSX/TypeScript/TSX syntax fixtures. Filesystem publication,
|
|
55
|
+
symlink, copy fallback, ENOSPC, failed rename, and forced-termination recovery
|
|
56
|
+
also run on Ubuntu, macOS, and Windows. Test262's module, async,
|
|
55
57
|
raw, parse/resolution-negative, Annex B sloppy-script extension, and explicit
|
|
56
58
|
`Function.prototype.toString`/function-source-coercion tests are intentionally
|
|
57
59
|
excluded from the source-rewrite comparison, with reason counts printed for
|
|
@@ -66,11 +68,14 @@ blocks the trusted-publishing workflow.
|
|
|
66
68
|
|
|
67
69
|
## Agent query workflow
|
|
68
70
|
|
|
69
|
-
Each run is stored locally as
|
|
70
|
-
`.supercov/runs/<run-id
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
Each run is stored locally as two immutable files under
|
|
72
|
+
`.supercov/runs/<run-id>/`: `evidence.raw.gz` and `run.json`. The archive
|
|
73
|
+
contains the exact coverage denominator manifest plus raw per-worker and
|
|
74
|
+
background evidence. Every query reconstructs its requested coverage view
|
|
75
|
+
directly from that archive; no derived report or query cache is retained.
|
|
76
|
+
Loose evidence is removed only after the whole run directory is atomically
|
|
77
|
+
visible. HTML is not generated during a test run; agents should use bounded
|
|
78
|
+
CLI queries instead of loading the complete derived model into context.
|
|
74
79
|
|
|
75
80
|
```sh
|
|
76
81
|
# Orient using only a few lines.
|
|
@@ -102,8 +107,8 @@ Use `--filter passed` for verified coverage from successful attempts of
|
|
|
102
107
|
ultimately passing tests, or `--filter failed` to inspect only execution from
|
|
103
108
|
failed attempts (including failed retries of flaky tests). Reports record
|
|
104
109
|
attempt status and classify each test as passed, failed, flaky, skipped, timed
|
|
105
|
-
out, interrupted, or unknown. The
|
|
106
|
-
|
|
110
|
+
out, interrupted, or unknown. The passed and failed subsets are stored inside
|
|
111
|
+
the same compressed report rather than duplicated into presentation files.
|
|
107
112
|
|
|
108
113
|
The run ID is positional because all coverage queries operate on one immutable
|
|
109
114
|
run. `latest` is a convenience selector for interactive use. Every query
|
|
@@ -120,8 +125,12 @@ CLI:
|
|
|
120
125
|
1. refreshes a stable isolated source namespace under
|
|
121
126
|
`.supercov/cache/instrumented-workspace/<project>/`, links the existing
|
|
122
127
|
dependency tree, and creates all Vite, Vitest, Playwright, and build output
|
|
123
|
-
only there;
|
|
124
|
-
|
|
128
|
+
only there; file data uses copy-on-write reflinks where the filesystem
|
|
129
|
+
supports them, and falls back to copying where it does not; the stable path
|
|
130
|
+
lets VM/container snapshot systems reuse a coverage build without touching
|
|
131
|
+
the application's ordinary build; when the complete source/config/toolchain
|
|
132
|
+
fingerprint is unchanged, the prior instrumented output and manifest are
|
|
133
|
+
carried into the refreshed source snapshot and the build is skipped;
|
|
125
134
|
2. inventories every `app/**/*.ts(x)` and `src/**/*.ts(x)` file for the
|
|
126
135
|
denominator, then instruments modules loaded by Vite without changing the
|
|
127
136
|
project's config; when no build script exists, it instruments only the
|
|
@@ -136,30 +145,55 @@ CLI:
|
|
|
136
145
|
5. attributes every source hit and decision vector to its individual test,
|
|
137
146
|
automatically wraps Playwright actions and assertions, and records the
|
|
138
147
|
action/assertion phase responsible for each correlated hit; then merges
|
|
139
|
-
server and browser evidence into
|
|
140
|
-
6. atomically publishes the evidence/report into `.supercov
|
|
141
|
-
|
|
148
|
+
server and browser evidence into one compressed JSON report; and
|
|
149
|
+
6. atomically publishes the compressed evidence/report into `.supercov/`, then
|
|
150
|
+
removes loose evidence and terminal per-run work state, retaining only the
|
|
151
|
+
immutable run and disposable isolated build namespace. The
|
|
142
152
|
ordinary application build is never read as an input, overwritten, or
|
|
143
153
|
rebuilt afterward.
|
|
144
154
|
|
|
145
155
|
Only `.supercov/` is modified in the user's checkout. A per-project lock
|
|
146
156
|
rejects overlapping runs before either can build. Run state is durably written
|
|
147
|
-
through preparing/building/testing/reporting
|
|
157
|
+
through preparing/building/testing/reporting phases; SIGINT, SIGTERM,
|
|
148
158
|
and SIGHUP are forwarded to the entire child process group. If the process is
|
|
149
159
|
killed without a cleanup opportunity, the next invocation marks the dead PID's
|
|
150
|
-
run abandoned and refreshes the isolated namespace before using it.
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
run abandoned and refreshes the isolated namespace before using it. Cache
|
|
161
|
+
refresh is transactional: a new sibling generation is prepared completely,
|
|
162
|
+
the stable name is switched only at publication, and the prior complete
|
|
163
|
+
generation is retained until that switch succeeds. The next invocation
|
|
164
|
+
discards orphan staging trees or restores the prior generation if a host crash
|
|
165
|
+
landed between the two same-filesystem renames. Report, evidence archive, and
|
|
166
|
+
state writes use sibling-temp files, fsync, and atomic rename; lock acquisition
|
|
167
|
+
uses exclusive creation and fsync. Published `run.json` is the durable terminal
|
|
168
|
+
record, so terminal work state is not retained.
|
|
153
169
|
|
|
154
170
|
Retention is deterministic because UTC run IDs sort chronologically:
|
|
155
171
|
|
|
156
172
|
```sh
|
|
157
|
-
npx supercov
|
|
158
|
-
npx supercov
|
|
173
|
+
npx supercov prune --keep 20
|
|
174
|
+
npx supercov prune --keep 20 --dry-run
|
|
175
|
+
npx supercov clean --keep 20 # also removes the shared build cache
|
|
159
176
|
```
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
Neither operation runs automatically. `prune` removes explicit history beyond
|
|
179
|
+
the requested retention and orphan/terminal transient data while preserving
|
|
180
|
+
the shared cache. `clean` also removes that cache. Both acquire the same lock
|
|
181
|
+
as a coverage run, refuse to race an active run, and never touch files outside
|
|
182
|
+
`.supercov/`.
|
|
183
|
+
|
|
184
|
+
The complete ownership, crash-recovery, symlink, and future copy-free design is
|
|
185
|
+
documented in [Workspace isolation](docs/workspace-isolation.md).
|
|
186
|
+
|
|
187
|
+
Every run prints and stores monotonic phase timings for initialization,
|
|
188
|
+
workspace preparation, adapter setup, the instrumented build, the unchanged
|
|
189
|
+
test command, and report preparation. They are available in
|
|
190
|
+
`.supercov/runs/<run-id>/run.json` and in the JSON form of `supercov runs`.
|
|
191
|
+
These phase timings do not pretend to be end-to-end overhead: that percentage
|
|
192
|
+
requires an explicit comparison with the same command run without Supercov,
|
|
193
|
+
which Supercov never executes automatically because an arbitrary test command
|
|
194
|
+
may have side effects or external cost. See
|
|
195
|
+
[Performance and storage](docs/performance.md) for the comparison methodology,
|
|
196
|
+
strategy trade-offs, and a measured real-suite reference.
|
|
163
197
|
|
|
164
198
|
The automatic adapters currently support standard Playwright suites (ESM and
|
|
165
199
|
CommonJS specs in arbitrary project directories), project-owned Playwright
|
|
@@ -242,7 +276,7 @@ phase travels on browser requests into automatically wrapped Remix loaders,
|
|
|
242
276
|
actions, and the server document renderer. Node async context preserves that
|
|
243
277
|
ID through awaited helpers. An assertion also retains the preceding action ID,
|
|
244
278
|
making chains such as “click -> application lines/decisions -> visible
|
|
245
|
-
assertion” queryable in JSON
|
|
279
|
+
assertion” queryable in JSON.
|
|
246
280
|
|
|
247
281
|
Server evidence is safe when Playwright uses multiple workers against one
|
|
248
282
|
application server. Every routed request carries a run/worker/test/retry scope;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CoverageRunIntegrity } from "./types.ts";
|
|
2
|
+
import type { CoverageProject } from "./project.ts";
|
|
3
|
+
export declare const INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION = 1;
|
|
4
|
+
export interface InstrumentedBuildCache {
|
|
5
|
+
schemaVersion: typeof INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION;
|
|
6
|
+
key: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
artifactPaths: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function instrumentedBuildCacheKey(integrity: CoverageRunIntegrity, project: CoverageProject): string;
|
|
11
|
+
export declare function readInstrumentedBuildCache(workspace: string, key: string): InstrumentedBuildCache | undefined;
|
|
12
|
+
export declare function writeInstrumentedBuildCache(workspace: string, key: string): InstrumentedBuildCache | undefined;
|
|
13
|
+
export declare function buildCacheReusePaths(metadata: InstrumentedBuildCache): string[];
|
|
14
|
+
//# sourceMappingURL=buildCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildCache.d.ts","sourceRoot":"","sources":["../src/buildCache.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,uCAAuC,IAAI,CAAC;AAUzD,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,OAAO,uCAAuC,CAAC;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAgBD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,oBAAoB,EAC/B,OAAO,EAAE,eAAe,GACvB,MAAM,CAeR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,sBAAsB,GAAG,SAAS,CAqBpC;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,sBAAsB,GAAG,SAAS,CAqCpC;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,sBAAsB,GAC/B,MAAM,EAAE,CAEV"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
4
|
+
import { atomicWriteFileSync } from "./atomic.js";
|
|
5
|
+
export const INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION = 1;
|
|
6
|
+
const BUILD_OUTPUT_CANDIDATES = [
|
|
7
|
+
"build",
|
|
8
|
+
"dist",
|
|
9
|
+
".next",
|
|
10
|
+
".nuxt",
|
|
11
|
+
".output",
|
|
12
|
+
];
|
|
13
|
+
function metadataPath(workspace) {
|
|
14
|
+
return resolve(workspace, ".supercov/build-cache.json");
|
|
15
|
+
}
|
|
16
|
+
function safeRelativePath(path) {
|
|
17
|
+
return Boolean(path &&
|
|
18
|
+
!isAbsolute(path) &&
|
|
19
|
+
path !== ".." &&
|
|
20
|
+
!path.startsWith(`..${sep}`) &&
|
|
21
|
+
!path.split(/[\\/]/).includes(".."));
|
|
22
|
+
}
|
|
23
|
+
export function instrumentedBuildCacheKey(integrity, project) {
|
|
24
|
+
return createHash("sha256")
|
|
25
|
+
.update(JSON.stringify({
|
|
26
|
+
schemaVersion: INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION,
|
|
27
|
+
executionFingerprint: integrity.fingerprint.execution,
|
|
28
|
+
adapter: project.buildAdapter,
|
|
29
|
+
command: project.buildCommand,
|
|
30
|
+
environment: project.buildEnvironment,
|
|
31
|
+
node: process.versions.node,
|
|
32
|
+
platform: process.platform,
|
|
33
|
+
architecture: process.arch,
|
|
34
|
+
}))
|
|
35
|
+
.digest("hex");
|
|
36
|
+
}
|
|
37
|
+
export function readInstrumentedBuildCache(workspace, key) {
|
|
38
|
+
let metadata;
|
|
39
|
+
try {
|
|
40
|
+
metadata = JSON.parse(readFileSync(metadataPath(workspace), "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (metadata.schemaVersion !== INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION ||
|
|
46
|
+
metadata.key !== key ||
|
|
47
|
+
!Array.isArray(metadata.artifactPaths) ||
|
|
48
|
+
metadata.artifactPaths.length === 0 ||
|
|
49
|
+
metadata.artifactPaths.some((path) => !safeRelativePath(path) || !existsSync(resolve(workspace, path)))) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return metadata;
|
|
53
|
+
}
|
|
54
|
+
export function writeInstrumentedBuildCache(workspace, key) {
|
|
55
|
+
let declared = {};
|
|
56
|
+
try {
|
|
57
|
+
declared = JSON.parse(readFileSync(resolve(workspace, ".supercov/build-outputs.json"), "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Older/native Vite integrations use the conservative known outputs.
|
|
61
|
+
}
|
|
62
|
+
const existingOutputs = [
|
|
63
|
+
...new Set([
|
|
64
|
+
...BUILD_OUTPUT_CANDIDATES,
|
|
65
|
+
...(declared.paths ?? []).filter(safeRelativePath),
|
|
66
|
+
]),
|
|
67
|
+
].filter((path) => existsSync(resolve(workspace, path)));
|
|
68
|
+
const outputs = existingOutputs.filter((path) => !existingOutputs.some((parent) => parent !== path && path.startsWith(`${parent}/`)));
|
|
69
|
+
const manifest = ".supercov/manifest.json";
|
|
70
|
+
if (outputs.length === 0 || !existsSync(resolve(workspace, manifest)))
|
|
71
|
+
return undefined;
|
|
72
|
+
const metadata = {
|
|
73
|
+
schemaVersion: INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION,
|
|
74
|
+
key,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
artifactPaths: [...outputs, manifest],
|
|
77
|
+
};
|
|
78
|
+
atomicWriteFileSync(metadataPath(workspace), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
79
|
+
return metadata;
|
|
80
|
+
}
|
|
81
|
+
export function buildCacheReusePaths(metadata) {
|
|
82
|
+
return [...metadata.artifactPaths, ".supercov/build-cache.json"];
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=buildCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildCache.js","sourceRoot":"","sources":["../src/buildCache.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { atomicRenameSync, atomicWriteFileSync } from "./atomic.js";
|
|
6
|
-
import {
|
|
7
|
+
import { printCoverageSummary } from "./reporter.js";
|
|
8
|
+
import { analyzeCoverageArchive } from "./runAnalysis.js";
|
|
7
9
|
import { coverageQueryCommands, runQueryCommand } from "./query.js";
|
|
8
10
|
import { discoverCoverageProject } from "./project.js";
|
|
9
11
|
import { createRunIntegrity } from "./integrity.js";
|
|
12
|
+
import { writeEvidenceArchive } from "./evidenceArchive.js";
|
|
13
|
+
import { buildCacheReusePaths, instrumentedBuildCacheKey, readInstrumentedBuildCache, writeInstrumentedBuildCache, } from "./buildCache.js";
|
|
10
14
|
import { instrumentDirectWorkspace } from "./directInstrumenter.js";
|
|
11
|
-
import { acquireProjectLock, cachedWorkspacePath, cleanCoverageStorage, prepareCachedWorkspace, recoverAbandonedRuns, updateRunState, writeRunState, } from "./workspace.js";
|
|
15
|
+
import { acquireProjectLock, cachedWorkspacePath, cleanCoverageStorage, finalizePublishedRunStorage, prepareCachedWorkspace, pruneCoverageStorage, recoverAbandonedRuns, updateRunState, writeRunState, } from "./workspace.js";
|
|
16
|
+
function roundedTimings(timings) {
|
|
17
|
+
return Object.fromEntries(Object.entries(timings).map(([phase, duration]) => [
|
|
18
|
+
phase,
|
|
19
|
+
Math.round(duration * 10) / 10,
|
|
20
|
+
]));
|
|
21
|
+
}
|
|
22
|
+
function formatTimings(timings, totalMs) {
|
|
23
|
+
const rounded = roundedTimings(timings);
|
|
24
|
+
return [
|
|
25
|
+
`initialization=${rounded.initializationMs}ms`,
|
|
26
|
+
`workspace=${rounded.workspacePreparationMs}ms`,
|
|
27
|
+
`setup=${rounded.adapterSetupMs}ms`,
|
|
28
|
+
`build=${rounded.instrumentedBuildMs}ms`,
|
|
29
|
+
`tests=${rounded.testCommandMs}ms`,
|
|
30
|
+
`evidence=${rounded.evidencePublicationMs}ms`,
|
|
31
|
+
`total=${Math.round(totalMs * 10) / 10}ms`,
|
|
32
|
+
].join(" ");
|
|
33
|
+
}
|
|
12
34
|
let activeChild;
|
|
13
35
|
let signalEscalation;
|
|
14
36
|
function terminateChild(signal) {
|
|
@@ -68,7 +90,7 @@ function signalExitCode(signal) {
|
|
|
68
90
|
return 143;
|
|
69
91
|
return 128;
|
|
70
92
|
}
|
|
71
|
-
function
|
|
93
|
+
function parseRetentionOptions(command, args) {
|
|
72
94
|
let keep = 20;
|
|
73
95
|
let dryRun = false;
|
|
74
96
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -82,21 +104,37 @@ function parseCleanOptions(args) {
|
|
|
82
104
|
keep = value;
|
|
83
105
|
}
|
|
84
106
|
else
|
|
85
|
-
throw new Error(`Unknown
|
|
107
|
+
throw new Error(`Unknown ${command} option: ${argument}`);
|
|
86
108
|
}
|
|
87
109
|
return { keep, dryRun };
|
|
88
110
|
}
|
|
89
111
|
function cleanCommand(args) {
|
|
90
|
-
const options =
|
|
112
|
+
const options = parseRetentionOptions("clean", args);
|
|
91
113
|
const result = cleanCoverageStorage(process.cwd(), options);
|
|
92
114
|
console.log(`[supercov] ${options.dryRun ? "would remove" : "removed"} ${result.removedRuns.length} stored run(s), ${result.removedWorkspaces.length} per-run workspace(s), and ${result.removedBuildCache ? "the" : "no"} isolated build cache; keeping ${options.keep} newest run(s)`);
|
|
93
115
|
for (const id of result.removedRuns)
|
|
94
116
|
console.log(id);
|
|
95
117
|
}
|
|
118
|
+
function pruneCommand(args) {
|
|
119
|
+
const options = parseRetentionOptions("prune", args);
|
|
120
|
+
const result = pruneCoverageStorage(process.cwd(), options);
|
|
121
|
+
console.log(`[supercov] ${options.dryRun ? "would remove" : "removed"} ${result.removedRuns.length} stored run(s), ${result.removedWorkspaces.length} terminal/orphan work director${result.removedWorkspaces.length === 1 ? "y" : "ies"}, and ${result.removedEvidence.length} loose evidence director${result.removedEvidence.length === 1 ? "y" : "ies"}; keeping ${options.keep} newest run(s) and preserving the shared cache`);
|
|
122
|
+
for (const id of result.removedRuns)
|
|
123
|
+
console.log(id);
|
|
124
|
+
}
|
|
96
125
|
async function createCoverageRun(command) {
|
|
97
126
|
const root = process.cwd();
|
|
98
127
|
const runId = new Date().toISOString().replace(/[:.]/g, "-");
|
|
99
128
|
const runStartedAt = Date.now();
|
|
129
|
+
const runStartedMonotonic = performance.now();
|
|
130
|
+
const timings = {
|
|
131
|
+
initializationMs: 0,
|
|
132
|
+
workspacePreparationMs: 0,
|
|
133
|
+
adapterSetupMs: 0,
|
|
134
|
+
instrumentedBuildMs: 0,
|
|
135
|
+
testCommandMs: 0,
|
|
136
|
+
evidencePublicationMs: 0,
|
|
137
|
+
};
|
|
100
138
|
const recovered = recoverAbandonedRuns(root);
|
|
101
139
|
if (recovered.length > 0)
|
|
102
140
|
console.error(`[supercov] recovered abandoned run(s): ${recovered.join(", ")}`);
|
|
@@ -105,7 +143,12 @@ async function createCoverageRun(command) {
|
|
|
105
143
|
const packageSource = fileURLToPath(new URL(".", import.meta.url));
|
|
106
144
|
const runIntegrity = createRunIntegrity(root, project, packageSource);
|
|
107
145
|
const workspace = cachedWorkspacePath(root);
|
|
108
|
-
const
|
|
146
|
+
const buildCacheKey = instrumentedBuildCacheKey(runIntegrity, project);
|
|
147
|
+
const reusableBuild = project.buildAdapter === "vite"
|
|
148
|
+
? readInstrumentedBuildCache(workspace, buildCacheKey)
|
|
149
|
+
: undefined;
|
|
150
|
+
const serverEvidenceRoot = resolve(workspace, ".supercov/server-evidence");
|
|
151
|
+
const runStagingDirectory = resolve(root, ".supercov/work", runId, "run-publication");
|
|
109
152
|
const storedRunDirectory = resolve(root, ".supercov/runs", runId);
|
|
110
153
|
const startedAt = new Date(runStartedAt).toISOString();
|
|
111
154
|
writeRunState(root, runId, {
|
|
@@ -138,11 +181,21 @@ async function createCoverageRun(command) {
|
|
|
138
181
|
}
|
|
139
182
|
let buildResult;
|
|
140
183
|
let testResult;
|
|
141
|
-
let
|
|
184
|
+
let publicationFailed = false;
|
|
185
|
+
let runPublished = false;
|
|
186
|
+
let timingsPrinted = false;
|
|
187
|
+
timings.initializationMs = performance.now() - runStartedMonotonic;
|
|
142
188
|
try {
|
|
143
|
-
|
|
189
|
+
let phaseStarted = performance.now();
|
|
190
|
+
const isolatedRoot = prepareCachedWorkspace(root, {
|
|
191
|
+
...(reusableBuild
|
|
192
|
+
? { reusePaths: buildCacheReusePaths(reusableBuild) }
|
|
193
|
+
: {}),
|
|
194
|
+
});
|
|
195
|
+
timings.workspacePreparationMs = performance.now() - phaseStarted;
|
|
144
196
|
if (receivedSignal)
|
|
145
197
|
throw new Error(`Interrupted by ${receivedSignal}`);
|
|
198
|
+
phaseStarted = performance.now();
|
|
146
199
|
const generatedDirectory = resolve(isolatedRoot, ".supercov");
|
|
147
200
|
const evidenceDirectoryRelative = `.supercov/evidence/${runId}`;
|
|
148
201
|
const isolatedEvidenceDirectory = resolve(isolatedRoot, evidenceDirectoryRelative);
|
|
@@ -151,6 +204,7 @@ async function createCoverageRun(command) {
|
|
|
151
204
|
const generatedViteConfig = resolve(generatedDirectory, "vite.config.mjs");
|
|
152
205
|
const generatedVitestConfig = resolve(generatedDirectory, "vitest.config.mjs");
|
|
153
206
|
const manifestPath = resolve(generatedDirectory, "manifest.json");
|
|
207
|
+
const buildOutputMetadataPath = resolve(generatedDirectory, "build-outputs.json");
|
|
154
208
|
const isolatedPlaywrightConfig = project.playwrightConfig
|
|
155
209
|
? resolve(isolatedRoot, relative(root, project.playwrightConfig))
|
|
156
210
|
: undefined;
|
|
@@ -258,7 +312,7 @@ async function createCoverageRun(command) {
|
|
|
258
312
|
` const relocateOutput = output => output ? ({ ...output, dir: output.dir ? relocate(output.dir, 'Rollup output') : output.dir, file: output.file ? relocate(output.file, 'Rollup output') : output.file }) : output;`,
|
|
259
313
|
` const rollupOutput = config.build?.rollupOptions?.output;`,
|
|
260
314
|
` const safe = { ...config, cacheDir: resolve(isolatedRoot, '.supercov/vite-cache'), build: { ...config.build, outDir: relocate(config.build?.outDir ?? 'dist', 'Vite build output'), rollupOptions: { ...config.build?.rollupOptions, output: Array.isArray(rollupOutput) ? rollupOutput.map(relocateOutput) : relocateOutput(rollupOutput) } } };`,
|
|
261
|
-
` return mergeConfig(safe, { plugins: [mcdcVitePlugin(${JSON.stringify({ root: isolatedRoot, sourceRoots: project.sourceRoots, manifestPath })})] });`,
|
|
315
|
+
` return mergeConfig(safe, { plugins: [mcdcVitePlugin(${JSON.stringify({ root: isolatedRoot, sourceRoots: project.sourceRoots, manifestPath, buildOutputMetadataPath })})] });`,
|
|
262
316
|
`}`,
|
|
263
317
|
"",
|
|
264
318
|
].join("\n"));
|
|
@@ -291,6 +345,7 @@ async function createCoverageRun(command) {
|
|
|
291
345
|
SUPERCOV_EXECUTION_FINGERPRINT: runIntegrity.fingerprint.execution,
|
|
292
346
|
SUPERCOV_EXECUTION_LOG: resolve(isolatedEvidenceDirectory, "execution.jsonl"),
|
|
293
347
|
SUPERCOV_RUN_ID: runId,
|
|
348
|
+
SUPERCOV_SERVER_EVIDENCE_ROOT: serverEvidenceRoot,
|
|
294
349
|
SUPERCOV_MANIFEST: manifestPath,
|
|
295
350
|
SUPERCOV_PLAYWRIGHT_MODULE: project.playwrightModule,
|
|
296
351
|
SUPERCOV_PLAYWRIGHT_TEST_EXPORT: project.playwrightTestExport,
|
|
@@ -311,13 +366,19 @@ async function createCoverageRun(command) {
|
|
|
311
366
|
]
|
|
312
367
|
.filter(Boolean)
|
|
313
368
|
.join(" ");
|
|
369
|
+
timings.adapterSetupMs = performance.now() - phaseStarted;
|
|
314
370
|
updateRunState(root, runId, { status: "building" });
|
|
315
371
|
console.error(`[supercov] instrumenting isolated workspace ${isolatedRoot}`);
|
|
316
372
|
if (Object.keys(project.buildEnvironment).length > 0)
|
|
317
373
|
console.error(`[supercov] inferred build mode from command/config: ${Object.entries(project.buildEnvironment)
|
|
318
374
|
.map(([key, value]) => `${key}=${value}`)
|
|
319
375
|
.join(" ")}`);
|
|
320
|
-
|
|
376
|
+
phaseStarted = performance.now();
|
|
377
|
+
if (reusableBuild) {
|
|
378
|
+
console.error(`[supercov] reusing exact-fingerprint instrumented build ${buildCacheKey.slice(0, 12)}`);
|
|
379
|
+
buildResult = { status: 0, signal: null };
|
|
380
|
+
}
|
|
381
|
+
else if (project.buildAdapter === "direct") {
|
|
321
382
|
instrumentDirectWorkspace(isolatedRoot, project.sourceRoots, manifestPath);
|
|
322
383
|
buildResult = { status: 0, signal: null };
|
|
323
384
|
}
|
|
@@ -336,6 +397,12 @@ async function createCoverageRun(command) {
|
|
|
336
397
|
},
|
|
337
398
|
});
|
|
338
399
|
}
|
|
400
|
+
if (buildResult.status === 0 &&
|
|
401
|
+
project.buildAdapter === "vite" &&
|
|
402
|
+
!reusableBuild) {
|
|
403
|
+
writeInstrumentedBuildCache(isolatedRoot, buildCacheKey);
|
|
404
|
+
}
|
|
405
|
+
timings.instrumentedBuildMs = performance.now() - phaseStarted;
|
|
339
406
|
if (receivedSignal)
|
|
340
407
|
throw new Error(`Interrupted by ${receivedSignal}`);
|
|
341
408
|
if (buildResult.error)
|
|
@@ -343,6 +410,7 @@ async function createCoverageRun(command) {
|
|
|
343
410
|
if (buildResult.status === 0) {
|
|
344
411
|
updateRunState(root, runId, { status: "testing" });
|
|
345
412
|
console.error(`[supercov] running in isolated workspace: ${command.join(" ")}`);
|
|
413
|
+
phaseStarted = performance.now();
|
|
346
414
|
testResult = await runChild(command[0], command.slice(1), {
|
|
347
415
|
cwd: isolatedRoot,
|
|
348
416
|
env: {
|
|
@@ -351,38 +419,65 @@ async function createCoverageRun(command) {
|
|
|
351
419
|
SUPERCOV_CJS_INTERCEPT: "1",
|
|
352
420
|
},
|
|
353
421
|
});
|
|
422
|
+
timings.testCommandMs = performance.now() - phaseStarted;
|
|
354
423
|
if (receivedSignal)
|
|
355
424
|
throw new Error(`Interrupted by ${receivedSignal}`);
|
|
356
425
|
if (testResult.error)
|
|
357
426
|
throw testResult.error;
|
|
358
|
-
updateRunState(root, runId, { status: "
|
|
427
|
+
updateRunState(root, runId, { status: "publishing" });
|
|
428
|
+
phaseStarted = performance.now();
|
|
359
429
|
rmSync(persistedEvidenceDirectory, { recursive: true, force: true });
|
|
360
430
|
if (existsSync(isolatedEvidenceDirectory))
|
|
361
431
|
atomicRenameSync(isolatedEvidenceDirectory, persistedEvidenceDirectory);
|
|
362
432
|
try {
|
|
363
|
-
rmSync(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
433
|
+
rmSync(runStagingDirectory, { recursive: true, force: true });
|
|
434
|
+
const evidenceArchivePath = resolve(runStagingDirectory, "evidence.raw.gz");
|
|
435
|
+
const rawEvidence = writeEvidenceArchive([
|
|
436
|
+
{ file: manifestPath, path: "manifest.json" },
|
|
437
|
+
{ directory: persistedEvidenceDirectory },
|
|
438
|
+
{
|
|
439
|
+
directory: resolve(serverEvidenceRoot, runId),
|
|
440
|
+
prefix: "server",
|
|
441
|
+
},
|
|
442
|
+
], evidenceArchivePath);
|
|
443
|
+
printCoverageSummary(analyzeCoverageArchive(evidenceArchivePath, {
|
|
444
|
+
runId,
|
|
445
|
+
testExitCode: testResult.status,
|
|
446
|
+
integrity: runIntegrity,
|
|
447
|
+
generatedAt: startedAt,
|
|
448
|
+
}));
|
|
449
|
+
timings.evidencePublicationMs = performance.now() - phaseStarted;
|
|
450
|
+
atomicWriteFileSync(resolve(runStagingDirectory, "run.json"), `${JSON.stringify({
|
|
369
451
|
id: runId,
|
|
370
452
|
startedAt,
|
|
371
453
|
durationMs: Date.now() - runStartedAt,
|
|
372
454
|
command,
|
|
373
455
|
testExitCode: testResult.status,
|
|
374
456
|
integrity: runIntegrity,
|
|
457
|
+
rawEvidence,
|
|
375
458
|
isolatedBuild: true,
|
|
459
|
+
instrumentedBuildCache: {
|
|
460
|
+
key: buildCacheKey,
|
|
461
|
+
reused: Boolean(reusableBuild),
|
|
462
|
+
},
|
|
463
|
+
timings: roundedTimings(timings),
|
|
376
464
|
}, null, 2)}\n`);
|
|
377
|
-
atomicRenameSync(
|
|
465
|
+
atomicRenameSync(runStagingDirectory, storedRunDirectory);
|
|
466
|
+
runPublished = true;
|
|
467
|
+
console.log(`[coverage] evidence: ${resolve(storedRunDirectory, "evidence.raw.gz")}`);
|
|
378
468
|
}
|
|
379
469
|
catch (error) {
|
|
380
|
-
|
|
381
|
-
|
|
470
|
+
timings.evidencePublicationMs = performance.now() - phaseStarted;
|
|
471
|
+
publicationFailed = true;
|
|
472
|
+
console.error("[supercov] failed to publish coverage evidence", error);
|
|
382
473
|
}
|
|
383
474
|
}
|
|
384
|
-
const resultCode = exitCode(buildResult) || exitCode(testResult) || (
|
|
475
|
+
const resultCode = exitCode(buildResult) || exitCode(testResult) || (publicationFailed ? 1 : 0);
|
|
385
476
|
updateRunState(root, runId, { status: resultCode === 0 ? "complete" : "failed" });
|
|
477
|
+
if (runPublished && !finalizePublishedRunStorage(root, runId))
|
|
478
|
+
throw new Error(`Published run ${runId} is missing a durable artifact`);
|
|
479
|
+
console.error(`[supercov] timings ${formatTimings(timings, performance.now() - runStartedMonotonic)}`);
|
|
480
|
+
timingsPrinted = true;
|
|
386
481
|
return resultCode;
|
|
387
482
|
}
|
|
388
483
|
catch (error) {
|
|
@@ -403,6 +498,8 @@ async function createCoverageRun(command) {
|
|
|
403
498
|
return receivedSignal ? signalExitCode(receivedSignal) : 1;
|
|
404
499
|
}
|
|
405
500
|
finally {
|
|
501
|
+
if (!timingsPrinted)
|
|
502
|
+
console.error(`[supercov] timings ${formatTimings(timings, performance.now() - runStartedMonotonic)}`);
|
|
406
503
|
if (signalEscalation) {
|
|
407
504
|
clearTimeout(signalEscalation);
|
|
408
505
|
signalEscalation = undefined;
|
|
@@ -410,7 +507,11 @@ async function createCoverageRun(command) {
|
|
|
410
507
|
for (const [signal, handler] of signalHandlers)
|
|
411
508
|
process.removeListener(signal, handler);
|
|
412
509
|
try {
|
|
413
|
-
rmSync(
|
|
510
|
+
rmSync(runStagingDirectory, { recursive: true, force: true });
|
|
511
|
+
rmSync(resolve(serverEvidenceRoot, runId), {
|
|
512
|
+
recursive: true,
|
|
513
|
+
force: true,
|
|
514
|
+
});
|
|
414
515
|
// The stable isolated namespace is a deliberate build/snapshot cache.
|
|
415
516
|
// It never overlaps the user's ordinary build and `supercov clean`
|
|
416
517
|
// removes it deterministically.
|
|
@@ -426,9 +527,12 @@ const rawArgs = commandArgs[0] === "--help" || commandArgs[0] === "-h"
|
|
|
426
527
|
? ["help", ...commandArgs.slice(1)]
|
|
427
528
|
: commandArgs;
|
|
428
529
|
const queryCommand = rawArgs[0];
|
|
429
|
-
if (queryCommand === "clean") {
|
|
530
|
+
if (queryCommand === "clean" || queryCommand === "prune") {
|
|
430
531
|
try {
|
|
431
|
-
|
|
532
|
+
if (queryCommand === "clean")
|
|
533
|
+
cleanCommand(rawArgs.slice(1));
|
|
534
|
+
else
|
|
535
|
+
pruneCommand(rawArgs.slice(1));
|
|
432
536
|
}
|
|
433
537
|
catch (error) {
|
|
434
538
|
console.error(`[supercov] ${error instanceof Error ? error.message : String(error)}`);
|