blob: 7b00be7bbabed93fa065e50e62103978e70dd156 [file] [log] [blame] [view]
Bradley Burns8c3e4862021-12-09 14:57:00 -08001---
2layout: documentation
3title: Code Coverage with Bazel
4---
5
6# Code coverage with Bazel
7
8Bazel features a `coverage` sub-command to produce code coverage
9reports on repositories that can be tested with `bazel coverage`. Due
10to the idiosyncrasies of the various language ecosystems, it is not
11always trivial to make this work for a given project.
12
13This page documents the general process for creating and viewing
14coverage reports, and also features some language-specific notes for
15languages whose configuration is well-known. It is best read by first
16reading [the general section](#Creating-a-coverage-report), and then
17reading about the requirements for a specific language. Note also the
18[remote execution section](#Remote-execution), which requires some
19additional considerations.
20
21While a lot of customization is possible, this document focuses on
22producing and consuming [`lcov`][lcov] reports, which is currently the
23most well-supported route.
24
25## Creating a coverage report
26
27### Preparation
28
29The basic workflow for creating coverage reports requires the
30following:
31
32- A basic repository with test targets
33- A toolchain with the language-specific code coverage tools installed
34- A correct "instrumentation" configuration
35
36The former two are language-specific and mostly straightforward,
37however the latter can be more difficult for complex projects.
38
39"Instrumentation" in this case refers to the coverage tools that are
40used for a specific target. Bazel allows turning this on for a
41specific subset of files using the
42[`--instrumentation_filter`](../command-line-reference.html#flag--instrumentation_filter)
43flag, which specifies a filter for targets that are tested with the
44instrumentation enabled. To enable instrumentation for tests, the
45[`--instrument_test_targets`](../command-line-reference.html#flag--instrument_test_targets)
46flag is required.
47
48By default, bazel tries to match the target package(s), and prints the
49relevant filter as an `INFO` message.
50
51### Running coverage
52
53To produce a coverage report, use [`bazel coverage
54--combined_report=lcov
55[target]`](../command-line-reference.html#coverage). This runs the
56tests for the target, generating coverage reports in the lcov format
57for each file.
58
59Once finished, bazel runs an action that collects all the produced
60coverage files, and merges them into one, which is then finally
61created under `$(bazel info
62output_path)/_coverage/_coverage_report.dat`.
63
64Coverage reports are also produced if tests fail, though note that
65this does not extend to the failed tests - only passing tests are
66reported.
67
68### Viewing coverage
69
70The coverage report is only output in the non-human-readable `lcov`
71format. From this, we can use the `genhtml` utility (part of [the lcov
72project][lcov]) to produce a report that can be viewed in a web
73browser:
74
75```console
76genhtml --output genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat"
77```
78
79Note that `genhtml` reads the source code as well, to annotate missing
80coverage in these files. For this to work, it is expected that
81`genhtml` is executed in the root of the bazel project.
82
83To view the result, simply open the `index.html` file produced in the
84`genhtml` directory in any web browser.
85
86For further help and information around the `genhtml` tool, or the
87`lcov` coverage format, see [the lcov project][lcov].
88
89## Remote execution
90
91Running with remote test execution currently has a few caveats:
92
93- The report combination action cannot yet run remotely. This is
94 because Bazel does not consider the coverage output files as part of
95 its graph (see [this issue][remote_report_issue]), and can therefore
96 not correctly treat them as inputs to the combination action. To
97 work around this, use `--strategy=CoverageReport=local`.
98 - Note: It may be necessary to specify something like
99 `--strategy=CoverageReport=local,remote` instead, if Bazel is set
100 up to try `local,remote`, due to how Bazel resolves strategies.
101- `--remote_download_minimal` and similar flags can also not be used
102 as a consequence of the former.
103- Bazel will currently fail to create coverage information if tests
104 have been cached previously. To work around this,
105 `--nocache_test_results` can be set specifically for coverage runs,
106 although this of course incurs a heavy cost in terms of test times.
107- `--experimental_split_coverage_postprocessing` and
108 `--experimental_fetch_all_coverage_outputs`
109 - Usually coverage is run as part of the test action, and so by
110 default, we don't get all coverage back as outputs of the remote
111 execution by default. These flags override the default and obtain
112 the coverage data. See [this issue][split_coverage_issue] for more
113 details.
114
115## Language-specific configuration
116
117### Java
118
119Java should work out-of-the-box with the default configuration. The
120[bazel toolchains][bazel_toolchains] contain everything necessary for
121remote execution, as well, including JUnit.
122
123### Python
124
125#### Prerequisites
126
127Running coverage with python has some prerequisites:
128
129- A bazel binary that includes [b01c859][python_coverage_commit],
130 which should be any Bazel >3.0.
131- A [modified version of coverage.py][modified_coveragepy].
132<!-- TODO: Upstream an lcov implementation so that this becomes usable -->
133
134#### Consuming the modified coverage.py
135
136A way to do this is via [rules_python][rules_python], this provides
137the ability to use a `requirements.txt` file, the requirements listed
138in the file are then created as bazel targets using the
139[pip_install][pip_install_rule] repository rule.
140
141The `requirements.txt` should have the following entry:
142
143```text
144git+https://github.com/ulfjack/coveragepy.git@lcov-support
145```
146
147The `rules_python`, `pip_install`, and the `requirements.txt` file should then be used in the WORKSPACE file as:
148
149```python
150load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
151
152http_archive(
153 name = "rules_python",
154 url = "https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz",
155 sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332",
156)
157
158load("@rules_python//python:pip.bzl", "pip_install")
159
160pip_install(
161 name = "python_deps",
162 requirements = "//:requirements.txt",
163)
164```
165
166Note: The version of `rules_python` is incidental - this was simply
167the latest at the time of writing. Refer to the
168[upstream][rules_python] for up-to-date instructions.
169
170The coverage.py requirement can then be consumed by test targets by
171setting the following in `BUILD` files:
172
173```python
174load("@python_deps//:requirements.bzl", "entry_point")
175
176alias(
177 name = "python_coverage_tools",
178 actual = entry_point("coverage"),
179)
180
181py_test(
182 name = "test",
183 srcs = ["test.py"],
184 env = {
185 "PYTHON_COVERAGE": "$(location :python_coverage_tools)",
186 },
187 deps = [
188 ":main",
189 ":python_coverage_tools",
190 ],
191)
192```
193<!-- TODO: Allow specifying a target for `PYTHON_COVERAGE`, instead of having to use `$(location)` -->
194
195
196[lcov]: https://github.com/linux-test-project/lcov
197[rules_python]: https://github.com/bazelbuild/rules_python
198[bazel_toolchains]: https://github.com/bazelbuild/bazel-toolchains
199[remote_report_issue]: https://github.com/bazelbuild/bazel/issues/4685
200[split_coverage_issue]: https://github.com/bazelbuild/bazel/issues/4685
201[python_coverage_commit]: https://github.com/bazelbuild/bazel/commit/b01c85962d88661ec9f6c6704c47d8ce67ca4d2a
202[modified_coveragepy]: https://github.com/ulfjack/coveragepy/tree/lcov-support
203[pip_install_rule]: https://github.com/bazelbuild/rules_python#installing-pip-dependencies