supercov 0.0.8 → 0.0.11
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 +14 -1
- package/bin/native.js +87 -0
- package/bin/supercov.js +20 -3
- package/dist/agentJson.d.ts +2 -1
- package/dist/agentJson.d.ts.map +1 -1
- package/dist/agentJson.js +18 -0
- package/dist/agentJson.js.map +1 -1
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +9 -5
- package/dist/analyze.js.map +1 -1
- package/dist/cli.js +126 -23
- package/dist/cli.js.map +1 -1
- package/dist/directInstrumenter.d.ts.map +1 -1
- package/dist/directInstrumenter.js +39 -19
- package/dist/directInstrumenter.js.map +1 -1
- package/dist/engineEvidence.d.ts +9 -0
- package/dist/engineEvidence.d.ts.map +1 -0
- package/dist/engineEvidence.js +31 -0
- package/dist/engineEvidence.js.map +1 -0
- package/dist/engineInstrumenter.d.ts +16 -0
- package/dist/engineInstrumenter.d.ts.map +1 -0
- package/dist/engineInstrumenter.js +50 -0
- package/dist/engineInstrumenter.js.map +1 -0
- package/dist/engineProcess.d.ts +11 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +59 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/evidenceArchive.d.ts +1 -0
- package/dist/evidenceArchive.d.ts.map +1 -1
- package/dist/evidenceArchive.js +59 -13
- package/dist/evidenceArchive.js.map +1 -1
- package/dist/instrumenter.d.ts +8 -1
- package/dist/instrumenter.d.ts.map +1 -1
- package/dist/instrumenter.js +585 -113
- package/dist/instrumenter.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +20 -1
- package/dist/integrity.js.map +1 -1
- package/dist/launchSupervisor.d.ts.map +1 -1
- package/dist/launchSupervisor.js +3 -0
- package/dist/launchSupervisor.js.map +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +9 -6
- package/dist/merge.js.map +1 -1
- package/dist/nodeAssert.d.ts +31 -0
- package/dist/nodeAssert.d.ts.map +1 -0
- package/dist/nodeAssert.js +30 -0
- package/dist/nodeAssert.js.map +1 -0
- package/dist/nodeAssertAdapter.d.ts +6 -0
- package/dist/nodeAssertAdapter.d.ts.map +1 -0
- package/dist/nodeAssertAdapter.js +94 -0
- package/dist/nodeAssertAdapter.js.map +1 -0
- package/dist/nodeAssertStrict.d.ts +31 -0
- package/dist/nodeAssertStrict.d.ts.map +1 -0
- package/dist/nodeAssertStrict.js +28 -0
- package/dist/nodeAssertStrict.js.map +1 -0
- package/dist/nodeAssertionInstrumenter.d.ts +8 -0
- package/dist/nodeAssertionInstrumenter.d.ts.map +1 -0
- package/dist/nodeAssertionInstrumenter.js +271 -0
- package/dist/nodeAssertionInstrumenter.js.map +1 -0
- package/dist/nodeTest.d.ts.map +1 -1
- package/dist/nodeTest.js +14 -5
- package/dist/nodeTest.js.map +1 -1
- package/dist/playwright.d.ts.map +1 -1
- package/dist/playwright.js +177 -89
- package/dist/playwright.js.map +1 -1
- package/dist/processDiagnostics.d.ts +34 -0
- package/dist/processDiagnostics.d.ts.map +1 -0
- package/dist/processDiagnostics.js +155 -0
- package/dist/processDiagnostics.js.map +1 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +39 -4
- package/dist/project.js.map +1 -1
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +224 -11
- package/dist/query.js.map +1 -1
- package/dist/register.mjs +97 -4
- package/dist/register.mjs.map +1 -1
- package/dist/resolve-loader.d.mts.map +1 -1
- package/dist/resolve-loader.mjs +16 -2
- package/dist/resolve-loader.mjs.map +1 -1
- package/dist/runAnalysis.d.ts.map +1 -1
- package/dist/runAnalysis.js +3 -1
- package/dist/runAnalysis.js.map +1 -1
- package/dist/runnerEvidence.d.ts +3 -3
- package/dist/runnerEvidence.d.ts.map +1 -1
- package/dist/runnerEvidence.js +7 -4
- package/dist/runnerEvidence.js.map +1 -1
- package/dist/runtime.d.ts +77 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +395 -26
- package/dist/runtime.js.map +1 -1
- package/dist/runtimeIsolation.d.ts +3 -0
- package/dist/runtimeIsolation.d.ts.map +1 -0
- package/dist/runtimeIsolation.js +9 -0
- package/dist/runtimeIsolation.js.map +1 -0
- package/dist/sourceDiscovery.d.ts.map +1 -1
- package/dist/sourceDiscovery.js +7 -2
- package/dist/sourceDiscovery.js.map +1 -1
- package/dist/transport.js +2 -2
- package/dist/transport.js.map +1 -1
- package/dist/vitePlugin.d.ts.map +1 -1
- package/dist/vitePlugin.js +21 -7
- package/dist/vitePlugin.js.map +1 -1
- package/dist/vitest.js +38 -7
- package/dist/vitest.js.map +1 -1
- package/dist/waivers.d.ts +52 -0
- package/dist/waivers.d.ts.map +1 -0
- package/dist/waivers.js +121 -0
- package/dist/waivers.js.map +1 -0
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +216 -37
- package/dist/workspace.js.map +1 -1
- package/package.json +29 -3
package/dist/waivers.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { SupercovError } from "./agentJson.js";
|
|
4
|
+
export const WAIVERS_FILE = "supercov.waivers.json";
|
|
5
|
+
export const WAIVERS_SCHEMA_VERSION = 1;
|
|
6
|
+
function normalizedSource(source) {
|
|
7
|
+
return source.replace(/\s+/g, " ").trim();
|
|
8
|
+
}
|
|
9
|
+
function invalidWaiver(index, problem) {
|
|
10
|
+
return new SupercovError("INVALID_ARGUMENT", `${WAIVERS_FILE} waiver ${index + 1} ${problem}`, { details: { file: WAIVERS_FILE, waiver: index + 1 } });
|
|
11
|
+
}
|
|
12
|
+
export function readCoverageWaivers(root) {
|
|
13
|
+
const path = resolve(root, WAIVERS_FILE);
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = readFileSync(path, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new SupercovError("INVALID_ARGUMENT", `${WAIVERS_FILE} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, { details: { file: WAIVERS_FILE } });
|
|
27
|
+
}
|
|
28
|
+
const record = parsed;
|
|
29
|
+
if (record?.version !== WAIVERS_SCHEMA_VERSION || !Array.isArray(record.waivers)) {
|
|
30
|
+
throw new SupercovError("INVALID_ARGUMENT", `${WAIVERS_FILE} must be {"version": 1, "waivers": [...]}`, { details: { file: WAIVERS_FILE } });
|
|
31
|
+
}
|
|
32
|
+
const waivers = record.waivers.map((entry, index) => {
|
|
33
|
+
const waiver = entry;
|
|
34
|
+
if (typeof waiver?.file !== "string" || waiver.file.length === 0)
|
|
35
|
+
throw invalidWaiver(index, "requires a non-empty file");
|
|
36
|
+
if (typeof waiver.condition !== "string" || waiver.condition.length === 0)
|
|
37
|
+
throw invalidWaiver(index, "requires a non-empty condition");
|
|
38
|
+
if (typeof waiver.reason !== "string" || waiver.reason.trim().length === 0)
|
|
39
|
+
throw invalidWaiver(index, "requires a non-empty reason");
|
|
40
|
+
if (waiver.decision !== undefined && typeof waiver.decision !== "string")
|
|
41
|
+
throw invalidWaiver(index, "has a non-string decision");
|
|
42
|
+
if (waiver.line !== undefined &&
|
|
43
|
+
(!Number.isSafeInteger(waiver.line) || waiver.line < 1))
|
|
44
|
+
throw invalidWaiver(index, "has a non-positive line");
|
|
45
|
+
if (/^C\d+$/.test(waiver.condition) && !waiver.decision)
|
|
46
|
+
throw invalidWaiver(index, `uses the positional condition ${waiver.condition} without a decision`);
|
|
47
|
+
return {
|
|
48
|
+
file: waiver.file,
|
|
49
|
+
...(waiver.decision !== undefined ? { decision: waiver.decision } : {}),
|
|
50
|
+
...(waiver.line !== undefined ? { line: waiver.line } : {}),
|
|
51
|
+
condition: waiver.condition,
|
|
52
|
+
reason: waiver.reason,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return { path, waivers };
|
|
56
|
+
}
|
|
57
|
+
export function evaluateCoverageWaivers(decisions, source) {
|
|
58
|
+
const applied = [];
|
|
59
|
+
const contradicted = [];
|
|
60
|
+
const unmatched = [];
|
|
61
|
+
const waivedByDecision = new Map();
|
|
62
|
+
const appliedByFile = new Map();
|
|
63
|
+
for (const waiver of source.waivers) {
|
|
64
|
+
const matches = [];
|
|
65
|
+
for (const decision of decisions) {
|
|
66
|
+
if (decision.meta.file !== waiver.file)
|
|
67
|
+
continue;
|
|
68
|
+
if (waiver.line !== undefined && decision.meta.line !== waiver.line)
|
|
69
|
+
continue;
|
|
70
|
+
if (waiver.decision !== undefined &&
|
|
71
|
+
decision.meta.id !== waiver.decision &&
|
|
72
|
+
normalizedSource(decision.meta.source) !==
|
|
73
|
+
normalizedSource(waiver.decision))
|
|
74
|
+
continue;
|
|
75
|
+
for (const condition of decision.conditions) {
|
|
76
|
+
const positional = `C${condition.index + 1}`;
|
|
77
|
+
if (waiver.condition !== positional &&
|
|
78
|
+
normalizedSource(condition.source) !==
|
|
79
|
+
normalizedSource(waiver.condition))
|
|
80
|
+
continue;
|
|
81
|
+
matches.push({
|
|
82
|
+
waiver,
|
|
83
|
+
decisionId: decision.meta.id,
|
|
84
|
+
file: decision.meta.file,
|
|
85
|
+
line: decision.meta.line,
|
|
86
|
+
conditionIndex: condition.index,
|
|
87
|
+
conditionSource: condition.source,
|
|
88
|
+
covered: condition.covered,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (matches.length === 0) {
|
|
93
|
+
unmatched.push(waiver);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const match of matches) {
|
|
97
|
+
if (match.covered) {
|
|
98
|
+
contradicted.push(match);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const byCondition = waivedByDecision.get(match.decisionId) ??
|
|
102
|
+
new Map();
|
|
103
|
+
if (byCondition.has(match.conditionIndex))
|
|
104
|
+
continue;
|
|
105
|
+
applied.push(match);
|
|
106
|
+
byCondition.set(match.conditionIndex, match.waiver);
|
|
107
|
+
waivedByDecision.set(match.decisionId, byCondition);
|
|
108
|
+
appliedByFile.set(match.file, (appliedByFile.get(match.file) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
path: source.path,
|
|
113
|
+
waivers: source.waivers,
|
|
114
|
+
applied,
|
|
115
|
+
contradicted,
|
|
116
|
+
unmatched,
|
|
117
|
+
waivedByDecision,
|
|
118
|
+
appliedByFile,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=waivers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waivers.js","sourceRoot":"","sources":["../src/waivers.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const RUN_STORE_CONTRACT_VERSION = 1;
|
|
1
2
|
export type RunStateStatus = "preparing" | "building" | "testing" | "publishing" | "complete" | "failed" | "interrupted" | "abandoned";
|
|
2
3
|
export interface RunState {
|
|
3
4
|
id: string;
|
|
@@ -10,6 +11,30 @@ export interface RunState {
|
|
|
10
11
|
signal?: NodeJS.Signals;
|
|
11
12
|
error?: string;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Unlinking a large tree (workspace copies, per-attempt evidence) can take
|
|
16
|
+
* minutes of uninterruptible I/O — observed at 26+ minutes on a real project.
|
|
17
|
+
* Removal must never block the command: rename the tree into the store's
|
|
18
|
+
* trash (atomic on the same filesystem, so the path disappears instantly) and
|
|
19
|
+
* let a detached deleter unlink it after this process has already returned.
|
|
20
|
+
* A crash between rename and unlink just leaves the entry in the trash, and
|
|
21
|
+
* every later invocation sweeps whatever it finds there.
|
|
22
|
+
*/
|
|
23
|
+
export declare function removeStoredTreeDeferred(root: string, target: string): string | undefined;
|
|
24
|
+
/** The detached deleter's whole program; also executed verbatim by tests. */
|
|
25
|
+
export declare const TRASH_DELETER_SCRIPT: string;
|
|
26
|
+
/** Unlink everything in the trash without making anyone wait for it. */
|
|
27
|
+
export declare function spawnTrashDeleter(root: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* The instrumented workspace deliberately lives outside the dotted store.
|
|
30
|
+
* Widely used libraries treat *any* dot-prefixed path segment as a hidden
|
|
31
|
+
* dotfile — `send` (and therefore `express.static`, `serve-static` and
|
|
32
|
+
* `res.sendFile`) answers 404 for them by default — so an application under
|
|
33
|
+
* test cannot serve its own files from a dotted workspace. Copied test files
|
|
34
|
+
* are kept out of the user's ordinary runner discovery by
|
|
35
|
+
* `pruneCachedWorkspaceSources` at run end, not by hiding the path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function workspaceContainerPath(root: string): string;
|
|
13
38
|
export declare function isolatedWorkspacePath(root: string, runId: string): string;
|
|
14
39
|
/**
|
|
15
40
|
* Stable isolated namespace used as a live mount by VM/container runners.
|
|
@@ -18,6 +43,17 @@ export declare function isolatedWorkspacePath(root: string, runId: string): stri
|
|
|
18
43
|
* The project lock makes refreshes single-writer.
|
|
19
44
|
*/
|
|
20
45
|
export declare function cachedWorkspacePath(root: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Remove the copied source and test files from the stable cached workspace
|
|
48
|
+
* once a run ends. Between runs those copies are pure liability: the next
|
|
49
|
+
* refresh re-copies them anyway, but an ordinary test runner invoked at the
|
|
50
|
+
* project root (whose default discovery does not exclude dot-directories —
|
|
51
|
+
* Vitest 4 excludes only node_modules and .git) would find the copied test
|
|
52
|
+
* files and silently double-count the user's suite. Keep only what the next
|
|
53
|
+
* run genuinely reuses: dependency symlinks, the generated adapter directory,
|
|
54
|
+
* and the instrumented build artifacts the build cache declares.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pruneCachedWorkspaceSources(root: string): string[];
|
|
21
57
|
export interface CacheRecoveryResult {
|
|
22
58
|
restoredPrevious: boolean;
|
|
23
59
|
removedStaging: number;
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { copyFileSync, constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, fsyncSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, statSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { copyFileSync, constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, fsyncSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
3
5
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
6
|
import { atomicRenameSync, atomicWriteFileSync } from "./atomic.js";
|
|
7
|
+
export const RUN_STORE_CONTRACT_VERSION = 1;
|
|
5
8
|
const TERMINAL = new Set([
|
|
6
9
|
"complete",
|
|
7
10
|
"failed",
|
|
@@ -9,6 +12,7 @@ const TERMINAL = new Set([
|
|
|
9
12
|
"abandoned",
|
|
10
13
|
]);
|
|
11
14
|
const ROOT_EXCLUSIONS = new Set([
|
|
15
|
+
".cache",
|
|
12
16
|
".git",
|
|
13
17
|
".supercov",
|
|
14
18
|
".mcdc-pool",
|
|
@@ -22,6 +26,122 @@ const ROOT_EXCLUSIONS = new Set([
|
|
|
22
26
|
"playwright-report",
|
|
23
27
|
"test-results",
|
|
24
28
|
]);
|
|
29
|
+
const NESTED_SUPERCOV_EXCLUSIONS = new Set([".supercov", ".mcdc-pool"]);
|
|
30
|
+
const TRASH_DIRECTORY = ".supercov/.trash";
|
|
31
|
+
/**
|
|
32
|
+
* Unlinking a large tree (workspace copies, per-attempt evidence) can take
|
|
33
|
+
* minutes of uninterruptible I/O — observed at 26+ minutes on a real project.
|
|
34
|
+
* Removal must never block the command: rename the tree into the store's
|
|
35
|
+
* trash (atomic on the same filesystem, so the path disappears instantly) and
|
|
36
|
+
* let a detached deleter unlink it after this process has already returned.
|
|
37
|
+
* A crash between rename and unlink just leaves the entry in the trash, and
|
|
38
|
+
* every later invocation sweeps whatever it finds there.
|
|
39
|
+
*/
|
|
40
|
+
export function removeStoredTreeDeferred(root, target) {
|
|
41
|
+
const resolvedRoot = resolve(root);
|
|
42
|
+
const resolvedTarget = resolve(target);
|
|
43
|
+
if (!existsSync(resolvedTarget))
|
|
44
|
+
return undefined;
|
|
45
|
+
const trash = resolve(root, TRASH_DIRECTORY);
|
|
46
|
+
const store = resolve(resolvedRoot, ".supercov");
|
|
47
|
+
const container = workspaceContainerPath(resolvedRoot);
|
|
48
|
+
const inStore = resolvedTarget !== store &&
|
|
49
|
+
inside(store, resolvedTarget) &&
|
|
50
|
+
!inside(trash, resolvedTarget);
|
|
51
|
+
const inWorkspaceContainer = isWorkspaceContainer(container) && inside(container, resolvedTarget);
|
|
52
|
+
if (!inStore && !inWorkspaceContainer)
|
|
53
|
+
throw new Error(`Refusing to defer removal outside Supercov-owned storage: ${resolvedTarget}`);
|
|
54
|
+
mkdirSync(trash, { recursive: true });
|
|
55
|
+
const destination = resolve(trash, `${process.pid}-${randomUUID()}`);
|
|
56
|
+
try {
|
|
57
|
+
renameSync(resolvedTarget, destination);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const code = error.code ?? "unknown";
|
|
61
|
+
throw new Error(`Could not move Supercov data into deferred trash (${code}): ${resolvedTarget}`, { cause: error });
|
|
62
|
+
}
|
|
63
|
+
return destination;
|
|
64
|
+
}
|
|
65
|
+
/** The detached deleter's whole program; also executed verbatim by tests. */
|
|
66
|
+
export const TRASH_DELETER_SCRIPT = [
|
|
67
|
+
`const { closeSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } = require("node:fs");`,
|
|
68
|
+
`const { resolve } = require("node:path");`,
|
|
69
|
+
`const trash = process.argv[1];`,
|
|
70
|
+
`const lock = resolve(trash, ".deleter.lock");`,
|
|
71
|
+
`const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };`,
|
|
72
|
+
`try {`,
|
|
73
|
+
` const existing = Number(readFileSync(lock, "utf8"));`,
|
|
74
|
+
` if (Number.isSafeInteger(existing) && existing > 0 && alive(existing)) process.exit(0);`,
|
|
75
|
+
` try { unlinkSync(lock); } catch {}`,
|
|
76
|
+
`} catch {}`,
|
|
77
|
+
`let descriptor;`,
|
|
78
|
+
`try { descriptor = openSync(lock, "wx"); writeFileSync(descriptor, String(process.pid)); closeSync(descriptor); } catch { process.exit(0); }`,
|
|
79
|
+
`let entries = [];`,
|
|
80
|
+
`try { entries = readdirSync(trash); } catch {}`,
|
|
81
|
+
`for (const entry of entries) {`,
|
|
82
|
+
` if (entry === ".deleter.lock") continue;`,
|
|
83
|
+
` try { rmSync(resolve(trash, entry), { recursive: true, force: true, maxRetries: 3 }); } catch {}`,
|
|
84
|
+
`}`,
|
|
85
|
+
`try { unlinkSync(lock); } catch {}`,
|
|
86
|
+
].join("\n");
|
|
87
|
+
/** Unlink everything in the trash without making anyone wait for it. */
|
|
88
|
+
export function spawnTrashDeleter(root) {
|
|
89
|
+
const trash = resolve(root, TRASH_DIRECTORY);
|
|
90
|
+
try {
|
|
91
|
+
if (readdirSync(trash).length === 0)
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
spawn(process.execPath, ["-e", TRASH_DELETER_SCRIPT, trash], {
|
|
99
|
+
// A detached process must never inherit a project-owned directory as
|
|
100
|
+
// its cwd. Windows keeps that directory open and rejects the final
|
|
101
|
+
// recursive removal with EPERM until the deleter exits.
|
|
102
|
+
cwd: tmpdir(),
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: "ignore",
|
|
105
|
+
}).unref();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Spawning is best-effort; the next invocation sweeps the same trash.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Marks `supercov/` as ours, so a project may own a directory by that name. */
|
|
112
|
+
const WORKSPACE_CONTAINER_MARKER = ".supercov-workspace-store";
|
|
113
|
+
/**
|
|
114
|
+
* The instrumented workspace deliberately lives outside the dotted store.
|
|
115
|
+
* Widely used libraries treat *any* dot-prefixed path segment as a hidden
|
|
116
|
+
* dotfile — `send` (and therefore `express.static`, `serve-static` and
|
|
117
|
+
* `res.sendFile`) answers 404 for them by default — so an application under
|
|
118
|
+
* test cannot serve its own files from a dotted workspace. Copied test files
|
|
119
|
+
* are kept out of the user's ordinary runner discovery by
|
|
120
|
+
* `pruneCachedWorkspaceSources` at run end, not by hiding the path.
|
|
121
|
+
*/
|
|
122
|
+
export function workspaceContainerPath(root) {
|
|
123
|
+
return resolve(root, "supercov");
|
|
124
|
+
}
|
|
125
|
+
/** True only for a `supercov/` directory this tool created. */
|
|
126
|
+
function isWorkspaceContainer(path) {
|
|
127
|
+
return (basename(path) === "supercov" &&
|
|
128
|
+
existsSync(resolve(path, WORKSPACE_CONTAINER_MARKER)));
|
|
129
|
+
}
|
|
130
|
+
function ensureWorkspaceContainer(root) {
|
|
131
|
+
const container = workspaceContainerPath(root);
|
|
132
|
+
mkdirSync(container, { recursive: true });
|
|
133
|
+
// Self-ignoring, so the directory never reaches the user's diff, and marked
|
|
134
|
+
// so source copying can distinguish it from a project's own `supercov/`.
|
|
135
|
+
for (const [file, contents] of [
|
|
136
|
+
[".gitignore", "*\n"],
|
|
137
|
+
[WORKSPACE_CONTAINER_MARKER, "Supercov instrumented workspace. Safe to delete.\n"],
|
|
138
|
+
]) {
|
|
139
|
+
const path = resolve(container, file);
|
|
140
|
+
if (!existsSync(path))
|
|
141
|
+
atomicWriteFileSync(path, contents);
|
|
142
|
+
}
|
|
143
|
+
return container;
|
|
144
|
+
}
|
|
25
145
|
function processExists(pid) {
|
|
26
146
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
27
147
|
return false;
|
|
@@ -54,7 +174,39 @@ export function isolatedWorkspacePath(root, runId) {
|
|
|
54
174
|
* The project lock makes refreshes single-writer.
|
|
55
175
|
*/
|
|
56
176
|
export function cachedWorkspacePath(root) {
|
|
57
|
-
return resolve(root, "
|
|
177
|
+
return resolve(workspaceContainerPath(root), "workspace", basename(root));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Remove the copied source and test files from the stable cached workspace
|
|
181
|
+
* once a run ends. Between runs those copies are pure liability: the next
|
|
182
|
+
* refresh re-copies them anyway, but an ordinary test runner invoked at the
|
|
183
|
+
* project root (whose default discovery does not exclude dot-directories —
|
|
184
|
+
* Vitest 4 excludes only node_modules and .git) would find the copied test
|
|
185
|
+
* files and silently double-count the user's suite. Keep only what the next
|
|
186
|
+
* run genuinely reuses: dependency symlinks, the generated adapter directory,
|
|
187
|
+
* and the instrumented build artifacts the build cache declares.
|
|
188
|
+
*/
|
|
189
|
+
export function pruneCachedWorkspaceSources(root) {
|
|
190
|
+
const workspace = cachedWorkspacePath(root);
|
|
191
|
+
if (!existsSync(workspace))
|
|
192
|
+
return [];
|
|
193
|
+
const metadata = readJson(resolve(workspace, ".supercov/build-cache.json"));
|
|
194
|
+
const keepRoots = new Set(["node_modules", ".supercov"]);
|
|
195
|
+
for (const artifact of metadata?.artifactPaths ?? []) {
|
|
196
|
+
const top = artifact.split(/[\\/]/)[0];
|
|
197
|
+
if (top)
|
|
198
|
+
keepRoots.add(top);
|
|
199
|
+
}
|
|
200
|
+
const removed = [];
|
|
201
|
+
for (const entry of readdirSync(workspace, { withFileTypes: true })) {
|
|
202
|
+
if (keepRoots.has(entry.name))
|
|
203
|
+
continue;
|
|
204
|
+
removeStoredTreeDeferred(root, resolve(workspace, entry.name));
|
|
205
|
+
removed.push(entry.name);
|
|
206
|
+
}
|
|
207
|
+
if (removed.length > 0)
|
|
208
|
+
spawnTrashDeleter(root);
|
|
209
|
+
return removed.sort();
|
|
58
210
|
}
|
|
59
211
|
function cacheTransactionPrefix(root, kind) {
|
|
60
212
|
return `.${basename(root)}.${kind}-`;
|
|
@@ -93,11 +245,13 @@ export function recoverCachedWorkspace(root) {
|
|
|
93
245
|
restoredPrevious = true;
|
|
94
246
|
}
|
|
95
247
|
for (const path of staging)
|
|
96
|
-
|
|
248
|
+
removeStoredTreeDeferred(root, path);
|
|
97
249
|
for (const path of previous)
|
|
98
|
-
|
|
250
|
+
removeStoredTreeDeferred(root, path);
|
|
99
251
|
for (const path of invalidPrevious)
|
|
100
|
-
|
|
252
|
+
removeStoredTreeDeferred(root, path);
|
|
253
|
+
if (staging.length + previous.length + invalidPrevious.length > 0)
|
|
254
|
+
spawnTrashDeleter(root);
|
|
101
255
|
return {
|
|
102
256
|
restoredPrevious,
|
|
103
257
|
removedStaging: staging.length,
|
|
@@ -133,18 +287,16 @@ export function finalizePublishedRunStorage(root, runId) {
|
|
|
133
287
|
!existsSync(resolve(runDirectory, "evidence.raw.gz"))) {
|
|
134
288
|
return false;
|
|
135
289
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
rmSync(resolve(root, ".supercov/work", runId), {
|
|
141
|
-
recursive: true,
|
|
142
|
-
force: true,
|
|
143
|
-
});
|
|
290
|
+
removeStoredTreeDeferred(root, resolve(root, ".supercov/evidence", runId));
|
|
291
|
+
removeStoredTreeDeferred(root, resolve(root, ".supercov/work", runId));
|
|
292
|
+
spawnTrashDeleter(root);
|
|
144
293
|
return true;
|
|
145
294
|
}
|
|
146
295
|
/** Recover dead runs and finish any publication whose atomic rename landed. */
|
|
147
296
|
export function recoverAbandonedRuns(root) {
|
|
297
|
+
// A prior detached deleter may have been killed with the host. Sweep its
|
|
298
|
+
// durable trash before inspecting current run state.
|
|
299
|
+
spawnTrashDeleter(root);
|
|
148
300
|
const workRoot = resolve(root, ".supercov/work");
|
|
149
301
|
if (!existsSync(workRoot))
|
|
150
302
|
return [];
|
|
@@ -165,14 +317,8 @@ export function recoverAbandonedRuns(root) {
|
|
|
165
317
|
// Never trust a persisted path as a deletion target. A partially written
|
|
166
318
|
// or manually edited state file cannot widen cleanup beyond this run's
|
|
167
319
|
// deterministic workspace namespace.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
force: true,
|
|
171
|
-
});
|
|
172
|
-
rmSync(resolve(root, ".supercov/work", entry.name, "run-publication"), {
|
|
173
|
-
recursive: true,
|
|
174
|
-
force: true,
|
|
175
|
-
});
|
|
320
|
+
removeStoredTreeDeferred(root, isolatedWorkspacePath(root, entry.name));
|
|
321
|
+
removeStoredTreeDeferred(root, resolve(root, ".supercov/work", entry.name, "run-publication"));
|
|
176
322
|
if (finalizePublishedRunStorage(root, entry.name)) {
|
|
177
323
|
// The run directory is published by one atomic rename before the run
|
|
178
324
|
// state flips terminal. Its run.json is the durable terminal record, so
|
|
@@ -182,10 +328,7 @@ export function recoverAbandonedRuns(root) {
|
|
|
182
328
|
// Evidence moved out of the disposable workspace before run
|
|
183
329
|
// generation is not a visible run. Remove that orphan on recovery so a
|
|
184
330
|
// hard kill cannot accumulate partial run data indefinitely.
|
|
185
|
-
|
|
186
|
-
recursive: true,
|
|
187
|
-
force: true,
|
|
188
|
-
});
|
|
331
|
+
removeStoredTreeDeferred(root, resolve(root, ".supercov/evidence", entry.name));
|
|
189
332
|
updateRunState(root, entry.name, {
|
|
190
333
|
status: "abandoned",
|
|
191
334
|
error: `Recovered after process ${state.pid} exited without cleanup`,
|
|
@@ -193,6 +336,8 @@ export function recoverAbandonedRuns(root) {
|
|
|
193
336
|
}
|
|
194
337
|
recovered.push(entry.name);
|
|
195
338
|
}
|
|
339
|
+
if (recovered.length > 0)
|
|
340
|
+
spawnTrashDeleter(root);
|
|
196
341
|
return recovered.sort();
|
|
197
342
|
}
|
|
198
343
|
export function acquireProjectLock(root, runId) {
|
|
@@ -248,9 +393,14 @@ function inside(root, path) {
|
|
|
248
393
|
function copyTree(source, destination, root = false, sourceRoot = source, destinationRoot = destination, hooks = {}, finalDestinationRoot = destinationRoot) {
|
|
249
394
|
mkdirSync(destination, { recursive: true });
|
|
250
395
|
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
251
|
-
if (root && ROOT_EXCLUSIONS.has(entry.name))
|
|
396
|
+
if ((root && ROOT_EXCLUSIONS.has(entry.name)) ||
|
|
397
|
+
NESTED_SUPERCOV_EXCLUSIONS.has(entry.name))
|
|
252
398
|
continue;
|
|
253
399
|
const from = resolve(source, entry.name);
|
|
400
|
+
// Never copy our own workspace into itself. Matched by marker rather than
|
|
401
|
+
// by name so a project may legitimately own a `supercov/` directory.
|
|
402
|
+
if (entry.isDirectory() && isWorkspaceContainer(from))
|
|
403
|
+
continue;
|
|
254
404
|
const to = resolve(destination, entry.name);
|
|
255
405
|
const stat = lstatSync(from);
|
|
256
406
|
if (stat.isDirectory())
|
|
@@ -301,7 +451,8 @@ function copyTree(source, destination, root = false, sourceRoot = source, destin
|
|
|
301
451
|
/** Create a copy-on-write project snapshot whose build outputs are disposable. */
|
|
302
452
|
export function prepareIsolatedWorkspace(root, runId) {
|
|
303
453
|
const workspace = isolatedWorkspacePath(root, runId);
|
|
304
|
-
|
|
454
|
+
if (removeStoredTreeDeferred(root, workspace))
|
|
455
|
+
spawnTrashDeleter(root);
|
|
305
456
|
copyTree(root, workspace, true);
|
|
306
457
|
const nodeModules = resolve(root, "node_modules");
|
|
307
458
|
if (existsSync(nodeModules)) {
|
|
@@ -329,6 +480,7 @@ export function prepareIsolatedWorkspace(root, runId) {
|
|
|
329
480
|
/** Refresh the stable, disposable instrumented-build namespace. */
|
|
330
481
|
export function prepareCachedWorkspace(root, hooks = {}) {
|
|
331
482
|
const workspace = cachedWorkspacePath(root);
|
|
483
|
+
ensureWorkspaceContainer(root);
|
|
332
484
|
recoverCachedWorkspace(root);
|
|
333
485
|
const staging = cacheTransactionPath(root, "staging");
|
|
334
486
|
const previous = cacheTransactionPath(root, "previous");
|
|
@@ -396,13 +548,15 @@ export function prepareCachedWorkspace(root, hooks = {}) {
|
|
|
396
548
|
throw error;
|
|
397
549
|
}
|
|
398
550
|
finally {
|
|
399
|
-
|
|
551
|
+
if (removeStoredTreeDeferred(root, staging))
|
|
552
|
+
spawnTrashDeleter(root);
|
|
400
553
|
}
|
|
401
554
|
// Failure to remove an obsolete previous generation must not invalidate the
|
|
402
555
|
// newly published cache. The next invocation and `supercov clean` both
|
|
403
556
|
// remove it deterministically.
|
|
404
557
|
try {
|
|
405
|
-
|
|
558
|
+
if (removeStoredTreeDeferred(root, previous))
|
|
559
|
+
spawnTrashDeleter(root);
|
|
406
560
|
}
|
|
407
561
|
catch {
|
|
408
562
|
// Deliberately retained for recoverCachedWorkspace().
|
|
@@ -415,7 +569,8 @@ export function removeIsolatedWorkspace(root, runId) {
|
|
|
415
569
|
if (relativePath.startsWith("..") || relativePath.split(sep).length < 2) {
|
|
416
570
|
throw new Error(`Refusing to remove unexpected workspace path: ${workspace}`);
|
|
417
571
|
}
|
|
418
|
-
|
|
572
|
+
if (removeStoredTreeDeferred(root, workspace))
|
|
573
|
+
spawnTrashDeleter(root);
|
|
419
574
|
}
|
|
420
575
|
/** Deterministic retention: IDs sort newest-first because run IDs are UTC. */
|
|
421
576
|
function cleanCoverageStorageLocked(root, options, removeBuildCache) {
|
|
@@ -458,24 +613,48 @@ function cleanCoverageStorageLocked(root, options, removeBuildCache) {
|
|
|
458
613
|
if (removeTransientWork) {
|
|
459
614
|
removedWorkspaces.push(id);
|
|
460
615
|
if (!options.dryRun)
|
|
461
|
-
|
|
616
|
+
removeStoredTreeDeferred(root, work);
|
|
462
617
|
}
|
|
463
618
|
const hasLooseEvidence = existsSync(resolve(evidenceRoot, id));
|
|
464
619
|
if (hasLooseEvidence && (!hasPublishedRun || removeHistory)) {
|
|
465
620
|
removedEvidence.push(id);
|
|
466
621
|
if (!options.dryRun)
|
|
467
|
-
|
|
622
|
+
removeStoredTreeDeferred(root, resolve(evidenceRoot, id));
|
|
468
623
|
}
|
|
469
624
|
if (removeHistory) {
|
|
470
625
|
removedRuns.push(id);
|
|
471
626
|
if (!options.dryRun)
|
|
472
|
-
|
|
627
|
+
removeStoredTreeDeferred(root, resolve(runsRoot, id));
|
|
473
628
|
}
|
|
474
629
|
}
|
|
475
|
-
const buildCache =
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
630
|
+
const buildCache = workspaceContainerPath(root);
|
|
631
|
+
// Releases before the workspace was moved out of the dotted store used
|
|
632
|
+
// both of these names. `clean` is the explicit destructive command, so it
|
|
633
|
+
// must migrate them too; otherwise a project can retain millions of files
|
|
634
|
+
// in a layout the current engine will never reuse. `prune` deliberately
|
|
635
|
+
// preserves every cache generation.
|
|
636
|
+
const legacyBuildCaches = [
|
|
637
|
+
resolve(root, ".supercov/.cache"),
|
|
638
|
+
resolve(root, ".supercov/cache"),
|
|
639
|
+
];
|
|
640
|
+
const removableBuildCaches = removeBuildCache && active.size === 0
|
|
641
|
+
? [
|
|
642
|
+
...(existsSync(buildCache) && isWorkspaceContainer(buildCache)
|
|
643
|
+
? [buildCache]
|
|
644
|
+
: []),
|
|
645
|
+
...legacyBuildCaches.filter((path) => existsSync(path)),
|
|
646
|
+
]
|
|
647
|
+
: [];
|
|
648
|
+
const removedBuildCache = removableBuildCaches.length > 0;
|
|
649
|
+
if (!options.dryRun)
|
|
650
|
+
for (const path of removableBuildCaches)
|
|
651
|
+
removeStoredTreeDeferred(root, path);
|
|
652
|
+
if (!options.dryRun &&
|
|
653
|
+
(removedRuns.length > 0 ||
|
|
654
|
+
removedWorkspaces.length > 0 ||
|
|
655
|
+
removedEvidence.length > 0 ||
|
|
656
|
+
removedBuildCache))
|
|
657
|
+
spawnTrashDeleter(root);
|
|
479
658
|
return { removedRuns, removedWorkspaces, removedEvidence, removedBuildCache };
|
|
480
659
|
}
|
|
481
660
|
/** Cleanup is itself a project-wide transaction, so it cannot race a run. */
|