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.
@@ -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 no test that runs it was judged to fail if it changed.
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: a test that runs it
114
- exists, but nothing it asserts depends on what the statement does.
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 5
353
+ npx supercov report --runs 10
353
354
  npx supercov report --output artifacts/supercov-report.html --no-open
354
355
  ```
355
356
 
356
- Use `--runs N` to include up to 20 stored runs. Reports are capped at 24 MB so
357
- they remain suitable for a normal pull-request attachment; use `--runs 1` if a
358
- large project exceeds that target. See [Portable HTML reports](reports.md) for
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.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.0",
122
- "@supercov/cli-darwin-x64": "3.0.0",
123
- "@supercov/cli-linux-arm64-gnu": "3.0.0",
124
- "@supercov/cli-linux-arm64-musl": "3.0.0",
125
- "@supercov/cli-linux-x64-gnu": "3.0.0",
126
- "@supercov/cli-linux-x64-musl": "3.0.0",
127
- "@supercov/cli-win32-arm64": "3.0.0",
128
- "@supercov/cli-win32-x64": "3.0.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
- if (index < 0)
132
- return Reflect.apply(original, this, args);
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
- if (options?.skip || options?.todo || forcedStatus)
159
- writeRunnerEvidence(identity, "skipped", scope, evidenceDirectory);
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
- let status = options?.skip || options?.todo || forcedStatus
167
- ? "skipped"
168
- : "passed";
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
- status = "failed";
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
- status = "failed";
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
- status = "failed";
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 { relative, resolve, sep } from "node:path";
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