supercov 4.0.1 → 4.1.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/docs/cli.md
CHANGED
|
@@ -147,6 +147,7 @@ npx supercov runs <run-id> [query] [options]
|
|
|
147
147
|
| `decision <id \| path:line>` | Understand missing boolean outcomes and MC/DC witnesses |
|
|
148
148
|
| `line <path:line>` | See one line's state, obligations, and covering tests |
|
|
149
149
|
| `test <id \| name>` | See the coverage attributed to one test |
|
|
150
|
+
| `tests without-evidence` | List the tests that made assertions and recorded no coverage, which the summary counts in a warning |
|
|
150
151
|
| `kinds` | Group coverage by test level, such as unit or E2E |
|
|
151
152
|
| `runners` | Group coverage by test runner |
|
|
152
153
|
| `scope` | See what is source and what is not, by directory and reason; `--files` lists every file |
|
|
@@ -175,12 +176,16 @@ npx supercov runs latest files --group dir --depth 2 --metric branches
|
|
|
175
176
|
```
|
|
176
177
|
|
|
177
178
|
```
|
|
178
|
-
Directory Files Lines
|
|
179
|
-
app
|
|
180
|
-
lib 1 10
|
|
181
|
-
|
|
179
|
+
Directory Files Lines e2e unit no test All
|
|
180
|
+
app 5 23 86.96% 0.00% 8.70% 95.65%
|
|
181
|
+
lib 1 10 90.00% 40.00% 0.00% 90.00%
|
|
182
|
+
. 2 8 75.00% 0.00% 25.00% 100.00%
|
|
183
|
+
components 1 2 100.00% 0.00% 0.00% 100.00%
|
|
182
184
|
```
|
|
183
185
|
|
|
186
|
+
`no test` is what ran only while no test was running, such as a server
|
|
187
|
+
starting before the first test. The column is there when a directory has any.
|
|
188
|
+
|
|
184
189
|
In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
|
|
185
190
|
for each metric beside what is missing, so a percentage can be worked out for
|
|
186
191
|
any file or set of files.
|
package/docs/coverage-model.md
CHANGED
|
@@ -106,6 +106,14 @@ By test kind
|
|
|
106
106
|
no test lines 17.14% branches 0.00% MC/DC 0.00%
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
`files --group dir` has the same figure as a `no test` column wherever a
|
|
110
|
+
directory has any, so a directory's `All` never stands above its kinds
|
|
111
|
+
unexplained.
|
|
112
|
+
|
|
113
|
+
A test that made assertions and has no covered line to its name is counted in
|
|
114
|
+
a warning on the summary. `npx supercov runs latest tests without-evidence`
|
|
115
|
+
lists every such test, with its file, kind and outcome.
|
|
116
|
+
|
|
109
117
|
See [Supported suites](supported-suites.md) for the attribution available from
|
|
110
118
|
each runner.
|
|
111
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -119,14 +119,14 @@
|
|
|
119
119
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
120
120
|
},
|
|
121
121
|
"optionalDependencies": {
|
|
122
|
-
"@supercov/cli-darwin-arm64": "4.0
|
|
123
|
-
"@supercov/cli-darwin-x64": "4.0
|
|
124
|
-
"@supercov/cli-linux-arm64-gnu": "4.0
|
|
125
|
-
"@supercov/cli-linux-arm64-musl": "4.0
|
|
126
|
-
"@supercov/cli-linux-x64-gnu": "4.0
|
|
127
|
-
"@supercov/cli-linux-x64-musl": "4.0
|
|
128
|
-
"@supercov/cli-win32-arm64": "4.0
|
|
129
|
-
"@supercov/cli-win32-x64": "4.0
|
|
122
|
+
"@supercov/cli-darwin-arm64": "4.1.0",
|
|
123
|
+
"@supercov/cli-darwin-x64": "4.1.0",
|
|
124
|
+
"@supercov/cli-linux-arm64-gnu": "4.1.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-musl": "4.1.0",
|
|
126
|
+
"@supercov/cli-linux-x64-gnu": "4.1.0",
|
|
127
|
+
"@supercov/cli-linux-x64-musl": "4.1.0",
|
|
128
|
+
"@supercov/cli-win32-arm64": "4.1.0",
|
|
129
|
+
"@supercov/cli-win32-x64": "4.1.0"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
132
132
|
"@playwright/test": ">=1.55.0",
|
|
@@ -85,6 +85,34 @@ if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1" ||
|
|
|
85
85
|
// and may evaluate before any ESM instrumented file imports it.
|
|
86
86
|
globalThis.__supercovRuntime ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
|
|
87
87
|
}
|
|
88
|
+
// Next.js runs a middleware and a route with `runtime = "edge"` in a VM
|
|
89
|
+
// context of its own, the Edge Runtime's, and that context's global is not
|
|
90
|
+
// this one. An instrumented file found no runtime there: every request to an
|
|
91
|
+
// application with a middleware.ts failed on "Cannot read properties of
|
|
92
|
+
// undefined (reading 'mcdcBegin')", and `next build` failed collecting page
|
|
93
|
+
// data for an edge route. Such a context reads this process's runtime, so
|
|
94
|
+
// what runs there is measured like the rest of the server.
|
|
95
|
+
const virtualMachine = Module._load("node:vm", undefined, false);
|
|
96
|
+
const createContext = virtualMachine.createContext;
|
|
97
|
+
virtualMachine.createContext = function createContextWithRuntime(...parameters) {
|
|
98
|
+
const context = Reflect.apply(createContext, this, parameters);
|
|
99
|
+
if (parameters[1]?.name === "Edge Runtime") {
|
|
100
|
+
try {
|
|
101
|
+
Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
|
|
102
|
+
configurable: true,
|
|
103
|
+
enumerable: false,
|
|
104
|
+
get: () => globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__,
|
|
105
|
+
set(value) {
|
|
106
|
+
Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
|
|
107
|
+
configurable: true, enumerable: false, writable: true, value,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}
|
|
114
|
+
return context;
|
|
115
|
+
};
|
|
88
116
|
// Workers are independent Node processes and an explicit `execArgv: []`
|
|
89
117
|
// otherwise strips the preload that supplies the isolated runtime. Preserve
|
|
90
118
|
// every user option while adding exactly one Supercov import.
|
|
@@ -196,6 +224,32 @@ else if (/\/node_modules\/next\/dist\/compiled\/jest-worker\/processChild\.js$/.
|
|
|
196
224
|
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
197
225
|
};
|
|
198
226
|
}
|
|
227
|
+
// Next.js looks for lockfiles from its directory upwards, takes the outermost
|
|
228
|
+
// as its workspace root, and warns when it finds more than one. The copy
|
|
229
|
+
// holds the project's lockfile and lies inside the project, so every build
|
|
230
|
+
// under Supercov warned twice that the root "may not be correct", naming the
|
|
231
|
+
// copy's lockfile. The root Next picks is the project, which is right: it
|
|
232
|
+
// holds the copy and the dependencies. The copy's lockfile is left out of
|
|
233
|
+
// what the warning counts, and one the project really has twice still warns.
|
|
234
|
+
if (/\/node_modules\/(?:\.bin\/next$|next\/dist\/)/.test(entrypoint)) {
|
|
235
|
+
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
236
|
+
const copies = [...new Set([copy, (() => {
|
|
237
|
+
try {
|
|
238
|
+
return realpathSync(copy);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return copy;
|
|
242
|
+
}
|
|
243
|
+
})()])];
|
|
244
|
+
const declared = "function warnDuplicatedLockFiles(lockFiles) {";
|
|
245
|
+
const compile = Module.prototype._compile;
|
|
246
|
+
Module.prototype._compile = function _compile(content, filename, ...rest) {
|
|
247
|
+
if (typeof content === "string" && /[\\/]next[\\/]dist[\\/]lib[\\/]find-root\.js$/.test(filename)) {
|
|
248
|
+
content = content.replace(declared, `${declared} lockFiles = lockFiles.filter((file, index) => index === lockFiles.length - 1 || !${JSON.stringify(copies)}.some((copy) => file.startsWith(copy)));`);
|
|
249
|
+
}
|
|
250
|
+
return Reflect.apply(compile, this, [content, filename, ...rest]);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
199
253
|
function installAuthoredSourceView() {
|
|
200
254
|
let authored;
|
|
201
255
|
try {
|
|
@@ -886,21 +886,40 @@ function finishAssertionPhase(phase, error) {
|
|
|
886
886
|
phase.error = error instanceof Error ? error.message : String(error);
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
-
|
|
889
|
+
const INSTRUMENTATION_FRAME = /[\\/]\.supercov[\\/](?:node_modules[\\/])?(?:playwright|nodeTest|vitest|runtime|launchSupervisor|nodeAssert|nodeAssertStrict|nodeAssertAdapter|register|resolve-loader)\.(?:js|mjs)(?::|\))/u;
|
|
890
|
+
// A lexical wrapper runs the assertion in a function of its own, so its
|
|
891
|
+
// stack had two frames where the author wrote one: the function Supercov
|
|
892
|
+
// added, at the assertion, and below the runtime's frames the function the
|
|
893
|
+
// author wrote, at the wrapper. Vitest printed both, `:12:35` and `:12:1`.
|
|
894
|
+
// They are one frame again: the author's function, where the assertion is.
|
|
895
|
+
function cleanInstrumentationStack(error, lexical = false) {
|
|
890
896
|
if (!error || typeof error !== "object" || typeof error.stack !== "string")
|
|
891
897
|
return error;
|
|
892
898
|
const lines = error.stack.split("\n");
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
+
const hidden = lines.map((line, index) => index > 0 && INSTRUMENTATION_FRAME.test(line));
|
|
900
|
+
const first = hidden.indexOf(true);
|
|
901
|
+
if (first < 0)
|
|
902
|
+
return error;
|
|
903
|
+
const last = hidden.lastIndexOf(true);
|
|
904
|
+
let visible;
|
|
905
|
+
const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
|
|
906
|
+
const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
|
|
907
|
+
const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
|
|
908
|
+
if (added && written && added[2] === void 0 && !/^node:/.test(written[3])) {
|
|
909
|
+
const merged = written[2] === void 0 ? lines[first - 1] : `${written[1]}${written[2]} (${added[3]})`;
|
|
910
|
+
visible = [...lines.slice(0, first - 1), merged, ...lines.slice(last + 2)];
|
|
911
|
+
} else {
|
|
912
|
+
visible = lines.filter((line, index) => !hidden[index]);
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
error.stack = visible.join("\n");
|
|
916
|
+
} catch (e) {
|
|
899
917
|
}
|
|
900
918
|
return error;
|
|
901
919
|
}
|
|
902
920
|
function withNodeAssertionPhase(operation, source, callback) {
|
|
903
921
|
var _a8;
|
|
922
|
+
const lexical = typeof source === "string";
|
|
904
923
|
const context = currentRequestContext();
|
|
905
924
|
const scope = context.scope;
|
|
906
925
|
if (!scope)
|
|
@@ -944,13 +963,13 @@ function withNodeAssertionPhase(operation, source, callback) {
|
|
|
944
963
|
return value;
|
|
945
964
|
}, (error) => {
|
|
946
965
|
finishAssertionPhase(phase, error);
|
|
947
|
-
throw cleanInstrumentationStack(error);
|
|
966
|
+
throw cleanInstrumentationStack(error, lexical);
|
|
948
967
|
});
|
|
949
968
|
finishAssertionPhase(phase);
|
|
950
969
|
return result;
|
|
951
970
|
} catch (error) {
|
|
952
971
|
finishAssertionPhase(phase, error);
|
|
953
|
-
throw cleanInstrumentationStack(error);
|
|
972
|
+
throw cleanInstrumentationStack(error, lexical);
|
|
954
973
|
}
|
|
955
974
|
}
|
|
956
975
|
// Callee binding preserves receiver, getter/evaluation order, spreads and the
|
|
@@ -1,7 +1,112 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
3
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
4
|
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
4
6
|
import { appendEvidenceRecord } from "./atomic.mjs";
|
|
7
|
+
// The files of the copy Supercov rewrote, by path inside it, and where each
|
|
8
|
+
// is kept as its author wrote it or as a tool of the command changed it.
|
|
9
|
+
const copyRoots = (() => {
|
|
10
|
+
const copy = fileURLToPath(new URL("../../", import.meta.url));
|
|
11
|
+
try {
|
|
12
|
+
return [...new Set([copy, fs.realpathSync(copy)])];
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [copy];
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
let rewritten;
|
|
19
|
+
function rewrittenFile(path) {
|
|
20
|
+
if (rewritten === undefined) {
|
|
21
|
+
try {
|
|
22
|
+
rewritten = new Set(JSON.parse(fs.readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
rewritten = new Set();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
29
|
+
return undefined;
|
|
30
|
+
for (const root of copyRoots) {
|
|
31
|
+
const local = relative(root, path);
|
|
32
|
+
if (local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local)) {
|
|
33
|
+
const file = local.split(sep).join("/");
|
|
34
|
+
return rewritten.has(file) ? file : undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const SOURCE_MAP = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
40
|
+
/**
|
|
41
|
+
* A rewritten file's source map names the project's own file, by absolute
|
|
42
|
+
* path, so a Node stack trace reads as the project's. Vitest prints a frame
|
|
43
|
+
* relative to its root, which is the copy: `../../../../lib/crypto.ts:3:11`
|
|
44
|
+
* where it prints `lib/crypto.ts:3:11` without Supercov, and a file outside
|
|
45
|
+
* its module graph gets no code under the frame. Vite is handed the same
|
|
46
|
+
* code with a map that names the file itself.
|
|
47
|
+
*/
|
|
48
|
+
export function supercovSourceMaps() {
|
|
49
|
+
const { readFile } = fs.promises;
|
|
50
|
+
return {
|
|
51
|
+
name: "supercov:source-maps",
|
|
52
|
+
enforce: "pre",
|
|
53
|
+
async load(id) {
|
|
54
|
+
if (rewrittenFile(id) === undefined)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const code = await readFile(id, "utf8");
|
|
58
|
+
const found = code.lastIndexOf(SOURCE_MAP);
|
|
59
|
+
if (found < 0)
|
|
60
|
+
return null;
|
|
61
|
+
const encoded = code.slice(found + SOURCE_MAP.length).trim();
|
|
62
|
+
if (/\s/.test(encoded))
|
|
63
|
+
return null;
|
|
64
|
+
const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
65
|
+
map.sources = [basename(id)];
|
|
66
|
+
return { code: code.slice(0, found + 1), map };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Vitest reads a file a failure names: to print the code around the nearest
|
|
75
|
+
// frame, and (Vitest 4) to look for a source map in it. In the copy that is
|
|
76
|
+
// the rewritten file. An assertion wrapped into one long line printed no code
|
|
77
|
+
// at all, any other line printed Supercov's, and positions that were already
|
|
78
|
+
// the author's were mapped a second time through the file's own map: a test
|
|
79
|
+
// file's frame at `:12:35` read `:12:0`. What Vitest itself reads is the text
|
|
80
|
+
// the file was rewritten from. What runs is not read here: Vite loads it.
|
|
81
|
+
let readsAuthored = false;
|
|
82
|
+
function readAuthoredFromVitest() {
|
|
83
|
+
if (readsAuthored)
|
|
84
|
+
return;
|
|
85
|
+
readsAuthored = true;
|
|
86
|
+
const { readFileSync, existsSync } = fs;
|
|
87
|
+
const kept = (name, file) => fileURLToPath(new URL(`./${name}/${file}`, import.meta.url));
|
|
88
|
+
const calledByVitest = () => {
|
|
89
|
+
const holder = {};
|
|
90
|
+
const limit = Error.stackTraceLimit;
|
|
91
|
+
try {
|
|
92
|
+
Error.stackTraceLimit = 1;
|
|
93
|
+
Error.captureStackTrace(holder, supercovReadFileSync);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
Error.stackTraceLimit = limit;
|
|
97
|
+
}
|
|
98
|
+
return /[\\/]vitest[\\/]dist[\\/]/.test(String(holder.stack).split("\n")[1] ?? "");
|
|
99
|
+
};
|
|
100
|
+
function supercovReadFileSync(path, ...rest) {
|
|
101
|
+
const file = rewrittenFile(path);
|
|
102
|
+
if (file === undefined || !calledByVitest())
|
|
103
|
+
return Reflect.apply(readFileSync, this, [path, ...rest]);
|
|
104
|
+
const changed = kept(".changed", file);
|
|
105
|
+
return Reflect.apply(readFileSync, this, [existsSync(changed) ? changed : kept(".authored", file), ...rest]);
|
|
106
|
+
}
|
|
107
|
+
fs.readFileSync = supercovReadFileSync;
|
|
108
|
+
syncBuiltinESMExports();
|
|
109
|
+
}
|
|
5
110
|
function sourcePath(moduleId) {
|
|
6
111
|
const absolute = moduleId.startsWith("file:")
|
|
7
112
|
? fileURLToPath(moduleId)
|
|
@@ -29,6 +134,7 @@ export default class SupercovVitestReporter {
|
|
|
29
134
|
}
|
|
30
135
|
onInit(vitest) {
|
|
31
136
|
this.configureProjects?.(vitest.projects);
|
|
137
|
+
readAuthoredFromVitest();
|
|
32
138
|
}
|
|
33
139
|
onBrowserInit(project) {
|
|
34
140
|
this.configureProjects?.([project]);
|