supercov 0.0.35 → 0.0.37

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 CHANGED
@@ -40,6 +40,7 @@ npx supercov -- cargo test
40
40
  npx supercov -- cargo nextest run
41
41
  npx supercov -- pytest
42
42
  npx supercov -- python -m unittest
43
+ npx supercov -- bundle exec rspec
43
44
  ```
44
45
 
45
46
  ## Give Supercov a job
@@ -101,6 +102,19 @@ From lines and branches to MC/DC, every gap becomes a concrete test target. Supe
101
102
 
102
103
  The denominator comes from source structure before the run, so adding or removing tests cannot silently change what 100% means. Ambiguous source scope, uninstrumented code, and missing evidence remain visible as completeness blockers.
103
104
 
105
+ ## Install for your language
106
+
107
+ The same binary ships through each language's own package manager, at the same version, from one release:
108
+
109
+ ```bash
110
+ npx supercov -- npm test # npm
111
+ uvx --from supercov-cli supercov -- pytest # PyPI
112
+ gem install supercov && supercov -- bundle exec rspec # RubyGems
113
+ cargo install supercov && supercov -- cargo test # crates.io, built from source
114
+ ```
115
+
116
+ `pip install supercov-cli` and `gem install supercov` install a wheel or gem that carries the binary for your platform; nothing is compiled. The crate builds from source and needs Rust 1.95.
117
+
104
118
  ## Supported languages
105
119
 
106
120
  | Language | Status | Start with |
@@ -116,6 +130,18 @@ The denominator comes from source structure before the run, so adding or removin
116
130
 
117
131
  Supercov requires Node.js 22 or newer. Rust support currently uses Rust 1.95; cargo-nextest 0.9.138 and 0.9.140 are supported. Python support requires CPython 3.12 or newer and measures pytest and unittest runs. Ruby support requires Ruby 3.3 or newer (3.4 or newer for full measurement) and measures RSpec, Minitest, test-unit and Cucumber runs.
118
132
 
133
+ ## Supported operating systems and architectures
134
+
135
+ The CLI is a single native binary. `npx supercov` selects the build for the machine it runs on; nothing is compiled during installation.
136
+
137
+ | Operating system | Architectures | Notes |
138
+ | --- | --- | --- |
139
+ | macOS | arm64 (Apple silicon), x64 (Intel) | macOS 11 or newer |
140
+ | Linux | arm64, x64 | glibc 2.28 or newer (Debian 10, Ubuntu 18.10, RHEL 8 and later), or musl (Alpine); chosen automatically |
141
+ | Windows | arm64, x64 | Windows 10 or newer |
142
+
143
+ On Windows, JavaScript and TypeScript suites are verified on every release; Python, Ruby, and Rust suites are not yet verified there. Containers, VMs, and remote executors run the same Linux builds. Every release publishes the same binaries to npm, PyPI (`supercov-cli`, a wheel per platform) and RubyGems (`supercov`, a gem per platform except arm64 Windows, which Ruby has no platform for), and the source to crates.io (`supercov`).
144
+
119
145
  ## Supported test suites
120
146
 
121
147
  Supercov uses exact per-test attribution where an adapter is available. For other supported runners, it reports aggregate structural coverage instead of guessing which test covered a path.
@@ -129,6 +155,9 @@ Supercov uses exact per-test attribution where an adapter is available. For othe
129
155
  | AVA and Mocha | Aggregate structural coverage |
130
156
  | Cargo's standard libtest runner | Exact test and attempt identity |
131
157
  | cargo-nextest | Exact test, attempt, retry, and binary identity |
158
+ | RSpec | Exact example and before/example/after phase identity |
159
+ | Minitest and test-unit | Exact test and setup/test/teardown identity |
160
+ | Cucumber | Exact scenario and hook-phase identity |
132
161
 
133
162
  Supercov works with Vite, Next, Turbopack, Webpack, esbuild, SWC, and projects with no build step. One command can collect evidence from several supported runners into a single run.
134
163
 
@@ -58,7 +58,8 @@ owners are allowed, and everyone else is denied. Branches, decision vectors, and
58
58
  MC/DC expose those missing cases instead of treating one executed line as proof
59
59
  that the decision is safe.
60
60
 
61
- The same principle applies to Rust boolean expressions and control flow.
61
+ The same principle applies to Rust, Python, and Ruby boolean expressions and
62
+ control flow.
62
63
 
63
64
  ## What 100% means
64
65
 
@@ -15,15 +15,22 @@ Supercov supports JavaScript, TypeScript, Rust, Python, and Ruby today.
15
15
 
16
16
  You need:
17
17
 
18
+ - macOS (arm64 or x64), Linux (arm64 or x64, glibc 2.28 or newer or musl), or
19
+ Windows (arm64 or x64; JavaScript and TypeScript suites are verified there
20
+ today);
18
21
  - Node.js 22 or newer;
19
22
  - a test command that already works in the repository; and
20
23
  - for Rust, the Rust 1.95 toolchain;
21
24
  - for Python, CPython 3.12 or newer with pytest or unittest;
22
25
  - for Ruby, Ruby 3.4 or newer with RSpec, Minitest, test-unit or Cucumber (3.3 measures lines, methods and simple branches only).
23
26
 
24
- The CLI is distributed through npm, even for Rust projects. The first `npx`
25
- invocation may download Supercov from the npm registry. Supercov itself does not
26
- upload your source or coverage evidence to a Supercov service.
27
+ The CLI is a native binary. `npx supercov` picks the build for your operating
28
+ system and architecture, and nothing is compiled on install; the same binary is
29
+ on PyPI as `supercov-cli` (`uvx --from supercov-cli supercov`) and on RubyGems
30
+ as `supercov` (`gem install supercov`), at the same version, and the source is
31
+ on crates.io (`cargo install supercov`). The first invocation may download
32
+ Supercov from the registry. Supercov itself does not upload your source or
33
+ coverage evidence to a Supercov service.
27
34
 
28
35
  ## 1. Run your real test command
29
36
 
@@ -63,6 +63,20 @@ TypeScript, and TSX are supported.
63
63
  Supercov instruments an isolated copy. It does not ask you to add an import,
64
64
  reporter, plugin, or alternate build output.
65
65
 
66
+ A suite that runs its own compiled output is built before the tests start.
67
+ Supercov reads the tests: one that imports from `dist/`, or launches a package
68
+ script that does—`spawn("npm", ["run", "start"])`, `execSync("npm run
69
+ start")`, or any launch handed to the shell as one string—means the build is
70
+ part of the run, and a `build` script is run inside the isolated copy first.
71
+ Without it the server under test would never exist. A launch that only names a
72
+ build subcommand, such as `vite build`, is not taken as consuming a build.
73
+
74
+ Instrumented TypeScript is exempt from the host's type policy whether the
75
+ compile is Supercov's own build step or the test command's own `tsc`.
76
+ Instrumentation necessarily rewrites control-flow expressions in ways a type
77
+ checker cannot narrow through, so a project that compiles inside its test
78
+ command builds under measurement exactly as it does without it.
79
+
66
80
  ### Browsers, servers, and child processes
67
81
 
68
82
  Playwright support includes Chromium, Firefox, and WebKit, along with pages,
@@ -82,6 +96,15 @@ a short drain window after the test command finishes so buffered evidence can
82
96
  arrive. Work without a reliable test identity is kept as background coverage
83
97
  instead of being assigned to an arbitrary test.
84
98
 
99
+ A child a test stops in teardown keeps the coverage it produced. Buffered
100
+ evidence is written when a terminating signal arrives—`SIGTERM`, `SIGINT`,
101
+ `SIGHUP`—not only when the process exits on its own, so killing a gateway
102
+ after the request it served does not lose the request. The program's own
103
+ signal handling is untouched: a process with no handler still dies from the
104
+ signal exactly as it would unmeasured, and one with its own handler keeps
105
+ it. `SIGKILL` cannot be caught by anything and is the one stop that loses
106
+ whatever was still buffered.
107
+
85
108
  ## Rust
86
109
 
87
110
  | Runner | Attribution | Current requirement |
@@ -208,6 +231,16 @@ decisions, `||=`, loops, `rescue` flow, a second statement on a line) is
208
231
  declared unmeasured on that interpreter rather than shown as a gap. Ruby 3.4
209
232
  and newer measure everything.
210
233
 
234
+ Ruby reads its own coverage as the interpreter exits, and that shapes what a
235
+ stopped process keeps. A process ended by a signal it can catch—`SIGTERM`
236
+ from a test's teardown, `SIGINT`—unwinds through that exit and reports
237
+ everything it measured. A process killed with `SIGKILL`, or one that leaves
238
+ through `exit!`, never gets there and takes with it whatever it observed since
239
+ its last test boundary. Supercov cannot recover that or say which lines it
240
+ would have been, so the run declares that a process did not report, which
241
+ blocks completeness, rather than counting those lines against the code. Stop a
242
+ Ruby server with `SIGTERM`, or wait for it to exit, and it reports.
243
+
211
244
  ```sh
212
245
  npx supercov -- rspec
213
246
  npx supercov -- bundle exec rspec
@@ -100,6 +100,14 @@ Read the reason in the summary, `scope`, or `gaps` output. Fix a configuration
100
100
  problem when one is named. Otherwise stop the coverage loop and report the
101
101
  limit; do not change application code or add a meaningless test to chase 100%.
102
102
 
103
+ One limit names a cause you can remove. `ruby-process-did-not-report` means a
104
+ Ruby process the run measured ended without reporting: it was killed with
105
+ `SIGKILL`, or left through `exit!`, before the exit where Ruby hands over its
106
+ coverage. Whatever that process observed since its last test boundary is gone,
107
+ and its lines are not shown as uncovered because nothing can say whether they
108
+ ran. Stop the process with `SIGTERM` instead, or wait for it to exit, and the
109
+ limit does not appear.
110
+
103
111
  ## Coverage is aggregate instead of per test
104
112
 
105
113
  Aggregate coverage still shows which source ran, but Supercov cannot truthfully
@@ -68,12 +68,15 @@ aggregate instead of being assigned to whichever test happened to be nearby.
68
68
 
69
69
  Release checks cover program behavior before and after instrumentation, line and
70
70
  branch results, decision vectors and MC/DC, supported JavaScript, TypeScript,
71
- and Rust runner contracts, Chromium, Firefox, and WebKit execution, clean
72
- installation, interrupted-run recovery, and isolated publication.
71
+ Rust, Python, and Ruby runner contracts, Chromium, Firefox, and WebKit
72
+ execution, clean installation, interrupted-run recovery, and isolated
73
+ publication.
73
74
 
74
75
  JavaScript behavior is exercised against a pinned TC39 Test262 corpus. MC/DC
75
76
  cases are also compared with an independent LLVM implementation so a
76
- self-consistent calculation error does not pass unnoticed.
77
+ self-consistent calculation error does not pass unnoticed. Python and Ruby
78
+ gates run real suites through their supported runners and assert the resulting
79
+ coverage totals, test identity, and measurement limits.
77
80
 
78
81
  These checks reduce risk; they do not replace reviewing the assertions and
79
82
  behavior protected by a new test.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.35",
4
- "description": "Zero-edit, runner-aware coverage completeness for JavaScript, TypeScript, Rust, and Python test suites",
3
+ "version": "0.0.37",
4
+ "description": "Zero-edit, runner-aware coverage completeness for JavaScript, TypeScript, Rust, Python, and Ruby test suites",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/supercorp-ai/supercov.git"
9
9
  },
10
- "homepage": "https://github.com/supercorp-ai/supercov#readme",
10
+ "homepage": "https://supercov.com",
11
11
  "bugs": {
12
12
  "url": "https://github.com/supercorp-ai/supercov/issues"
13
13
  },
@@ -65,6 +65,7 @@
65
65
  "benchmark:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-benchmark.mjs",
66
66
  "check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && node scripts/package-preflight.mjs",
67
67
  "release:check": "npm run check && npm run test:engine && npm run test:fixture && npm run test:watchdog && npm run test:engine-contract && npm run test:agent && npm run test:child-attribution && npm run test:host-loader && npm run test:python-monitoring && npm run test:ruby-coverage && npm run test:packed-npx && npm run test:clang-mcdc && npm run benchmark:check",
68
+ "release:bump": "node scripts/bump-version.mjs",
68
69
  "prepack": "node scripts/package-preflight.mjs",
69
70
  "prepublishOnly": "npm run release:check",
70
71
  "test:child-attribution": "cargo build -p supercov && node scripts/rust-child-attribution-integration.mjs",
@@ -73,12 +74,14 @@
73
74
  "test:ruby-coverage": "cargo build -p supercov && node scripts/ruby-coverage-integration.mjs"
74
75
  },
75
76
  "optionalDependencies": {
76
- "@supercov/cli-darwin-arm64": "0.0.35",
77
- "@supercov/cli-darwin-x64": "0.0.35",
78
- "@supercov/cli-linux-arm64-gnu": "0.0.35",
79
- "@supercov/cli-linux-arm64-musl": "0.0.35",
80
- "@supercov/cli-linux-x64-gnu": "0.0.35",
81
- "@supercov/cli-linux-x64-musl": "0.0.35"
77
+ "@supercov/cli-darwin-arm64": "0.0.37",
78
+ "@supercov/cli-darwin-x64": "0.0.37",
79
+ "@supercov/cli-linux-arm64-gnu": "0.0.37",
80
+ "@supercov/cli-linux-arm64-musl": "0.0.37",
81
+ "@supercov/cli-linux-x64-gnu": "0.0.37",
82
+ "@supercov/cli-linux-x64-musl": "0.0.37",
83
+ "@supercov/cli-win32-arm64": "0.0.37",
84
+ "@supercov/cli-win32-x64": "0.0.37"
82
85
  },
83
86
  "peerDependencies": {
84
87
  "@playwright/test": ">=1.55.0",
@@ -13,6 +13,7 @@ import http from "node:http";
13
13
  import https from "node:https";
14
14
  import { syncBuiltinESMExports } from "node:module";
15
15
  import { relative, resolve, sep } from "node:path";
16
+ import { pathToFileURL } from "node:url";
16
17
  import * as standardPlaywright from "@playwright/test";
17
18
  import * as coverageRuntime from "./runtime.mjs";
18
19
  import { inferTestProvenance } from "./provenance.mjs";
@@ -147,8 +148,11 @@ function callerSource() {
147
148
  .replace(/\/$/, "");
148
149
  if (!projectRoot)
149
150
  return normalized;
151
+ // A stack frame's URL is what Node's pathToFileURL produces -- on Windows
152
+ // `file:///C:/...`, never a hand-built `file://C:/...` -- so strip the
153
+ // prefix derived the same way, then the bare path for frames without one.
150
154
  return normalized
151
- .replace(`file://${projectRoot}/`, "")
155
+ .replace(`${pathToFileURL(projectRoot).href}/`, "")
152
156
  .replace(`${projectRoot}/`, "");
153
157
  }
154
158
  class CoveragePhaseController {
@@ -168,9 +172,11 @@ class CoveragePhaseController {
168
172
  scriptUpdate = Promise.resolve();
169
173
  proxyCache = new WeakMap();
170
174
  runtimeSnapshots;
171
- // Contexts this controller found rather than created: it does not know
175
+ // Contexts whose creation the collector did not observe: it does not know
172
176
  // what `extraHTTPHeaders` their owner configured, so it must not rewrite
173
177
  // them (the scope cookie and the fetch patch still carry attribution).
178
+ // A worker-scoped context launched through a patched BrowserType is found
179
+ // later but is not opaque: its original headers were captured at launch.
174
180
  adoptedContexts = new Set();
175
181
  // Listeners installed on contexts that outlive this test, removed on
176
182
  // dispose so a worker-scoped context does not keep registering pages with
@@ -257,8 +263,10 @@ class CoveragePhaseController {
257
263
  if (this.disposed)
258
264
  return;
259
265
  for (const context of liveTrackedContexts()) {
260
- if (!this.contexts.has(context))
261
- await this.registerContext(context, this.configuredHeaders, { adopted: true }).catch(() => undefined);
266
+ if (!this.contexts.has(context)) {
267
+ const headersKnown = trackedContextConfiguredHeaders.has(context);
268
+ await this.registerContext(context, trackedContextConfiguredHeaders.get(context) ?? this.configuredHeaders, { adopted: !headersKnown }).catch(() => undefined);
269
+ }
262
270
  }
263
271
  }
264
272
  async registerPage(page) {
@@ -266,8 +274,11 @@ class CoveragePhaseController {
266
274
  return;
267
275
  timingCount("registerPage");
268
276
  this.pages.add(page);
269
- await this.registerContext(page.context(), this.configuredHeaders, {
270
- adopted: !this.contexts.has(page.context()) && liveTrackedContexts().has(page.context()),
277
+ const context = page.context();
278
+ const tracked = liveTrackedContexts().has(context);
279
+ const headersKnown = trackedContextConfiguredHeaders.has(context);
280
+ await this.registerContext(context, trackedContextConfiguredHeaders.get(context) ?? this.configuredHeaders, {
281
+ adopted: !this.contexts.has(context) && tracked && !headersKnown,
271
282
  });
272
283
  const cdp = await page.context().newCDPSession(page).catch(() => undefined);
273
284
  if (cdp)
@@ -399,6 +410,14 @@ class CoveragePhaseController {
399
410
  this.disposed = true;
400
411
  await Promise.all([...this.pendingRegistrations]);
401
412
  await this.scriptUpdate;
413
+ // A worker-scoped context survives this test. Remove its test scope
414
+ // while preserving the exact headers supplied by the suite so work
415
+ // between tests cannot be charged to the attempt that just ended.
416
+ await Promise.all([...this.contexts]
417
+ .filter((context) => !this.adoptedContexts.has(context))
418
+ .map((context) => context
419
+ .setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? {})
420
+ .catch(() => undefined)));
402
421
  for (const [context, listeners] of this.contextListeners) {
403
422
  for (const [event, listener] of listeners)
404
423
  context.off?.(event, listener);
@@ -505,9 +524,10 @@ class CoveragePhaseController {
505
524
  trackBrowser(result);
506
525
  if (typeof candidate["pages"] === "function" &&
507
526
  typeof candidate["route"] === "function") {
508
- trackContext(result);
509
- await this.registerContext(result, (sourceArgs?.[0]
510
- ?.extraHTTPHeaders ?? this.configuredHeaders));
527
+ const configuredHeaders = sourceArgs?.[0]
528
+ ?.extraHTTPHeaders ?? this.configuredHeaders;
529
+ trackContext(result, configuredHeaders);
530
+ await this.registerContext(result, configuredHeaders);
511
531
  }
512
532
  if (typeof candidate["frames"] === "function" &&
513
533
  typeof candidate["context"] === "function")
@@ -630,6 +650,10 @@ const controllers = new Map();
630
650
  // launches are recorded here, and each test's controller adopts what is live.
631
651
  const trackedBrowsers = new Set();
632
652
  const trackedContexts = new Set();
653
+ // Creation-time headers for contexts launched through patched Playwright
654
+ // methods. Capturing an explicit empty object matters: it proves replacing
655
+ // the headers later is safe, unlike a context discovered only by enumeration.
656
+ const trackedContextConfiguredHeaders = new WeakMap();
633
657
  const patchedPrototypes = new WeakSet();
634
658
  function trackBrowser(browser) {
635
659
  if (!browser || typeof browser !== "object" || trackedBrowsers.has(browser))
@@ -638,8 +662,12 @@ function trackBrowser(browser) {
638
662
  browser.once?.("disconnected", () => trackedBrowsers.delete(browser));
639
663
  patchBrowserPrototype(browser);
640
664
  }
641
- function trackContext(context) {
642
- if (!context || typeof context !== "object" || trackedContexts.has(context))
665
+ function trackContext(context, configuredHeaders) {
666
+ if (!context || typeof context !== "object")
667
+ return;
668
+ if (configuredHeaders !== undefined)
669
+ trackedContextConfiguredHeaders.set(context, configuredHeaders);
670
+ if (trackedContexts.has(context))
643
671
  return;
644
672
  trackedContexts.add(context);
645
673
  context.once?.("close", () => trackedContexts.delete(context));
@@ -685,10 +713,11 @@ function patchBrowserPrototype(browser) {
685
713
  patchedPrototypes.add(prototype);
686
714
  patchOnce(browser, "newContext", (original) => async function (...args) {
687
715
  const context = await original.apply(this, args);
688
- trackContext(context);
716
+ const configuredHeaders = args[0]?.extraHTTPHeaders ?? {};
717
+ trackContext(context, configuredHeaders);
689
718
  const controller = activeController;
690
719
  if (controller && !controller.contexts.has(context))
691
- await controller.registerContext(context, args[0]?.extraHTTPHeaders ?? controller.configuredHeaders, { adopted: true }).catch(() => undefined);
720
+ await controller.registerContext(context, configuredHeaders).catch(() => undefined);
692
721
  return context;
693
722
  });
694
723
  }
@@ -734,10 +763,11 @@ function installBrowserLaunchTracking() {
734
763
  }
735
764
  patchOnce(browserType, "launchPersistentContext", (original) => async function (...args) {
736
765
  const context = await original.apply(this, args);
737
- trackContext(context);
766
+ const configuredHeaders = args[1]?.extraHTTPHeaders ?? {};
767
+ trackContext(context, configuredHeaders);
738
768
  const controller = activeController;
739
769
  if (controller)
740
- await controller.registerContext(context, args[1]?.extraHTTPHeaders ?? controller.configuredHeaders, { adopted: true }).catch(() => undefined);
770
+ await controller.registerContext(context, configuredHeaders).catch(() => undefined);
741
771
  return context;
742
772
  });
743
773
  }
@@ -18,8 +18,10 @@ function belongsToProject(parentURL) {
18
18
  return false;
19
19
  if (!PROJECT_ROOT)
20
20
  return parentURL.includes("/tests/");
21
- const normalizedRoot = PROJECT_ROOT.replaceAll("\\", "/").replace(/\/$/, "");
22
- const projectURL = `file://${normalizedRoot}/`;
21
+ // Derive the URL the way Node derives parentURL, or the two never match on
22
+ // Windows: a hand-built `file://C:/...` is not the `file:///C:/...` that
23
+ // pathToFileURL produces, and every project module would read as foreign.
24
+ const projectURL = pathToFileURL(PROJECT_ROOT).href.replace(/\/?$/, "/");
23
25
  const generatedURL = `${projectURL}.supercov/`;
24
26
  return (parentURL.startsWith(projectURL) && !parentURL.startsWith(generatedURL));
25
27
  }