supercov 3.0.0 → 3.0.2
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/assertions.md
CHANGED
|
@@ -43,8 +43,13 @@ times out, or the process crashes. The change depends on the statement:
|
|
|
43
43
|
| a `when`, `switch`, `try`, `loop` or Kotlin `throw` a function ends on | every value it returns becomes that stand-in (skipping it would leave the function nothing to return) |
|
|
44
44
|
| declaration or assignment with a value | the value becomes that stand-in, without being evaluated |
|
|
45
45
|
| JSX expression | the value becomes undefined |
|
|
46
|
+
| `return true`/`false`, or a declaration of `true`/`false` (JavaScript, TypeScript, Python) | the boolean becomes the other one: `false` becoming `undefined` changes nothing a truthiness check reads |
|
|
47
|
+
| `return undefined` or `return None` | skipped, since returning the stand-in would be the same code |
|
|
48
|
+
| `throw` in TypeScript | throws a bare `new Error()` instead: deleting a throw seldom compiles, because it usually guards the narrowing the next line relies on |
|
|
46
49
|
| anything else | skipped: the statement does not run |
|
|
47
50
|
|
|
51
|
+
A constructor's `super(...)` is not assessed: skipping it does not compile.
|
|
52
|
+
|
|
48
53
|
The stand-in is `undefined` in JavaScript and TypeScript, `None` in Python and
|
|
49
54
|
`nil` in Ruby. Go and the JVM languages replace a value after it is computed,
|
|
50
55
|
as their mutation testers do: the zero value in Go, and `null`, `0` or `false`
|
|
@@ -96,7 +101,7 @@ npx supercov runs latest assertions --test "closes the stream" # what one test
|
|
|
96
101
|
```text
|
|
97
102
|
Run run_4f2a: 93.8% asserted (2731 of 2913 executed statements)
|
|
98
103
|
|
|
99
|
-
Files, most statements not asserted first: a statement is not asserted when
|
|
104
|
+
Files, most statements not asserted first: a statement is not asserted when none of the tests asked about it was judged to fail if it changed. Up to 15 of the tests that run a statement are asked, so a test never asked can still catch it.
|
|
100
105
|
NOT ASSERTED ASSERTED FILE
|
|
101
106
|
41 388/429 src/adapters.ts
|
|
102
107
|
...
|
|
@@ -110,8 +115,13 @@ down, so an agent reads only as much as it needs. `runs latest file` and
|
|
|
110
115
|
`runs latest line` show the same verdicts beside the coverage, and
|
|
111
116
|
`npx supercov runs latest` and the HTML report show the same share.
|
|
112
117
|
|
|
113
|
-
A statement that is not asserted is a place to add a check:
|
|
114
|
-
|
|
118
|
+
A statement that is not asserted is a place to add a check: tests run it, but
|
|
119
|
+
nothing the asked tests assert depends on what the statement does. Five of the
|
|
120
|
+
tests that run a statement are asked first, and up to 15 when none of those
|
|
121
|
+
catches it: tests whose file quotes text the statement writes (a log line, a
|
|
122
|
+
message), then tests named like the source file, then one per test file and
|
|
123
|
+
describe block. A statement many tests run can still be caught by one that was
|
|
124
|
+
never asked; the statement view (`assertions <file>:<line>`) says how many were.
|
|
115
125
|
|
|
116
126
|
## Which tests check a change
|
|
117
127
|
|
|
@@ -130,6 +140,10 @@ test or its helpers, or to which tests run it, asks again. A dependency or
|
|
|
130
140
|
configuration change asks everything again. After a typical commit only a few
|
|
131
141
|
dozen questions are asked; the rest are reused.
|
|
132
142
|
|
|
143
|
+
A reused answer is the one Jev gave before, right or wrong. When you have shown
|
|
144
|
+
an answer wrong, `assess --refresh` asks every question again and keeps the new
|
|
145
|
+
answers.
|
|
146
|
+
|
|
133
147
|
## Cost, speed and accuracy
|
|
134
148
|
|
|
135
149
|
- **Cost:** about 0.006 cents per statement for a first assessment: 14 projects
|
package/docs/cli.md
CHANGED
|
@@ -182,6 +182,7 @@ after source changes.
|
|
|
182
182
|
```sh supercov-example
|
|
183
183
|
npx supercov runs <run-id> assertions assess --dry-run
|
|
184
184
|
npx supercov runs <run-id> assertions assess
|
|
185
|
+
npx supercov runs <run-id> assertions assess --refresh # ask again instead of reusing saved answers
|
|
185
186
|
npx supercov runs <run-id> assertions
|
|
186
187
|
npx supercov runs <run-id> assertions --all --json
|
|
187
188
|
```
|
|
@@ -349,13 +350,13 @@ up to ten recent runs. Selecting a run compares it with the previous saved run
|
|
|
349
350
|
automatically.
|
|
350
351
|
|
|
351
352
|
```sh
|
|
352
|
-
npx supercov report --runs
|
|
353
|
+
npx supercov report --runs 10
|
|
353
354
|
npx supercov report --output artifacts/supercov-report.html --no-open
|
|
354
355
|
```
|
|
355
356
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
357
|
+
A report includes the five newest runs; `--runs N` includes up to 20. A large
|
|
358
|
+
project's report can pass GitHub's 25 MB attachment limit; `--runs 1` makes
|
|
359
|
+
the smallest. See [Portable HTML reports](reports.md) for
|
|
359
360
|
the privacy and source-integrity rules.
|
|
360
361
|
|
|
361
362
|
## Find a smaller test set
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Coverage, security and code quality for coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coverage",
|
|
@@ -118,14 +118,14 @@
|
|
|
118
118
|
"test:windows-tls": "node scripts/windows-tls-test.mjs"
|
|
119
119
|
},
|
|
120
120
|
"optionalDependencies": {
|
|
121
|
-
"@supercov/cli-darwin-arm64": "3.0.
|
|
122
|
-
"@supercov/cli-darwin-x64": "3.0.
|
|
123
|
-
"@supercov/cli-linux-arm64-gnu": "3.0.
|
|
124
|
-
"@supercov/cli-linux-arm64-musl": "3.0.
|
|
125
|
-
"@supercov/cli-linux-x64-gnu": "3.0.
|
|
126
|
-
"@supercov/cli-linux-x64-musl": "3.0.
|
|
127
|
-
"@supercov/cli-win32-arm64": "3.0.
|
|
128
|
-
"@supercov/cli-win32-x64": "3.0.
|
|
121
|
+
"@supercov/cli-darwin-arm64": "3.0.2",
|
|
122
|
+
"@supercov/cli-darwin-x64": "3.0.2",
|
|
123
|
+
"@supercov/cli-linux-arm64-gnu": "3.0.2",
|
|
124
|
+
"@supercov/cli-linux-arm64-musl": "3.0.2",
|
|
125
|
+
"@supercov/cli-linux-x64-gnu": "3.0.2",
|
|
126
|
+
"@supercov/cli-linux-x64-musl": "3.0.2",
|
|
127
|
+
"@supercov/cli-win32-arm64": "3.0.2",
|
|
128
|
+
"@supercov/cli-win32-x64": "3.0.2"
|
|
129
129
|
},
|
|
130
130
|
"peerDependencies": {
|
|
131
131
|
"@playwright/test": ">=1.55.0",
|
|
@@ -93,19 +93,30 @@ function restoreUserError(error, depth = 0) {
|
|
|
93
93
|
// gives what --test-name-pattern matches. A store rather than a stack: an
|
|
94
94
|
// async suite registers tests after it awaits.
|
|
95
95
|
const suitePath = new AsyncLocalStorage();
|
|
96
|
+
// A suite marked todo or skip marks every test inside it, as node:test counts
|
|
97
|
+
// them: `describe.todo` tests run, and are todo whatever they do.
|
|
98
|
+
const suiteMark = new AsyncLocalStorage();
|
|
99
|
+
function markOf(options, mark) {
|
|
100
|
+
if (mark === "skipped" || options?.skip)
|
|
101
|
+
return "skipped";
|
|
102
|
+
if (mark === "todo" || options?.todo)
|
|
103
|
+
return "todo";
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
96
106
|
function suiteName(args, callback) {
|
|
97
107
|
return typeof args[0] === "string" ? args[0] : callback.name || "<anonymous>";
|
|
98
108
|
}
|
|
99
|
-
function wrappedSuite(original) {
|
|
109
|
+
function wrappedSuite(original, mark) {
|
|
100
110
|
const wrapped = function supercovNodeSuite(...args) {
|
|
101
111
|
const index = callbackIndex(args);
|
|
102
112
|
if (index < 0)
|
|
103
113
|
return Reflect.apply(original, this, args);
|
|
104
114
|
const callback = args[index];
|
|
105
115
|
const path = [...(suitePath.getStore() ?? []), suiteName(args, callback)];
|
|
116
|
+
const marked = suiteMark.getStore() ?? markOf(testOptions(args, index), mark);
|
|
106
117
|
const next = [...args];
|
|
107
118
|
next[index] = function supercovNodeSuiteCallback(...callbackArgs) {
|
|
108
|
-
return suitePath.run(path, () => Reflect.apply(callback, this, callbackArgs));
|
|
119
|
+
return suitePath.run(path, () => suiteMark.run(marked, () => Reflect.apply(callback, this, callbackArgs)));
|
|
109
120
|
};
|
|
110
121
|
// node:test reports a suite at its direct caller, as it does a test.
|
|
111
122
|
const registration = registrationAt(callerLocation(supercovNodeSuite));
|
|
@@ -119,7 +130,7 @@ function wrappedSuite(original) {
|
|
|
119
130
|
Object.defineProperty(wrapped, property, {
|
|
120
131
|
configurable: true,
|
|
121
132
|
enumerable: true,
|
|
122
|
-
value: wrappedSuite(member),
|
|
133
|
+
value: wrappedSuite(member, property === "only" ? undefined : property === "skip" ? "skipped" : "todo"),
|
|
123
134
|
});
|
|
124
135
|
}
|
|
125
136
|
return wrapped;
|
|
@@ -128,9 +139,9 @@ const registrationCounts = new Map();
|
|
|
128
139
|
function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
129
140
|
const wrapped = function supercovNodeTest(...args) {
|
|
130
141
|
const index = callbackIndex(args);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const callback = args[index];
|
|
142
|
+
// `test.todo("name")` and `it.todo("name")` have no body and are still
|
|
143
|
+
// tests the runner counts.
|
|
144
|
+
const callback = index < 0 ? {} : args[index];
|
|
134
145
|
const location = callerLocation(supercovNodeTest);
|
|
135
146
|
const identity = {
|
|
136
147
|
runner: "node:test",
|
|
@@ -153,19 +164,27 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
153
164
|
registrationCounts.set(registrationKey, registrationOrdinal + 1);
|
|
154
165
|
identity.registrationOrdinal = registrationOrdinal;
|
|
155
166
|
const scope = runnerExecutionScope(identity);
|
|
156
|
-
const options = testOptions(args, index);
|
|
167
|
+
const options = testOptions(args, index < 0 ? args.length : index);
|
|
157
168
|
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
const marked = suiteMark.getStore() ?? markOf(options, forcedStatus);
|
|
170
|
+
if (index < 0) {
|
|
171
|
+
writeRunnerEvidence(identity, marked ?? "passed", scope, evidenceDirectory);
|
|
172
|
+
return Reflect.apply(original, this, args);
|
|
173
|
+
}
|
|
174
|
+
if (marked)
|
|
175
|
+
writeRunnerEvidence(identity, marked, scope, evidenceDirectory);
|
|
160
176
|
const next = [...args];
|
|
161
177
|
const execute = (callbackThis, context, done) => {
|
|
162
178
|
// A test or its hooks may intentionally modify Supercov's public
|
|
163
179
|
// environment while testing integrations. Keep this attempt's transport
|
|
164
180
|
// destination fixed to the value present when the test was registered.
|
|
165
181
|
beginBufferedServerEvidence(scope);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
// A todo test runs, and stays todo whether its body passes or fails.
|
|
183
|
+
let status = marked ?? "passed";
|
|
184
|
+
const fail = () => {
|
|
185
|
+
if (status !== "todo")
|
|
186
|
+
status = "failed";
|
|
187
|
+
};
|
|
169
188
|
const contextProxy = new Proxy(context, {
|
|
170
189
|
get(target, property) {
|
|
171
190
|
// Read with the real context as receiver: TestContext accessors
|
|
@@ -174,7 +193,7 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
174
193
|
const value = Reflect.get(target, property, target);
|
|
175
194
|
if ((property === "skip" || property === "todo") && typeof value === "function") {
|
|
176
195
|
return (...callArgs) => {
|
|
177
|
-
status = "skipped";
|
|
196
|
+
status = property === "todo" ? "todo" : "skipped";
|
|
178
197
|
return Reflect.apply(value, target, callArgs);
|
|
179
198
|
};
|
|
180
199
|
}
|
|
@@ -202,7 +221,7 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
202
221
|
if (callback.length >= 2) {
|
|
203
222
|
const callbackDone = (error) => {
|
|
204
223
|
if (error) {
|
|
205
|
-
|
|
224
|
+
fail();
|
|
206
225
|
restoreUserError(error);
|
|
207
226
|
}
|
|
208
227
|
finishBody();
|
|
@@ -216,7 +235,7 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
216
235
|
finishBody();
|
|
217
236
|
return value;
|
|
218
237
|
}, (error) => {
|
|
219
|
-
|
|
238
|
+
fail();
|
|
220
239
|
finishBody();
|
|
221
240
|
throw restoreUserError(error);
|
|
222
241
|
});
|
|
@@ -224,7 +243,7 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
224
243
|
return result;
|
|
225
244
|
}
|
|
226
245
|
catch (error) {
|
|
227
|
-
|
|
246
|
+
fail();
|
|
228
247
|
finishBody();
|
|
229
248
|
throw restoreUserError(error);
|
|
230
249
|
}
|
|
@@ -249,7 +268,7 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
|
|
|
249
268
|
Object.defineProperty(wrapped, property, {
|
|
250
269
|
configurable: true,
|
|
251
270
|
enumerable: true,
|
|
252
|
-
value: wrappedRegistration(member, parentTestId, property === "only" ? undefined : "skipped"),
|
|
271
|
+
value: wrappedRegistration(member, parentTestId, property === "only" ? undefined : property === "skip" ? "skipped" : "todo"),
|
|
253
272
|
});
|
|
254
273
|
}
|
|
255
274
|
return wrapped;
|
|
@@ -1,13 +1,77 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
4
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
3
5
|
import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
|
|
4
6
|
const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
|
|
5
7
|
const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}`)
|
|
6
8
|
.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
9
|
+
// Playwright's own code frame builder, from the copy that runs the tests.
|
|
10
|
+
let frameBuilder;
|
|
11
|
+
function codeFrame() {
|
|
12
|
+
if (frameBuilder !== undefined)
|
|
13
|
+
return frameBuilder;
|
|
14
|
+
try {
|
|
15
|
+
const require = createRequire(join(process.cwd(), "noop.js"));
|
|
16
|
+
const root = dirname(require.resolve("playwright/package.json"));
|
|
17
|
+
frameBuilder = require(join(root, "lib/transform/babelBundle.js")).codeFrameColumns ?? null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
frameBuilder = null;
|
|
21
|
+
}
|
|
22
|
+
return frameBuilder;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Playwright prints the failing line from the file it ran: Supercov's copy in
|
|
26
|
+
* the workspace, where assertions carry the wrappers that time them. Show the
|
|
27
|
+
* project's own line instead. Every line keeps its number in the copy.
|
|
28
|
+
*/
|
|
29
|
+
export function originalSnippet(error, workspace = process.env.SUPERCOV_PROJECT_ROOT, source = process.env.SUPERCOV_SOURCE_PROJECT_ROOT, build = codeFrame()) {
|
|
30
|
+
const location = error?.location;
|
|
31
|
+
if (!error?.snippet || !location?.file || !workspace || !source || !build)
|
|
32
|
+
return;
|
|
33
|
+
const path = relative(workspace, location.file);
|
|
34
|
+
if (!path || path.startsWith("..") || isAbsolute(path))
|
|
35
|
+
return;
|
|
36
|
+
let copied;
|
|
37
|
+
let original;
|
|
38
|
+
try {
|
|
39
|
+
copied = readFileSync(location.file, "utf8");
|
|
40
|
+
original = readFileSync(join(source, path), "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (copied === original)
|
|
46
|
+
return;
|
|
47
|
+
const originalLine = original.split("\n")[location.line - 1];
|
|
48
|
+
if (originalLine === undefined)
|
|
49
|
+
return;
|
|
50
|
+
// Playwright reads the copy's inline source map, so the location is the
|
|
51
|
+
// original line. The map has one point per copied run of text, so the
|
|
52
|
+
// column is the start of the statement; Playwright points at the matcher,
|
|
53
|
+
// which the message names (`expect(received).toBe(expected)`).
|
|
54
|
+
let column = location.column <= originalLine.length + 1
|
|
55
|
+
? location.column
|
|
56
|
+
: originalLine.length - originalLine.trimStart().length + 1;
|
|
57
|
+
const matcher = /expect\(.*?\)\.(?:not\.|resolves\.|rejects\.|soft\.|poll\.)*(\w+)\(/.exec(String(error.message ?? "").replace(/\u001b\[[0-9;]*m/g, ""))?.[1];
|
|
58
|
+
const named = matcher ? originalLine.indexOf(`.${matcher}(`, column - 1) : -1;
|
|
59
|
+
if (named >= 0)
|
|
60
|
+
column = named + 2;
|
|
61
|
+
const start = { line: location.line, column };
|
|
62
|
+
const shown = build(copied, { start: location }, { highlightCode: true });
|
|
63
|
+
if (!error.snippet.includes(shown))
|
|
64
|
+
return;
|
|
65
|
+
error.snippet = error.snippet.replace(shown, build(original, { start }, { highlightCode: true }));
|
|
66
|
+
}
|
|
7
67
|
/** Records outcomes even when browser or fixture startup fails before coverage. */
|
|
8
68
|
export default class SupercovPlaywrightReporter {
|
|
9
69
|
records = [];
|
|
10
70
|
onTestEnd(test, result) {
|
|
71
|
+
// Reporters print failures at the end of the run, after every
|
|
72
|
+
// reporter has seen this test.
|
|
73
|
+
for (const error of result.errors ?? [])
|
|
74
|
+
originalSnippet(error);
|
|
11
75
|
const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"] ??
|
|
12
76
|
(GENERATED_EVIDENCE_DIRECTORY.startsWith("__")
|
|
13
77
|
? undefined
|