supercov 0.0.28 → 0.0.30
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 +1 -1
- package/docs/coverage-model.md +11 -0
- package/docs/troubleshooting.md +12 -0
- package/docs/workspace-isolation.md +13 -2
- package/package.json +7 -7
- package/runtime/javascript/launchSupervisor.js +40 -8
- package/runtime/javascript/playwright.js +18 -3
- package/runtime/javascript/runnerEvidence.js +19 -2
- package/runtime/javascript/runtime.js +29 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ Collections accept `--limit` and `--offset` and print a copyable next-page comma
|
|
|
154
154
|
|
|
155
155
|
## Local, private, and zero-edit
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
Everything Supercov writes lives under one hidden `.supercov/` directory: run evidence in `.supercov/runs/<run-id>/` and the isolated build cache in `.supercov/workspaces/`. It ignores itself in Git, so there is nothing to add to your `.gitignore`.
|
|
158
158
|
|
|
159
159
|
The Supercov CLI does not contact a Supercov service during a coverage run. Package tools such as `npx` may contact the npm registry to download Supercov when it is not already cached.
|
|
160
160
|
|
package/docs/coverage-model.md
CHANGED
|
@@ -124,5 +124,16 @@ When first-party source lives in unusual directories, declare it explicitly:
|
|
|
124
124
|
SUPERCOV_SOURCE_ROOTS=src,app npx supercov -- npm test
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
Supercov never treats these as project source, so they are neither measured
|
|
128
|
+
nor reported as limitations: another checkout nested in the tree (a directory
|
|
129
|
+
with its own `.git`, such as an agent worktree or a vendored clone), hidden
|
|
130
|
+
directories at the project root (`.shopify/`, `.vercel/`, `.idea/`), directories
|
|
131
|
+
named `generated`, and hashed bundler output inside `assets/`, `static/`, or
|
|
132
|
+
`public/`. Packages the root manifest declares in `workspaces` (or
|
|
133
|
+
`pnpm-workspace.yaml`) are discovered wherever they live; a declared package
|
|
134
|
+
without a conventional source directory is measured as a whole. Functions passed
|
|
135
|
+
to compile-time style macros (`stylex.create(...)`) are left as written because
|
|
136
|
+
the bundler consumes them at build time; nothing about them runs.
|
|
137
|
+
|
|
127
138
|
Choose roots that describe code the repository owns. Do not include dependencies
|
|
128
139
|
or generated output merely to make a warning disappear.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -141,3 +141,15 @@ with:
|
|
|
141
141
|
|
|
142
142
|
Do not include secrets, private source, or raw evidence from a repository you
|
|
143
143
|
cannot share.
|
|
144
|
+
|
|
145
|
+
## "malformed evidence record(s) ... were excluded"
|
|
146
|
+
|
|
147
|
+
Application processes append coverage evidence themselves. When two of them
|
|
148
|
+
share one file — pool runners that restore several VMs from a single snapshot
|
|
149
|
+
run clones of the same server process, with the same process id — an append
|
|
150
|
+
can tear another's line. Supercov skips the unreadable line, counts it under
|
|
151
|
+
`CORRUPT_EVIDENCE_RECORDS`, and treats it as a blocking limitation so the run
|
|
152
|
+
never looks complete. Since 0.0.29 the background and execution-trace writers also detect a
|
|
153
|
+
clone sharing their file and move to a fresh one, so this should be rare; if it
|
|
154
|
+
persists, check whether something outside Supercov appends to
|
|
155
|
+
`.supercov/…/server/background/`.
|
|
@@ -32,7 +32,9 @@ synced back to the project after the run, so `supercov -- npm test -- -u`
|
|
|
32
32
|
updates snapshots in the repository exactly as `npm test -- -u` would. Two
|
|
33
33
|
exceptions are reported instead of applied: changes the command makes to
|
|
34
34
|
instrumented source files (the instrumented copies must never overwrite your
|
|
35
|
-
sources) and deletions (never propagated automatically).
|
|
35
|
+
sources) and deletions (never propagated automatically). Changes inside any
|
|
36
|
+
`node_modules` directory are neither applied nor reported: dependency trees are
|
|
37
|
+
not command outputs.
|
|
36
38
|
|
|
37
39
|
## Files Supercov creates
|
|
38
40
|
|
|
@@ -41,7 +43,7 @@ sources) and deletions (never propagated automatically).
|
|
|
41
43
|
| `.supercov/runs/<run-id>/` | Completed immutable runs |
|
|
42
44
|
| `.supercov/work/` | Temporary state while a run is being prepared |
|
|
43
45
|
| `.supercov/locks/` | Prevents two operations from racing |
|
|
44
|
-
| `.supercov
|
|
46
|
+
| `.supercov/workspaces/` | Isolated source mirror and reusable instrumented build cache (safe to delete) |
|
|
45
47
|
|
|
46
48
|
Managed directories include Git ignore rules so run evidence and instrumented
|
|
47
49
|
builds do not become ordinary repository changes.
|
|
@@ -88,6 +90,15 @@ When a suite launches a container or VM from a mounted workspace, Supercov uses
|
|
|
88
90
|
the isolated workspace as the source presented to that environment. The runtime
|
|
89
91
|
must be able to cross the launch boundary and return evidence.
|
|
90
92
|
|
|
93
|
+
Dependencies stay out of the instrumented copy. The root `node_modules` is
|
|
94
|
+
linked entry by entry to the project's own, so an environment that mounts the
|
|
95
|
+
workspace must bring its own root dependencies (the supported launchers do).
|
|
96
|
+
Nested `node_modules` inside packages and extensions are materialised as real
|
|
97
|
+
directories: cloned copy-on-write where the filesystem allows it (APFS), and
|
|
98
|
+
hard-linked file by file elsewhere on Unix, so they resolve inside the mount
|
|
99
|
+
either way. Only when neither is possible, such as a dependency tree on another
|
|
100
|
+
volume, are they linked entry by entry like the root.
|
|
101
|
+
|
|
91
102
|
If a remote executor hides the launch or mount boundary, Supercov reports the
|
|
92
103
|
limitation instead of claiming unseen code was measured. See
|
|
93
104
|
[Supported suites](supported-suites.md) for the current boundary.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "Zero-edit, runner-aware coverage completeness for JavaScript test suites",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
"prepublishOnly": "npm run release:check"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@supercov/cli-darwin-arm64": "0.0.
|
|
72
|
-
"@supercov/cli-darwin-x64": "0.0.
|
|
73
|
-
"@supercov/cli-linux-arm64-gnu": "0.0.
|
|
74
|
-
"@supercov/cli-linux-arm64-musl": "0.0.
|
|
75
|
-
"@supercov/cli-linux-x64-gnu": "0.0.
|
|
76
|
-
"@supercov/cli-linux-x64-musl": "0.0.
|
|
71
|
+
"@supercov/cli-darwin-arm64": "0.0.30",
|
|
72
|
+
"@supercov/cli-darwin-x64": "0.0.30",
|
|
73
|
+
"@supercov/cli-linux-arm64-gnu": "0.0.30",
|
|
74
|
+
"@supercov/cli-linux-arm64-musl": "0.0.30",
|
|
75
|
+
"@supercov/cli-linux-x64-gnu": "0.0.30",
|
|
76
|
+
"@supercov/cli-linux-x64-musl": "0.0.30"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"@playwright/test": ">=1.55.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import childProcess from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { appendFileSync, mkdirSync, statSync } from "node:fs";
|
|
4
4
|
import Module, { syncBuiltinESMExports } from "node:module";
|
|
5
5
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -30,9 +30,26 @@ function isClassConstructor(value) {
|
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// A pid is not an identity: pool VMs restored from one snapshot run clones
|
|
34
|
+
// of this very process with the same pid, and their appends to one file over
|
|
35
|
+
// a shared mount tear each other's lines. The log name carries a token, and
|
|
36
|
+
// an append that finds the file a different size than this writer left it
|
|
37
|
+
// has met a clone and rotates to a fresh file. The token mixes time in as
|
|
38
|
+
// well as randomness because clones may also share their entropy state.
|
|
39
|
+
function writerToken() {
|
|
40
|
+
let random = "";
|
|
41
|
+
try {
|
|
42
|
+
random = randomBytes(3).toString("hex");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
random = Math.floor(Math.random() * 16777215).toString(16);
|
|
46
|
+
}
|
|
47
|
+
return `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
|
|
48
|
+
}
|
|
49
|
+
let executionLog = { token: writerToken(), path: undefined, size: 0 };
|
|
50
|
+
function executionLogPath(path, token = executionLog.token) {
|
|
34
51
|
const shard = (process.env["SUPERCOV_EXECUTION_LOG_SHARD"] ?? "host").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
35
|
-
const suffix = `.${shard}.${process.pid}.jsonl`;
|
|
52
|
+
const suffix = `.${shard}.${process.pid}-${token}.jsonl`;
|
|
36
53
|
return path.endsWith(".jsonl")
|
|
37
54
|
? `${path.slice(0, -".jsonl".length)}${suffix}`
|
|
38
55
|
: `${path}${suffix}`;
|
|
@@ -41,15 +58,30 @@ function record(value) {
|
|
|
41
58
|
const configuredPath = process.env["SUPERCOV_EXECUTION_LOG"];
|
|
42
59
|
if (!configuredPath)
|
|
43
60
|
return;
|
|
44
|
-
const path = executionLogPath(configuredPath);
|
|
45
61
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
let path = executionLogPath(configuredPath);
|
|
63
|
+
if (executionLog.path === path) {
|
|
64
|
+
let currentSize = 0;
|
|
65
|
+
try {
|
|
66
|
+
currentSize = statSync(path).size;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
currentSize = 0;
|
|
70
|
+
}
|
|
71
|
+
if (currentSize !== executionLog.size) {
|
|
72
|
+
executionLog = { token: writerToken(), path: undefined, size: 0 };
|
|
73
|
+
path = executionLogPath(configuredPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const line = `${JSON.stringify({
|
|
48
77
|
at: new Date().toISOString(),
|
|
49
78
|
pid: process.pid,
|
|
50
79
|
ppid: process.ppid,
|
|
51
80
|
...value,
|
|
52
|
-
})}\n
|
|
81
|
+
})}\n`;
|
|
82
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
+
appendFileSync(path, line);
|
|
84
|
+
executionLog = { token: executionLog.token, path, size: executionLog.size + Buffer.byteLength(line) };
|
|
53
85
|
}
|
|
54
86
|
catch {
|
|
55
87
|
// Process tracing is diagnostic and must never change test behavior.
|
|
@@ -6,7 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
9
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import childProcess from "node:child_process";
|
|
11
11
|
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
12
12
|
import http from "node:http";
|
|
@@ -33,7 +33,22 @@ const adapter = (targetModule === "@playwright/test"
|
|
|
33
33
|
const base = (adapter[targetTestExport] ?? adapter.test);
|
|
34
34
|
const baseExpect = adapter.expect;
|
|
35
35
|
const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
|
|
36
|
-
|
|
36
|
+
// A pid is not an identity. Pool runners restore several VMs from one
|
|
37
|
+
// snapshot and re-execute tests in fresh workers that all carry the same
|
|
38
|
+
// pid; without a per-process token their attempts hash to one identity and
|
|
39
|
+
// their journals land in one file. The token mixes time with randomness
|
|
40
|
+
// because restored clones may also share entropy state.
|
|
41
|
+
const processInstanceToken = (() => {
|
|
42
|
+
let random = "";
|
|
43
|
+
try {
|
|
44
|
+
random = randomBytes(3).toString("hex");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
random = Math.floor(Math.random() * 16777215).toString(16);
|
|
48
|
+
}
|
|
49
|
+
return `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
|
|
50
|
+
})();
|
|
51
|
+
const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}-${processInstanceToken}`)
|
|
37
52
|
.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
38
53
|
const GENERATED_RUN_ID = "__SUPERCOV_RUN_ID__";
|
|
39
54
|
const PHASE_STORAGE_KEY = "__supercov_phase";
|
|
@@ -755,7 +770,7 @@ function currentRunId() {
|
|
|
755
770
|
}
|
|
756
771
|
function executionScope(testInfo) {
|
|
757
772
|
const runId = currentRunId();
|
|
758
|
-
const workerId = `pid-${process.pid}-worker-${testInfo.workerIndex}`;
|
|
773
|
+
const workerId = `pid-${process.pid}-${processInstanceToken}-worker-${testInfo.workerIndex}`;
|
|
759
774
|
const testKey = createHash("sha256")
|
|
760
775
|
.update(testInfo.testId)
|
|
761
776
|
.digest("hex")
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { atomicWriteFileSync } from "./atomic.js";
|
|
6
6
|
import { inferTestProvenance } from "./provenance.js";
|
|
7
7
|
import { serverEvidencePath } from "./transport.js";
|
|
8
|
+
|
|
9
|
+
let cachedProcessInstanceToken;
|
|
10
|
+
function processInstanceToken() {
|
|
11
|
+
if (cachedProcessInstanceToken)
|
|
12
|
+
return cachedProcessInstanceToken;
|
|
13
|
+
let random = "";
|
|
14
|
+
try {
|
|
15
|
+
random = randomBytes(3).toString("hex");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
random = Math.floor(Math.random() * 16777215).toString(16);
|
|
19
|
+
}
|
|
20
|
+
cachedProcessInstanceToken = `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
|
|
21
|
+
return cachedProcessInstanceToken;
|
|
22
|
+
}
|
|
8
23
|
function localFile(file) {
|
|
9
24
|
if (!file)
|
|
10
25
|
return undefined;
|
|
@@ -24,7 +39,9 @@ export function runnerTestId(identity) {
|
|
|
24
39
|
export function runnerExecutionScope(identity) {
|
|
25
40
|
const testId = runnerTestId(identity);
|
|
26
41
|
const retry = identity.retry ?? 0;
|
|
27
|
-
|
|
42
|
+
// Same hazard as the Playwright shim: a pooled runner's fresh workers can
|
|
43
|
+
// share a pid, so the worker identity carries a per-process token.
|
|
44
|
+
const workerId = `${identity.runner}-${process.env["JEST_WORKER_ID"] ?? process.pid}-${processInstanceToken()}`;
|
|
28
45
|
const testKey = createHash("sha256").update(testId).digest("hex").slice(0, 24);
|
|
29
46
|
return {
|
|
30
47
|
version: 1,
|
|
@@ -150,6 +150,7 @@ function createState() {
|
|
|
150
150
|
persistedServerRecords: /* @__PURE__ */ new Set(),
|
|
151
151
|
backgroundBuffers: /* @__PURE__ */ new Map(),
|
|
152
152
|
backgroundWriters: /* @__PURE__ */ new Map(),
|
|
153
|
+
backgroundShardSizes: /* @__PURE__ */ new Map(),
|
|
153
154
|
backgroundSequence: 0,
|
|
154
155
|
runtimeSnapshots: false,
|
|
155
156
|
assertionPhases: /* @__PURE__ */ new Map(),
|
|
@@ -427,6 +428,16 @@ function writeExclusiveBackgroundRecord(fs, runId, writer, initialSequence, payl
|
|
|
427
428
|
}
|
|
428
429
|
throw Object.assign(new Error("Could not allocate a collision-free Supercov background evidence record"), { code: "SUPERCOV_BACKGROUND_COLLISION_LIMIT" });
|
|
429
430
|
}
|
|
431
|
+
function backgroundWriterToken() {
|
|
432
|
+
try {
|
|
433
|
+
const getBuiltinModule = process.getBuiltinModule;
|
|
434
|
+
const crypto = getBuiltinModule == null ? void 0 : getBuiltinModule("node:crypto");
|
|
435
|
+
if (crypto)
|
|
436
|
+
return crypto.randomBytes(4).toString("hex");
|
|
437
|
+
} catch (e) {
|
|
438
|
+
}
|
|
439
|
+
return Math.floor(Math.random() * 4294967295).toString(16);
|
|
440
|
+
}
|
|
430
441
|
function appendDurableBackgroundRecord(fs, runId, record) {
|
|
431
442
|
var _a8;
|
|
432
443
|
const records = state.backgroundBuffers.get(runId) != null ? state.backgroundBuffers.get(runId) : /* @__PURE__ */ new Map();
|
|
@@ -437,15 +448,32 @@ function appendDurableBackgroundRecord(fs, runId, record) {
|
|
|
437
448
|
const payload = JSON.stringify(record) + "\n";
|
|
438
449
|
fs.mkdirSync(directory, { recursive: true });
|
|
439
450
|
let path = state.backgroundWriters.get(runId);
|
|
451
|
+
// A pid is not an identity: pool VMs restored from one snapshot run clones
|
|
452
|
+
// of this very process, same pid and same cached shard path, and their
|
|
453
|
+
// appends over a shared mount tear each other's lines. The shard name gets
|
|
454
|
+
// a fresh random token, and an append that finds the file a different size
|
|
455
|
+
// than this writer left it has met a clone: it moves to a new shard.
|
|
456
|
+
if (path) {
|
|
457
|
+
let currentSize = -1;
|
|
458
|
+
try {
|
|
459
|
+
currentSize = fs.statSync(path).size;
|
|
460
|
+
} catch (e) {
|
|
461
|
+
}
|
|
462
|
+
if (currentSize !== (state.backgroundShardSizes.get(path) ?? -1)) {
|
|
463
|
+
path = void 0;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
440
466
|
if (!path) {
|
|
441
467
|
const shard = (_a8 = process.env["SUPERCOV_EXECUTION_LOG_SHARD"]) != null ? _a8 : "process";
|
|
442
|
-
const writer = `${shard}-${process.pid}`;
|
|
468
|
+
const writer = `${shard}-${process.pid}-${backgroundWriterToken()}`;
|
|
443
469
|
const nextSequence = writeExclusiveBackgroundRecord(fs, runId, writer, state.backgroundSequence, payload);
|
|
444
470
|
state.backgroundSequence = nextSequence;
|
|
445
471
|
path = backgroundEvidencePath(runId, `${writer}-${nextSequence - 1}`);
|
|
446
472
|
state.backgroundWriters.set(runId, path);
|
|
473
|
+
state.backgroundShardSizes.set(path, Buffer.byteLength(payload));
|
|
447
474
|
} else {
|
|
448
475
|
fs.appendFileSync(path, payload);
|
|
476
|
+
state.backgroundShardSizes.set(path, (state.backgroundShardSizes.get(path) ?? 0) + Buffer.byteLength(payload));
|
|
449
477
|
}
|
|
450
478
|
records.set(key, record);
|
|
451
479
|
state.backgroundBuffers.set(runId, records);
|