gocove

module
v0.0.0-...-a0dceb9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 27, 2026 License: MIT

README

gocove

CI Lint Go Reference Go Report Card

Branch and condition coverage for Go, layered on top of go test -cover.

gocove extends Go's built-in line coverage with branch and condition coverage. Where go test -cover tells you a line ran, gocove tells you which branch of an if, which case of a switch, and which side of a && or || was actually exercised by your tests.

Status

Beta — feature-complete; hardening for v1. The instrumentation pipeline is complete and validated; reports cover condition, block, and branch coverage in text, JSON, and HTML (4a/4c/4d); runs merge across CI shards (4b); --base=REF filters reports to lines changed in a PR (4e); and gocove gate enforces coverage thresholds in CI (4f). The current release line is v0.1.0-beta.* — usable today on Linux/macOS Go ≥ 1.23 module-mode projects; see Limitations for known caveats (Windows not supported; panic-then-os.Exit without cov.* loses observations). See CHANGELOG.md for the per-slice history.

Sub-project Scope Status
1 Foundation: runtime + .gcv/.gcmeta formats ✅ Shipped
2a Cover toolexec interception, catalog discovery ✅ Shipped
2b AST instrumentation passes ✅ Shipped
2c TestMain wrapping + Flush wiring ✅ Shipped
2d User-facing cov helper + diagnose pass ✅ Shipped
2e Implicit runtime injection (importcfg interception) ✅ Shipped
4a Condition coverage report (text) ✅ Shipped
4b Merge: combine multiple meta-dirs ✅ Shipped
4c Block + branch coverage in text report ✅ Shipped
4d JSON + HTML renderers (`--format=json html`)
4e Diff-aware reporting (--base=REF) ✅ Shipped
4f Gate (threshold-based exit code) ✅ Shipped
4g Exclude patterns (--exclude-pkg, --exclude-file) ✅ Shipped

See docs/superpowers/specs/ for full design.

How it works

gocove hooks the Go toolchain via -toolexec. When you run gocove test, gocove:

  1. Intercepts cmd/cover invocations, layering its own instrumentation on top of the cover-rewritten source.
  2. Wraps the auto-generated _testmain.go so coverage observations flush to disk before os.Exit.
  3. Emits .gcmeta (catalog) and .gcv (observations) files under $GOCOVE_META_DIR.

The runtime is small (~150 LOC). The toolexec shim is reversible — without gocove test, your tests run normally.

Install

go install github.com/srvgit/gocove/cmd/gocove@latest

Requires Go 1.23 or later.

go install does two things: it builds the gocove binary into $GOBIN, and it leaves the gocove source in $GOMODCACHE. Both matter — gocove doesn't embed its runtime in the binary; at run time it locates its own version via debug.BuildInfo and reads the runtime source from $GOMODCACHE/github.com/srvgit/gocove@<version>/... to build the toolexec trampoline. A populated, writable module cache is part of the install contract, not a transient download artifact.

Installation through a proxy

Default public proxy (no setup needed). Go's default GOPROXY=https://proxy.golang.org,direct lazy-fetches public GitHub modules on first request — the go install command above just works.

Corporate proxy that mirrors public modules (Athens, JFrog Artifactory, internal Go module proxies):

export GOPROXY=https://goproxy.corp.example.com,direct
go install github.com/srvgit/gocove/cmd/gocove@latest

If your proxy is allow-list based, ask your admin to whitelist github.com/srvgit/gocove.

Internal fork on a private host (git.corp/team/gocove):

export GOPRIVATE=git.corp/*
export GOSUMDB=off                # or point at an internal checksum DB
go install git.corp/team/gocove/cmd/gocove@latest

GOPRIVATE skips the public proxy and the public checksum database for matched paths. Make sure git credentials (SSH key or ~/.netrc) are set so go get can clone it.

Pinning a specific version (recommended for CI):

go install github.com/srvgit/gocove/cmd/gocove@<sha-or-tag>

Until tagged releases ship, @latest resolves to a pseudo-version of main. Pin a SHA in CI for reproducibility.

Install from source (git clone + build)

Useful when you want bleeding-edge main, when go install can't reach the proxy, or when developing on gocove itself.

git clone https://github.com/srvgit/gocove.git
cd gocove

# Install from the local checkout into $GOBIN (or $GOPATH/bin, or $HOME/go/bin
# if neither is set). Make sure that directory is on your $PATH.
go install ./cmd/gocove

# Verify it built — shows "gocove dev (built with go1.23.x or later)".
gocove version

If gocove isn't found after install, run go env GOBIN GOPATH and add the resolved bin directory to your $PATH.

One important env var when running a from-source build: because the binary was compiled from a working tree (no proxy-supplied module info), gocove can't find its runtime sources from $GOMODCACHE. Point it at your checkout:

export GOCOVE_DEV_SOURCE_DIR=/abs/path/to/gocove   # the directory you cloned into

Add the export to your shell profile if you'll keep using the from-source build, or set it in your CI job's environment. Without it, gocove test exits with gocove not resolvable from buildinfo; set GOCOVE_DEV_SOURCE_DIR — that's the signal the trampoline can't locate the runtime source.

Smoke-test:

cd examples/demo
GOCOVE_META_DIR=$PWD/.gocove gocove test ./...
gocove report --meta-dir=$GOCOVE_META_DIR

A go install-ed binary can find its sources without GOCOVE_DEV_SOURCE_DIR because the proxy stores them in $GOMODCACHE under the resolved version; only from-source builds need the override.

Offline / GOPROXY=off — same as "Install from source" above. The clone gives you everything; you don't need network at install time once the repo is on disk.

Note on the cov helper: the basic gocove test ./... flow doesn't need any go.mod change — the runtime is injected via toolexec. But if you import github.com/srvgit/gocove/cov for custom-TestMain packages, your project's go.mod needs require github.com/srvgit/gocove, and that fetch also goes through your GOPROXY. Same allow-listing rules apply.

Try it

A self-contained demo lives at examples/demo/. It's a tiny Go module with deliberately uneven test coverage; its README walks every gocove feature (test, report text/JSON/HTML, merge, gate, diff-aware --base) end-to-end with copy-paste commands.

Usage

Basic — packages without TestMain
gocove test ./...

That's it — no go.mod change required. gocove runs your tests through the normal go test -cover path with extra instrumentation, injecting the runtime archive into compile/link/vet via toolexec. Output:

ok  example.com/foo    0.012s    coverage: 87.4% of statements

gocove: meta dir = /var/folders/.../gocove-meta-XXX

.gcv and .gcmeta files appear under the printed meta dir.

Packages with custom TestMain

If your package has a custom TestMain that calls os.Exit directly, gocove cannot automatically flush observations before exit. Import the cov helper and use one of three patterns. Using cov.* requires require github.com/srvgit/gocove in your go.mod — unlike the basic gocove test ./... flow, cov is referenced in your _test.go source and go list resolves it before any toolexec runs.

import "github.com/srvgit/gocove/cov"

// 1. Drop-in for os.Exit(m.Run())
func TestMain(m *testing.M) { cov.Run(m) }

// 2. When passing m.Run to a wrapper (goleak.VerifyTestMain, etc.)
func TestMain(m *testing.M) {
    framework.EtcdMain(cov.Wrap(m.Run))
}

// 3. Manual control
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    cov.Flush()
    os.Exit(code)
}

If gocove test detects an unhelped TestMain, it emits a build-time warning naming the file and listing these three patterns. The warning is suppressible via --no-warn or GOCOVE_NO_WARN=1.

The cov helpers compile and link normally outside gocove test (they no-op when $GOCOVE_META_DIR is unset), so they're safe to leave in your test files permanently.

Reading observations
export GOCOVE_META_DIR=$PWD/.gocove
gocove test ./...
gocove report --meta-dir=$GOCOVE_META_DIR

Output is a per-package, per-condition table augmented with block + branch coverage:

gocove report
meta-dir: /path/to/.gocove
packages: 1    files: 4    conditions: 23

example.com/foo  (cond: 23,  hit: 18,  full: 2,  cond%: 8.7%,  block%: 50.0%,  branch%: 42.1%)
  shapes.go
    [T+F    ] 42:14    i < n               (block ✓, branch ✓)
    [T only ] 50:23    sum < 1000          (block ✓, branch ✓)
    [F only ] 58:7     x < 0               (block ✓, branch ✗)
    [(never)] 62:7     x > 0               (block ✓, branch ✗)
    ...

TOTAL  cond: 23   hit: 18   full: 2   cond%: 8.7%   block%: 50.0%   branch%: 42.1%

cond% is the strict definition: conditions where both branches were exercised. block% is the percentage of structural blocks (if-bodies, function bodies, etc.) that were entered at least once — sourced from Go's native -coverprofile= output, which gocove test now captures automatically into $GOCOVE_META_DIR/_gocove/cover.profile. branch% is the percentage of decision points where every recorded arm was taken at least once.

Note on branch% — for if x { ... } without an else, gocove records only the then-arm as a branch. A single hit therefore marks that decision point fully-branched, even though the test never exercised the false path. This lower-bounds true branch coverage; cond% remains the strict, complete metric. See spec 4c §10 risk 4 for the workaround a future slice will adopt.

If you have your own -coverprofile= flag in gocove test, gocove keeps your path; gocove report will fall back to skipping block/branch coverage if it can't find the file at the expected location.

Combining runs from multiple test invocations
GOCOVE_META_DIR=$PWD/.gocove-shard1 gocove test ./pkgA/...
GOCOVE_META_DIR=$PWD/.gocove-shard2 gocove test ./pkgB/...
gocove merge --in=$PWD/.gocove-shard1 --in=$PWD/.gocove-shard2 --out=$PWD/.gocove-merged
gocove report --meta-dir=$PWD/.gocove-merged

gocove merge validates that catalogs across inputs share the same fingerprint per import path — if your source has drifted between runs, the merge fails loudly rather than producing nonsense. --force overwrites --out if it already exists; merge refuses to write into a directory that overlaps any --in.

JSON and HTML output
gocove report --meta-dir=$GOCOVE_META_DIR --format=json > coverage.json
gocove report --meta-dir=$GOCOVE_META_DIR --format=html --output=coverage.html
open coverage.html

--format=json emits a stable, versioned schema (schema_version: "4d.1") for CI dashboards and tooling. --format=html emits a single self-contained page with no external CSS/JS — drop it into a build artifact and view it offline. --output=PATH overwrites the file if it exists (matching gofmt -w and go test -coverprofile=). The default --format=text is unchanged.

Diff-aware reporting (PR coverage)
gocove report --meta-dir=$GOCOVE_META_DIR --base=origin/main

--base=REF filters the report to conditions, blocks, and branches whose source lines were added or modified in HEAD vs REF. The diff is computed as git diff REF...HEAD (three-dot — same view GitHub's PR diff shows), so --base=origin/main answers "what fraction of the lines this PR changed are covered?" Works with all three formats. Add --repo=DIR if gocove is run from outside the repository.

For every realistic scenario — PR coverage, release-to-release reports, branch-to-branch comparisons, between-deploy audits, last-N-commits delta, what counts as "good" coverage on new code, and CI patterns for each — see the diff-aware coverage guide.

Enforcing coverage thresholds in CI
gocove gate --meta-dir=$GOCOVE_META_DIR --min-cond=80 --min-block=85
gocove gate --meta-dir=$GOCOVE_META_DIR --base=origin/main --min-cond=100   # PR coverage gate

gocove gate exits 0 if every configured --min-* is met, 1 if any fails. Each --min-* is a percent in [0, 100]; the comparison uses the displayed (one-decimal-rounded) percentage, so a metric printed as 80.0% always passes --min=80. Combine with --base=REF for diff-aware gates that demand high coverage on changed lines without fighting against historical coverage debt. --format=json emits a schema_version: "4f.1" JSON document for CI step parsing. Note: in absolute mode (no --base), an empty meta-dir with any gate set fails — silent-pass on missing observations would defeat the purpose.

Excluding packages and files

gocove report and gocove gate accept --exclude-pkg=PATTERN and --exclude-file=PATTERN to drop generated, vendored, or otherwise uninteresting code from the report and from gate threshold checks. Both flags are repeatable and accept comma-separated values, so all of these forms work:

# Drop a whole package subtree.
gocove report --meta-dir=$GOCOVE_META_DIR --exclude-pkg=example.com/internal/generated/...

# Drop multiple packages, repeated flag.
gocove gate --meta-dir=$GOCOVE_META_DIR --min-cond=80 \
  --exclude-pkg=example.com/internal/legacy \
  --exclude-pkg=example.com/internal/generated/...

# Drop multiple packages, comma-separated.
gocove report --meta-dir=$GOCOVE_META_DIR \
  --exclude-pkg=example.com/internal/legacy,example.com/internal/generated/...

# Drop generated files by basename glob (matches at any depth).
gocove report --meta-dir=$GOCOVE_META_DIR --exclude-file='*_generated.go'

# Drop one specific file in one specific package.
gocove report --meta-dir=$GOCOVE_META_DIR \
  --exclude-file=example.com/internal/db/schema.go

Pattern syntax (Go-idiomatic, no extra dependencies):

Pattern Means
example.com/foo exact import path match
example.com/foo/... this package and every subdirectory (Go's standard ... syntax)
example.com/*/generated one path segment, name "generated" (* does not cross /)
*_generated.go any file whose basename matches (no / in pattern → basename match at any depth)
example.com/internal/db/schema.go exact qualified file path
example.com/internal/*/swagger.go qualified path with one-segment glob

Totals are recomputed over the surviving set. Excluded packages and files leave both numerator and denominator — if a generated file had 60 conditions with 0 fully-covered, excluding it raises cond% by removing the dead weight from both sides. A summary line goes to stderr (report: excluded N package(s), M file(s) by --exclude-pkg/--exclude-file) so a typo'd glob doesn't silently erase your report.

Composes with --base=REF. The diff filter runs first, then exclusions. So --base=origin/main --exclude-file='*_generated.go' --min-cond=80 means "of the non-generated lines this PR changed, at least 80% must be fully condition-covered."

Exclusion is render-time only. gocove test always instruments and records observations for every package — exclusions are applied at report and gate time. This keeps cross-package coverage signal intact (no surprising blind spots) and lets you re-render the same .gcv data with different exclusion sets without rerunning tests.

The internal/gcvfile package's Read(io.Reader) (*File, error) is still available for programmatic access.

Development

git clone https://github.com/srvgit/gocove.git
cd gocove
go test ./... -race

The repository follows a spec-first, plan-driven workflow. See CONTRIBUTING.md for the full process. Architecture and design notes are under docs/superpowers/specs/; detailed implementation plans are under docs/superpowers/plans/.

Limitations

  • Custom TestMain requires the opt-in dep — the basic gocove test ./... flow works without any go.mod change, but if you import github.com/srvgit/gocove/cov in your test source you must add require github.com/srvgit/gocove because go list resolves test imports before any toolexec runs. See docs/superpowers/specs/2026-04-26-gocove-2e-design.md §11.
  • Wrapper-delegation TestMains — when your TestMain calls a third-party helper that takes *testing.M (e.g., goleak.VerifyTestMain(m)) rather than m.Run, you may need to restructure the TestMain to use cov.Wrap(m.Run) or call cov.Flush() manually. See docs/testmain.md (planned) for known wrappers.
  • Panic safetycov.Wrap defers Flush, so observations gathered before a panic in m.Run reach disk. Direct os.Exit(m.Run()) users (no cov.* opt-in) lose observations on panic.
  • Windows — currently Unix-only. Path-separator assumptions in tests need fixing for Windows CI.

Contributing

Contributions welcome. Please read CONTRIBUTING.md before opening a PR — the project uses a spec → plan → subagent-driven implementation flow that is non-standard but produces high-quality, well-reviewed changes.

For bugs and feature requests, open an issue. For security disclosures, see SECURITY.md.

License

MIT. See LICENSE.

Directories

Path Synopsis
cmd
gocove command
Command gocove is the gocove CLI entry point.
Command gocove is the gocove CLI entry point.
Package cov provides drop-in helpers for integrating gocove's branch coverage with user-defined TestMain functions.
Package cov provides drop-in helpers for integrating gocove's branch coverage with user-defined TestMain functions.
internal
buildinfo
Package buildinfo exposes gocove's version string and the Go build info of the running binary.
Package buildinfo exposes gocove's version string and the Go build info of the running binary.
cli
Package cli implements the gocove subcommand dispatcher.
Package cli implements the gocove subcommand dispatcher.
cli/compile
Package compile implements the toolexec interception for the `compile` tool.
Package compile implements the toolexec interception for the `compile` tool.
cli/cover
Package cover implements the gocove toolexec interception of go's cmd/cover invocation.
Package cover implements the gocove toolexec interception of go's cmd/cover invocation.
cli/link
Package link implements the toolexec interception for the `link` tool.
Package link implements the toolexec interception for the `link` tool.
cli/vet
Package vet implements the toolexec interception for the `vet` tool.
Package vet implements the toolexec interception for the `vet` tool.
exclude
Package exclude implements package- and file-level exclusion patterns for gocove report and gate.
Package exclude implements package- and file-level exclusion patterns for gocove report and gate.
gate
Package gate evaluates coverage thresholds for `gocove gate`.
Package gate evaluates coverage thresholds for `gocove gate`.
gcvfile
Package gcvfile encodes and decodes the .gcv sidecar coverage file produced by github.com/srvgit/gocove/runtime at process exit.
Package gcvfile encodes and decodes the .gcv sidecar coverage file produced by github.com/srvgit/gocove/runtime at process exit.
instrument
Package instrument runs the AST passes that turn cover-instrumented source into condition-instrumented source: catalog discovery, condition wrapping, and the cover-vars augmentation that wires up runtime.Register.
Package instrument runs the AST passes that turn cover-instrumented source into condition-instrumented source: catalog discovery, condition wrapping, and the cover-vars augmentation that wires up runtime.Register.
merge
Package merge combines multiple meta-dirs (each produced by an independent `gocove test` run) into a single output meta-dir that `gocove report` can consume.
Package merge combines multiple meta-dirs (each produced by an independent `gocove test` run) into a single output meta-dir that `gocove report` can consume.
meta
Package meta defines the on-disk catalog format (.gcmeta) emitted at instrumentation time and consumed by the merge/report pipeline.
Package meta defines the on-disk catalog format (.gcmeta) emitted at instrumentation time and consumed by the merge/report pipeline.
report
Package report joins .gcmeta catalogs with .gcv observation files into a unified per-condition view, then renders that view in human-readable text.
Package report joins .gcmeta catalogs with .gcv observation files into a unified per-condition view, then renders that view in human-readable text.
runtime_archive
Package runtime_archive ensures pre-built archives of github.com/srvgit/gocove/runtime (and its non-stdlib transitive closure) are available for the toolexec compile/link interception to inject into user builds.
Package runtime_archive ensures pre-built archives of github.com/srvgit/gocove/runtime (and its non-stdlib transitive closure) are available for the toolexec compile/link interception to inject into user builds.
Package runtime is the public ABI surface of gocove.
Package runtime is the public ABI surface of gocove.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL