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.
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.
For local inspection, turn the merged LCOV file into HTML:
genhtml --branch-coverage --output genhtml \
"$(bazel info output_path)/_coverage/_coverage_report.dat"
Run it from the workspace root. genhtml reads the source files to annotate uncovered lines, then writes an index.html you can open in a browser1.
genhtml ships with lcov: brew install lcov on macOS, apt install lcov on Debian/Ubuntu.
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
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
2.True or false: interpreting bazel coverage.
Choose True or False for each sentence
Footnotes
-
Code coverage with Bazel —
bazel coverageworkflow,--instrumentation_filter, merged LCOV path underoutput_path/_coverage,genhtml, and failed tests not contributing coverage ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 -
Bazel Training 101 (Part 16): Coverage and Test Cases — three-step model, cross-language aggregation, coverage as
bazel testplus extra flags, and analysis-cache discard caveat ↩1 ↩2 ↩3 -
rules_ruby repository map — the code-coverage section scopes the ruleset-specific SimpleCov/LCOV requirements, filters, and report behavior. ↩