Bradley Burns | 8c3e486 | 2021-12-09 14:57:00 -0800 | [diff] [blame] | 1 | --- |
| 2 | layout: documentation |
| 3 | title: Code Coverage with Bazel |
| 4 | --- |
| 5 | |
| 6 | # Code coverage with Bazel |
| 7 | |
| 8 | Bazel features a `coverage` sub-command to produce code coverage |
| 9 | reports on repositories that can be tested with `bazel coverage`. Due |
| 10 | to the idiosyncrasies of the various language ecosystems, it is not |
| 11 | always trivial to make this work for a given project. |
| 12 | |
| 13 | This page documents the general process for creating and viewing |
| 14 | coverage reports, and also features some language-specific notes for |
| 15 | languages whose configuration is well-known. It is best read by first |
| 16 | reading [the general section](#Creating-a-coverage-report), and then |
| 17 | reading about the requirements for a specific language. Note also the |
| 18 | [remote execution section](#Remote-execution), which requires some |
| 19 | additional considerations. |
| 20 | |
| 21 | While a lot of customization is possible, this document focuses on |
| 22 | producing and consuming [`lcov`][lcov] reports, which is currently the |
| 23 | most well-supported route. |
| 24 | |
| 25 | ## Creating a coverage report |
| 26 | |
| 27 | ### Preparation |
| 28 | |
| 29 | The basic workflow for creating coverage reports requires the |
| 30 | following: |
| 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 | |
| 36 | The former two are language-specific and mostly straightforward, |
| 37 | however the latter can be more difficult for complex projects. |
| 38 | |
| 39 | "Instrumentation" in this case refers to the coverage tools that are |
| 40 | used for a specific target. Bazel allows turning this on for a |
| 41 | specific subset of files using the |
| 42 | [`--instrumentation_filter`](../command-line-reference.html#flag--instrumentation_filter) |
| 43 | flag, which specifies a filter for targets that are tested with the |
| 44 | instrumentation enabled. To enable instrumentation for tests, the |
| 45 | [`--instrument_test_targets`](../command-line-reference.html#flag--instrument_test_targets) |
| 46 | flag is required. |
| 47 | |
| 48 | By default, bazel tries to match the target package(s), and prints the |
| 49 | relevant filter as an `INFO` message. |
| 50 | |
| 51 | ### Running coverage |
| 52 | |
| 53 | To produce a coverage report, use [`bazel coverage |
| 54 | --combined_report=lcov |
| 55 | [target]`](../command-line-reference.html#coverage). This runs the |
| 56 | tests for the target, generating coverage reports in the lcov format |
| 57 | for each file. |
| 58 | |
| 59 | Once finished, bazel runs an action that collects all the produced |
| 60 | coverage files, and merges them into one, which is then finally |
| 61 | created under `$(bazel info |
| 62 | output_path)/_coverage/_coverage_report.dat`. |
| 63 | |
| 64 | Coverage reports are also produced if tests fail, though note that |
| 65 | this does not extend to the failed tests - only passing tests are |
| 66 | reported. |
| 67 | |
| 68 | ### Viewing coverage |
| 69 | |
| 70 | The coverage report is only output in the non-human-readable `lcov` |
| 71 | format. From this, we can use the `genhtml` utility (part of [the lcov |
| 72 | project][lcov]) to produce a report that can be viewed in a web |
| 73 | browser: |
| 74 | |
| 75 | ```console |
| 76 | genhtml --output genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat" |
| 77 | ``` |
| 78 | |
| 79 | Note that `genhtml` reads the source code as well, to annotate missing |
| 80 | coverage in these files. For this to work, it is expected that |
| 81 | `genhtml` is executed in the root of the bazel project. |
| 82 | |
| 83 | To view the result, simply open the `index.html` file produced in the |
| 84 | `genhtml` directory in any web browser. |
| 85 | |
| 86 | For further help and information around the `genhtml` tool, or the |
| 87 | `lcov` coverage format, see [the lcov project][lcov]. |
| 88 | |
| 89 | ## Remote execution |
| 90 | |
| 91 | Running 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 | |
| 119 | Java should work out-of-the-box with the default configuration. The |
| 120 | [bazel toolchains][bazel_toolchains] contain everything necessary for |
| 121 | remote execution, as well, including JUnit. |
| 122 | |
| 123 | ### Python |
| 124 | |
| 125 | #### Prerequisites |
| 126 | |
| 127 | Running 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 | |
| 136 | A way to do this is via [rules_python][rules_python], this provides |
| 137 | the ability to use a `requirements.txt` file, the requirements listed |
| 138 | in the file are then created as bazel targets using the |
| 139 | [pip_install][pip_install_rule] repository rule. |
| 140 | |
| 141 | The `requirements.txt` should have the following entry: |
| 142 | |
| 143 | ```text |
| 144 | git+https://github.com/ulfjack/coveragepy.git@lcov-support |
| 145 | ``` |
| 146 | |
| 147 | The `rules_python`, `pip_install`, and the `requirements.txt` file should then be used in the WORKSPACE file as: |
| 148 | |
| 149 | ```python |
| 150 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") |
| 151 | |
| 152 | http_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 | |
| 158 | load("@rules_python//python:pip.bzl", "pip_install") |
| 159 | |
| 160 | pip_install( |
| 161 | name = "python_deps", |
| 162 | requirements = "//:requirements.txt", |
| 163 | ) |
| 164 | ``` |
| 165 | |
| 166 | Note: The version of `rules_python` is incidental - this was simply |
| 167 | the latest at the time of writing. Refer to the |
| 168 | [upstream][rules_python] for up-to-date instructions. |
| 169 | |
| 170 | The coverage.py requirement can then be consumed by test targets by |
| 171 | setting the following in `BUILD` files: |
| 172 | |
| 173 | ```python |
| 174 | load("@python_deps//:requirements.bzl", "entry_point") |
| 175 | |
| 176 | alias( |
| 177 | name = "python_coverage_tools", |
| 178 | actual = entry_point("coverage"), |
| 179 | ) |
| 180 | |
| 181 | py_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 |