blob: 7b00be7bbabed93fa065e50e62103978e70dd156 [file] [log] [blame] [view]
---
layout: documentation
title: Code Coverage with Bazel
---
# Code coverage with Bazel
Bazel features a `coverage` sub-command to produce code coverage
reports on repositories that can be tested with `bazel coverage`. Due
to the idiosyncrasies of the various language ecosystems, it is not
always trivial to make this work for a given project.
This page documents the general process for creating and viewing
coverage reports, and also features some language-specific notes for
languages whose configuration is well-known. It is best read by first
reading [the general section](#Creating-a-coverage-report), and then
reading about the requirements for a specific language. Note also the
[remote execution section](#Remote-execution), which requires some
additional considerations.
While a lot of customization is possible, this document focuses on
producing and consuming [`lcov`][lcov] reports, which is currently the
most well-supported route.
## Creating a coverage report
### Preparation
The basic workflow for creating coverage reports requires the
following:
- A basic repository with test targets
- A toolchain with the language-specific code coverage tools installed
- A correct "instrumentation" configuration
The former two are language-specific and mostly straightforward,
however the latter can be more difficult for complex projects.
"Instrumentation" in this case refers to the coverage tools that are
used for a specific target. Bazel allows turning this on for a
specific subset of files using the
[`--instrumentation_filter`](../command-line-reference.html#flag--instrumentation_filter)
flag, which specifies a filter for targets that are tested with the
instrumentation enabled. To enable instrumentation for tests, the
[`--instrument_test_targets`](../command-line-reference.html#flag--instrument_test_targets)
flag is required.
By default, bazel tries to match the target package(s), and prints the
relevant filter as an `INFO` message.
### Running coverage
To produce a coverage report, use [`bazel coverage
--combined_report=lcov
[target]`](../command-line-reference.html#coverage). This runs the
tests for the target, generating coverage reports in the lcov format
for each file.
Once finished, bazel runs an action that collects all the produced
coverage files, and merges them into one, which is then finally
created under `$(bazel info
output_path)/_coverage/_coverage_report.dat`.
Coverage reports are also produced if tests fail, though note that
this does not extend to the failed tests - only passing tests are
reported.
### Viewing coverage
The coverage report is only output in the non-human-readable `lcov`
format. From this, we can use the `genhtml` utility (part of [the lcov
project][lcov]) to produce a report that can be viewed in a web
browser:
```console
genhtml --output genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat"
```
Note that `genhtml` reads the source code as well, to annotate missing
coverage in these files. For this to work, it is expected that
`genhtml` is executed in the root of the bazel project.
To view the result, simply open the `index.html` file produced in the
`genhtml` directory in any web browser.
For further help and information around the `genhtml` tool, or the
`lcov` coverage format, see [the lcov project][lcov].
## Remote execution
Running with remote test execution currently has a few caveats:
- The report combination action cannot yet run remotely. This is
because Bazel does not consider the coverage output files as part of
its graph (see [this issue][remote_report_issue]), and can therefore
not correctly treat them as inputs to the combination action. To
work around this, use `--strategy=CoverageReport=local`.
- Note: It may be necessary to specify something like
`--strategy=CoverageReport=local,remote` instead, if Bazel is set
up to try `local,remote`, due to how Bazel resolves strategies.
- `--remote_download_minimal` and similar flags can also not be used
as a consequence of the former.
- Bazel will currently fail to create coverage information if tests
have been cached previously. To work around this,
`--nocache_test_results` can be set specifically for coverage runs,
although this of course incurs a heavy cost in terms of test times.
- `--experimental_split_coverage_postprocessing` and
`--experimental_fetch_all_coverage_outputs`
- Usually coverage is run as part of the test action, and so by
default, we don't get all coverage back as outputs of the remote
execution by default. These flags override the default and obtain
the coverage data. See [this issue][split_coverage_issue] for more
details.
## Language-specific configuration
### Java
Java should work out-of-the-box with the default configuration. The
[bazel toolchains][bazel_toolchains] contain everything necessary for
remote execution, as well, including JUnit.
### Python
#### Prerequisites
Running coverage with python has some prerequisites:
- A bazel binary that includes [b01c859][python_coverage_commit],
which should be any Bazel >3.0.
- A [modified version of coverage.py][modified_coveragepy].
<!-- TODO: Upstream an lcov implementation so that this becomes usable -->
#### Consuming the modified coverage.py
A way to do this is via [rules_python][rules_python], this provides
the ability to use a `requirements.txt` file, the requirements listed
in the file are then created as bazel targets using the
[pip_install][pip_install_rule] repository rule.
The `requirements.txt` should have the following entry:
```text
git+https://github.com/ulfjack/coveragepy.git@lcov-support
```
The `rules_python`, `pip_install`, and the `requirements.txt` file should then be used in the WORKSPACE file as:
```python
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "rules_python",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz",
sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332",
)
load("@rules_python//python:pip.bzl", "pip_install")
pip_install(
name = "python_deps",
requirements = "//:requirements.txt",
)
```
Note: The version of `rules_python` is incidental - this was simply
the latest at the time of writing. Refer to the
[upstream][rules_python] for up-to-date instructions.
The coverage.py requirement can then be consumed by test targets by
setting the following in `BUILD` files:
```python
load("@python_deps//:requirements.bzl", "entry_point")
alias(
name = "python_coverage_tools",
actual = entry_point("coverage"),
)
py_test(
name = "test",
srcs = ["test.py"],
env = {
"PYTHON_COVERAGE": "$(location :python_coverage_tools)",
},
deps = [
":main",
":python_coverage_tools",
],
)
```
<!-- TODO: Allow specifying a target for `PYTHON_COVERAGE`, instead of having to use `$(location)` -->
[lcov]: https://github.com/linux-test-project/lcov
[rules_python]: https://github.com/bazelbuild/rules_python
[bazel_toolchains]: https://github.com/bazelbuild/bazel-toolchains
[remote_report_issue]: https://github.com/bazelbuild/bazel/issues/4685
[split_coverage_issue]: https://github.com/bazelbuild/bazel/issues/4685
[python_coverage_commit]: https://github.com/bazelbuild/bazel/commit/b01c85962d88661ec9f6c6704c47d8ce67ca4d2a
[modified_coveragepy]: https://github.com/ulfjack/coveragepy/tree/lcov-support
[pip_install_rule]: https://github.com/bazelbuild/rules_python#installing-pip-dependencies