Skip to content

the-hotels-network/covlens

Repository files navigation

covlens

Go coverage tool that runs tests only on packages you changed and validates thresholds — with a modern HTML report.

TL;DR

go install github.com/the-hotels-network/covlens/cmd/covlens@latest
covlens          # run from any Go repo; checks diff + total coverage, opens HTML report
  • Diff coverage — measures only the lines you changed, not the whole file
  • Total coverage — validates an overall floor for the whole project
  • HTML report — self-contained, dark mode, syntax-highlighted, jump-to-uncovered
  • JSON sidecar — machine-readable output for CI tooling (no HTML scraping needed)
  • Exits 1 when a threshold fails — CI-friendly out of the box

Table of contents


What it does

  1. Finds all .go files changed relative to your base branch (committed, staged, and untracked)
  2. Runs go test -short -coverprofile on those packages (tests guarded by testing.Short() are skipped — see Test selection)
  3. Computes coverage for the changed lines only (not the whole file)
  4. Validates two thresholds and exits 1 if either fails — CI-friendly
  5. Generates a self-contained HTML report with syntax highlighting, a file sidebar, dark mode, and jump-to-uncovered
  6. Writes a machine-readable coverage_report.json sidecar so CI tooling can parse the result without scraping the HTML or stderr

Install

go install github.com/the-hotels-network/covlens/cmd/covlens@latest

Requires Go 1.25+, git, and go on your PATH. (Go's auto-toolchain feature means go install from an older Go version will automatically download the 1.25 toolchain.)

Heads-up: Go's auto-toolchain downloads omit covdata, which go test -coverprofile needs. If your system Go is older than your project's go.mod go directive, you'll see a "missing covdata" error. Install a matching system Go (from go.dev/dl/ or brew install go) so auto-toolchain doesn't trigger.

Usage

# Run from inside any Go repository
covlens

# Override thresholds
covlens --diff-threshold 90 --total-threshold 75

# Compare against a different base branch
covlens --base-branch develop

# Don't open the report automatically
covlens --no-open

# Scan the whole project — no diff, every file is measured
covlens --full

# Use a custom config file
covlens --config path/to/covlens.yaml

CLI flags

Flag Default Description
--base-branch main Branch to diff against
--diff-threshold 80 Minimum coverage % for changed lines
--total-threshold 70 Minimum coverage % for the whole project
--output-dir .coverage Directory for profiles, HTML report, and JSON sidecar
--no-open false Skip auto-opening the report in the browser
--open false Force opening the report in the browser (overrides html.auto_open: false in config)
--ratchet (-r) false Replace --total-threshold with a "must not drop vs. base branch" check
--full (-f) false Skip the diff and report coverage for every file in the project
--verbose (-v) false Stream go test output to stdout (default: capture to .coverage/test_output.log)
--config covlens.yaml Path to config file

Configuration

Place a covlens.yaml at your project root. CLI flags override it.

See example/covlens.yaml for a fully annotated reference.

Ignoring files and functions

Ignore a whole file

Add a //covlens:ignore comment before the package declaration:

//covlens:ignore — generated by protoc, do not edit
package mypkg

Ignore a single function

Add a //covlens:ignore doc comment directly above the function:

//covlens:ignore — wires up the HTTP server, covered by e2e tests
func main() {
    ...
}

Output

Every run writes its artifacts under output_dir (default: .coverage/):

  • coverage_report.html — self-contained HTML report
  • coverage_report.json — machine-readable sidecar
  • test_output.log — raw go test output; written when --verbose is not set (use --verbose to stream it to stdout instead)
  • coverage.out, coverage_diff.out — raw Go coverage profiles (kept for re-use; safe to delete)

The JSON sidecar is the integration point for CI tooling. Key fields:

{
  "schema": "1",
  "mode": "diff",
  "diff": {
    "status": "measured",
    "coverage": 92.5,
    "threshold": 80,
    "passed": true
  },
  "totalCoverage": 78.4,
  "totalThreshold": 70,
  "totalPassed": true,
  "htmlReportPath": "/abs/path/to/coverage_report.html",
  "files": [
    { "path": "pkg/foo.go", "coverage": 92.5, "status": "ok", "statements": 12, "covered": 11 }
  ]
}

diff is omitted in --full mode (no diff is computed). diff.status is one of "measured", "no-go-changes", "only-deletions", or "all-excluded" — the non-measured values indicate a vacuous pass (diff.passed: true but no measurement is meaningful).

schema is bumped on breaking changes; new fields are additive. The full type lives in internal/printer/json.

CI example (CircleCI)

version: 2.1
jobs:
  coverage:
    docker:
      - image: cimg/go:1.25
    steps:
      - checkout
      - run:
          name: Coverage check
          command: |
            go install github.com/the-hotels-network/covlens/cmd/covlens@latest
            covlens --no-open        # add --verbose to stream test output to the job log
      - store_artifacts:
          path: .coverage/           # captures HTML report, JSON sidecar, and test_output.log
          destination: coverage
workflows:
  test:
    jobs: [coverage]

The process exits 1 when a threshold is not met, which fails the job. store_artifacts makes the HTML report and JSON sidecar available in the CircleCI UI under the build's Artifacts tab.

PR comments and other CI glue

The JSON sidecar is the integration point for CI tooling — see Output. Reference scripts that wire covlens into specific CI/chat platforms live under scripts/ci/. For CircleCI + GitHub, scripts/ci/circleci/pr-comment.sh posts a sticky PR comment with the pass/fail summary and a link to the HTML report — see its README. These scripts are samples, not part of the binary's stability contract; see ADR 0003.

Multi-module repos

covlens walks up the directory tree from each changed file to find the nearest go.mod, so monorepos with multiple modules work without any configuration.

Test selection

covlens always passes -short to go test. Tests that opt in via if testing.Short() { t.Skip() } — typically slow integration or end-to-end suites — are skipped, and therefore do not contribute to coverage. Keep this in mind when calibrating thresholds: the coverage number reflects what your fast suite protects, not your full test matrix.

To run everything (including -short-gated tests), invoke go test ./... directly instead of going through covlens.

Recommended layout for e2e tests

For a cleaner separation, put your end-to-end tests in a dedicated sub-package and short-circuit the whole package with TestMain instead of sprinkling t.Skip() calls across each test:

// internal/myapp/e2e/main_test.go
package e2e

import (
    "flag"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    flag.Parse()
    if testing.Short() {
        os.Exit(0)
    }
    os.Exit(m.Run())
}

Every test in the e2e package now skips under -short automatically — new e2e tests need no boilerplate. To run them, invoke go test ./... from your repo root (covlens itself uses this layout; see internal/covlens/e2e/).

If your e2e-heavy files have no meaningful unit-test seam without a deeper refactor, list them in exclude_files so they don't drag the total coverage number down — see Ignoring files and functions.

Race detection

covlens does not pass -race to go test. The race detector requires CGO_ENABLED=1 and a C toolchain, which is absent from minimal CI images (golang:alpine, distroless, scratch-based), and it multiplies test wall time by 2–5×. Keeping it opt-in preserves portability and keeps covlens focused on coverage.

To enable race detection for a covlens run, set GOFLAGS in your environment:

GOFLAGS="-race" covlens

The subprocess inherits the environment, so the flag is appended to every go test invocation covlens makes. -covermode=atomic (which -race requires) is already set.

How diff coverage is calculated

For each changed file, covlens:

  1. Gets the changed line ranges via git diff
  2. Intersects those ranges with the coverage profile blocks
  3. Sums only the statements that fall within changed lines

This means: if you added 20 lines and all 20 are covered, diff coverage is 100% even if the rest of the file is untested.

Troubleshooting

compile: version "goX" does not match go tool version "goY"

You'll see this when your system Go is older than the version your project requires (e.g. system go1.25.10, but go.mod/go.work declares go 1.26):

# sync/atomic
compile: version "go1.26.0" does not match go tool version "go1.25.10"

It surfaces under covlens but not under a plain go test, because it's triggered specifically by coverage instrumentation. With GOTOOLCHAIN=auto, your system go re-execs into the newer downloaded toolchain — but the covdata step invokes a bare go resolved from PATH, which lands on your older system binary while pointed at the newer toolchain's files. That driver/toolchain mismatch is the error. This is an open Go bug: golang/go#77820.

Fix — pick one:

  • Set GOTOOLCHAIN to the version your project requires, so every go invocation (including the nested one) uses it:
    GOTOOLCHAIN=go1.26.0 covlens
  • Or upgrade your system Go to match the project.

go clean -cache does not help, and GOTOOLCHAIN=local fails outright when the project requires a newer Go than you have installed.

License

MIT © The Hotels Network


Built with Claude Code

About

Go coverage tool

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors