Share a Rust coverage report

Rust's coverage story got good and stayed awkward to share. Both of the tools people use produce a real HTML report, and both of them produce it inside target/ on whichever machine ran the tests.

Two tools, two output shapes

# LLVM source-based — more accurate, writes a directory
cargo llvm-cov --html
# → target/llvm-cov/html/index.html + per-file pages

# tarpaulin — writes ONE self-contained file
cargo tarpaulin --out Html
# → tarpaulin-report.html

Use cargo llvm-cov when you care about the numbers being right: it uses LLVM instrumentation and handles generics and macro expansion far better than tarpaulin's instruction-counting approach.

Use cargo tarpaulin --out Html when you care about the report being easy to move, because a single file publishes in one call with nothing to upload alongside it.

Publishing the tarpaulin single file

cargo tarpaulin --out Html || true

curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html tarpaulin-report.html \
        --arg title "Coverage — $(git rev-parse --short HEAD)" \
        '{title: $title, html: $html}')"

One call, no assets. Use a scoped token (reports:write) from CI secrets and PATCH a saved report id, so one URL accumulates a revision per run instead of a new orphan link per build.

The || true matters — tarpaulin runs your suite, so a failing test fails the step. Add if: always() in GitHub Actions. See publishing from CI.

Publishing the llvm-cov directory

target/llvm-cov/html/
├── index.html
├── coverage/…/src/lib.rs.html    # one page per source file
└── style.css

Drag that folder (or a zip of it) into Comma: index.html becomes the report body, the per-file pages upload alongside it, and relative references are rewritten so the drill-down from summary to annotated source resolves.

From CI, the entry page goes in the PATCH body and the per-file pages go up through POST /api/v1/reports/$REPORT_ID/assets as base64 — one call per file, worth scripting once. See the API reference.

What the URL changes

  • The PR links the coverage. Reviewers click instead of running cargo llvm-cov locally, which on a large workspace is not a quick thing to ask.
  • Review lands on the code. Select the uncovered arm of a match, leave a thread — "this is the error path, it needs a test" — and it stays anchored there across runs. See commenting on HTML.
  • Revision diffs beat the percentage. Two runs, one diff, which lines changed coverage. A Rust workspace's coverage number moves for uninteresting reasons constantly; the diff doesn't.

Who can see it

The coverage report contains your source code. Access is per report — private, your team, any signed-in user with the link, or public — and team or private is the right default for a private repo. Domain-gating is Enterprise. See sharing & access control.

Limits

  • Entry HTML: 5 MB. Tarpaulin inlines every covered source file, so a large workspace can exceed it — generate per-crate reports and publish the crates people review. Assets: 25 MB per file, 250 MB and 500 files total.
  • Scripts run, sandboxed (allow-scripts, no allow-same-origin).
  • 60 requests/minute per token.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Publish a coverage report →

Related