1.2.8 Coverage

Bazel's coverage subcommand answers more than whether tests pass: it reports which instrumented lines the passing tests executed. Operationally it is still a test run. Bazel rebuilds instrumented code, runs the selected tests, and then merges the produced tracefiles into LCOV output1,2.

What does bazel coverage add to a test run?
Bazel instruments selected code, runs tests, and then can merge their coverage files.
COMMAND
Run tests and request one merged LCOV report
LCOV is a text format for line coverage
bazel coverage //app:calculator_test --combined_report=lcov
--instrumentation_filter=...
Selects which code Bazel prepares to record line execution.
1 · INSTRUMENT
Prepare selected code to record executed lines
the filter sets the measurement boundary
Bazel rebuilds matching targets with coverage support.
2 · TEST
Run tests and record the lines they execute
pass or fail is still decided here
Each passing test can contribute coverage. Failed tests do not add coverage data.
3 · MERGE
Combine the available per-test coverage files
only when --combined_report=lcov is set
The result is one LCOV file under output_path/_coverage.
STEP 2 OUTPUT · ONE FILE PER TEST
bazel-testlogs/<package>/<target>/coverage.dat
STEP 3 OUTPUT · ONE MERGED REPORT
$(bazel info output_path)/_coverage/_coverage_report.dat
Coverage follows instrument → test → merge. The merged report describes only the coverage data that passing tests contributed.

bazel coverage is bazel test plus instrumentation

bazel coverage //app:calculator_test --combined_report=lcov rebuilds with instrumentation, runs tests, and aggregates the reports. Because coverage is bazel test with extra flags, switching into coverage mode can discard the warm analysis cache and feel slower than an ordinary rerun2.

Coverage support is also language-dependent. Bazel can aggregate coverage across languages, but the actual instrumentation comes from the relevant toolchain and test integration for each ecosystem1,2. The per-language setup, filter tuning, and CI reporting story continues in T9 Coverage Tools.

Two artifacts come out of a coverage run

A real run of the coverage-basics snippet shows that Bazel produces both a per-test tracefile and, when --combined_report=lcov is present, a merged report:

INFO: LCOV coverage report is located at .../bazel-out/_coverage/_coverage_report.dat
//app:calculator_test                                                    PASSED in 0.5s
  .../bazel-out/darwin_arm64-fastbuild/testlogs/app/calculator_test/coverage.dat

That distinction matters in daily use. Like the other test artifacts from 1.2.7 Test Output & Debugging, raw per-target coverage data lives under bazel-testlogs/<package>/<target>/coverage.dat. The merged LCOV file that you upload to CI tooling or feed into genhtml lives under $(bazel info output_path)/_coverage/_coverage_report.dat1.

The merged report is plain LCOV text, not a rendered HTML dashboard. In the snippet's Calculator.java, the uncovered negative branch shows up as DA:8,0, while covered lines have non-zero counts. The report tells you which lines the executed tests touched.

Coverage is only as good as the instrumentation boundary

Instrumenting the wrong code produces misleading numbers. --instrumentation_filter controls which targets Bazel instruments, and Bazel prints the default filter it inferred from the requested target packages as an INFO line1. The result depends on what code Bazel decided to count.

Coverage can also yield a partial picture when the invocation is red. Bazel may still produce coverage output if some tests fail, but failed tests do not contribute coverage data1. A coverage report describes only what the passing tests from that invocation executed. It says nothing about the coverage those failed tests would have contributed.

That last limitation is why coverage numbers and test selection cannot be separated for long. Once CI stops running the whole repo, later chapters on 6.5.1 Affected-Target Service Contract and H.7.3 Evidence Selection determine which tests feed the report, while T9 Coverage Tools goes deeper on filters, language-specific tooling, and CI uploads.

For a language-ruleset example, rules_ruby documents the extra runtime instrumentation and LCOV setup needed before bazel coverage can collect Ruby lines. Its documented report path provides a useful diagnostic boundary between "the coverage command ran" and "the language integration emitted usable records."3

key takeaway

Use bazel coverage when you want line-execution evidence alongside pass/fail. Expect an instrumented rebuild, look in bazel-testlogs/.../coverage.dat for per-test tracefiles, and in $(bazel info output_path)/_coverage/_coverage_report.dat for the merged LCOV file you upload to CI tooling or render locally with genhtml.

Check your understanding · 2 questions

1.Match each coverage artifact to its usual location:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Per-test tracefile
Merged LCOV report

2.True or false: interpreting bazel coverage.

Choose True or False for each sentence

Coverage requires instrumented code and test execution.
The merged LCOV file is already an HTML dashboard.
Failed tests do not contribute coverage data to the report.
0 of 2 answered

Footnotes

  1. Code coverage with Bazelbazel coverage workflow, --instrumentation_filter, merged LCOV path under output_path/_coverage, genhtml, and failed tests not contributing coverage 1 2 3 4 5 6

  2. Bazel Training 101 (Part 16): Coverage and Test Cases — three-step model, cross-language aggregation, coverage as bazel test plus extra flags, and analysis-cache discard caveat 1 2 3

  3. rules_ruby repository map — the code-coverage section scopes the ruleset-specific SimpleCov/LCOV requirements, filters, and report behavior.