xref: /aosp_15_r20/external/pigweed/pw_build/coverage_report.gni (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14
15import("//build_overrides/pigweed.gni")
16import("//build_overrides/pigweed_environment.gni")
17
18import("$dir_pw_build/python_action.gni")
19import("$dir_pw_toolchain/host_clang/toolchains.gni")
20
21# Expands to code coverage targets that can be used as dependencies to generate
22# coverage reports at build time.
23#
24# Arguments:
25# - enable_if (optional): Conditionally activates coverage report generation
26#   when set to a boolean expression that evaluates to true.
27# - failure_mode (optional/unstable): Specify the failure mode for llvm-profdata
28#   (used to merge inidividual profraw files from pw_test runs). Available
29#   options are "any" (default) or "all". This should be considered an
30#   unstable/deprecated argument that should only be used as a last resort to
31#   get a build working again. Using failure_mode = "all" usually indicates that
32#   there are underlying problems in the build or test infrastructure that
33#   should be independently resolved. Please reach out to the Pigweed team for
34#   assistance.
35# - Coverage Settings
36#   - filter_paths (optional): List of file paths (using GN path helpers like
37#     `//` is supported). These will be translated into absolute paths before
38#     being used. These filter source files so that the coverage report *ONLY*
39#     includes files that match one of these paths. These cannot be regular
40#     expressions, but can be concrete file or folder paths. Folder paths will
41#     allow all files in that directory or any recursive child directory.
42#   - ignore_filename_patterns (optional): List of file path regular expressions
43#     to ignore when generating the coverage report.
44# - pw_test Depedencies (required): These control which test binaries are used
45#   to collect usage data for the coverage report. The following can basically
46#   be used interchangeably with no actual difference in the template expansion.
47#   Only one of these is required to be provided.
48#   - tests: A list of pw_test targets.
49#   - group_deps: A list of pw_test_group targets.
50#
51# Expands To:
52# pw_coverage_report follows the overall Pigweed pattern where targets exist
53# for all build configurations, but are only configured to do meaningful work
54# under the correct build configuration. In this vein, pw_coverage_report
55# ensures that a coverage-enabled toolchain is being used and the provided
56# enable_if evaluates to true (if provided).
57#
58# - If a coverage-enabled toolchain is being used and the provided enable_if
59#   evaluates to true (if provided):
60#   - <target_name>.text: Generates a text representation of the coverage
61#                         report. This is the output of
62#                         `llvm-cov show --format text`.
63#   - <target_name>.html: Generates an HTML representation of the coverage
64#                         report. This is the output of
65#                         `llvm-cov show --format html`.
66#   - <target_name>.lcov: Generates an LCOV representation of the coverage
67#                         report. This is the output of
68#                         `llvm-cov export --format lcov`.
69#   - <target_name>.json: Generates a JSON representation of the coverage
70#                         report. This is the output of
71#                         `llvm-cov export --format text`.
72#
73#   - <target_name>: A group that takes dependencies on <target_name>.text,
74#                    <target_name>.html, <target_name>.lcov, and
75#                    <target_name>.json. This can be used to force generation of
76#                    all coverage artifacts without manually depending on each
77#                    target.
78#
79#   - The other targets this expands to should be considered private and not
80#     used as dependencies.
81# - If a coverage-enabled toolchain is not being used or the provided enable_if
82#   evaluates to false (if provided).
83#   - All of the above target names, but they are empty groups.
84template("pw_coverage_report") {
85  assert(defined(invoker.tests) || defined(invoker.group_deps),
86         "One of `tests` or `group_deps` must be provided.")
87  assert(!defined(invoker.failure_mode) ||
88             (invoker.failure_mode == "any" || invoker.failure_mode == "all"),
89         "failure_mode only supports \"any\" or \"all\".")
90
91  _report_name = target_name
92  _format_types = [
93    "text",
94    "html",
95    "lcov",
96    "json",
97  ]
98  _should_enable = !defined(invoker.enable_if) || invoker.enable_if
99
100  # These two Pigweed build arguments are required to be in these states to
101  # ensure binaries are instrumented for coverage and profraw files are
102  # exported.
103  if (_should_enable && pw_toolchain_COVERAGE_ENABLED) {
104    _test_metadata = "$target_out_dir/$_report_name.test_metadata.json"
105    _profdata_file = "$target_out_dir/merged.profdata"
106    _arguments = {
107      filter_paths = []
108      if (defined(invoker.filter_paths)) {
109        filter_paths += invoker.filter_paths
110      }
111
112      ignore_filename_patterns = []
113      if (defined(invoker.ignore_filename_patterns)) {
114        ignore_filename_patterns += invoker.ignore_filename_patterns
115      }
116
117      # Merge any provided `tests` or `group_deps` to `deps` and `run_deps`.
118      #
119      # `deps` are used to generate the .test_metadata.json file.
120      # `run_deps` are used to block on the test execution to generate a profraw
121      # file.
122      deps = []
123      run_deps = []
124      test_or_group_deps = []
125      if (defined(invoker.tests)) {
126        test_or_group_deps += invoker.tests
127      }
128      if (defined(invoker.group_deps)) {
129        test_or_group_deps += invoker.group_deps
130      }
131      foreach(dep, test_or_group_deps) {
132        deps += [ dep ]
133
134        dep_target = get_label_info(dep, "label_no_toolchain")
135        dep_toolchain = get_label_info(dep, "toolchain")
136        run_deps += [ "$dep_target.run($dep_toolchain)" ]
137      }
138    }
139
140    # Generate a list of all test binaries and their associated profraw files
141    # after executing we can use to generate the coverage report.
142    generated_file("_$_report_name.test_metadata") {
143      outputs = [ _test_metadata ]
144      data_keys = [
145        "unit_tests",
146        "profraws",
147      ]
148      output_conversion = "json"
149      deps = _arguments.deps
150    }
151
152    # Merge the generated profraws from instrumented binaries into a single
153    # profdata.
154    pw_python_action("_$_report_name.merge_profraws") {
155      _depfile_path = "$target_out_dir/$_report_name.merged_profraws.d"
156
157      module = "pw_build.merge_profraws"
158      args = [
159        "--llvm-profdata-path",
160        pw_toolchain_clang_tools.llvm_profdata,
161        "--test-metadata-path",
162        rebase_path(_test_metadata, root_build_dir),
163        "--profdata-path",
164        rebase_path(_profdata_file, root_build_dir),
165        "--depfile-path",
166        rebase_path(_depfile_path, root_build_dir),
167      ]
168
169      # TODO: b/256651964 - We really want `--failure-mode any` always to guarantee
170      # we don't silently ignore any profraw report. However, there are downstream
171      # projects that currently break when using `--failure-mode any`.
172      #
173      # See the task for examples of what is currently going wrong.
174      #
175      # Invalid profraw files will be ignored so coverage reports might have a
176      # slight variance between runs depending on if something failed or not.
177      if (defined(invoker.failure_mode)) {
178        args += [
179          "--failure-mode",
180          invoker.failure_mode,
181        ]
182      }
183
184      inputs = [ _test_metadata ]
185      sources = []
186      depfile = _depfile_path
187
188      outputs = [ _profdata_file ]
189
190      python_deps = [ "$dir_pw_build/py" ]
191      deps = _arguments.run_deps
192      public_deps = [ ":_$_report_name.test_metadata" ]
193    }
194
195    foreach(format, _format_types) {
196      pw_python_action("$_report_name.$format") {
197        _depfile_path = "$target_out_dir/$_report_name.$format.d"
198        _output_dir = "$target_out_dir/$_report_name/$format/"
199
200        module = "pw_build.generate_report"
201        args = [
202          "--llvm-cov-path",
203          pw_toolchain_clang_tools.llvm_cov,
204          "--format",
205          format,
206          "--test-metadata-path",
207          rebase_path(_test_metadata, root_build_dir),
208          "--profdata-path",
209          rebase_path(_profdata_file, root_build_dir),
210          "--root-dir",
211          rebase_path("//", root_build_dir),
212          "--build-dir",
213          ".",
214          "--output-dir",
215          rebase_path(_output_dir, root_build_dir),
216          "--depfile-path",
217          rebase_path(_depfile_path, root_build_dir),
218        ]
219        foreach(filter_path, _arguments.filter_paths) {
220          args += [
221            # We rebase to absolute paths here to resolve any "//" used in the
222            # filter_paths.
223            "--filter-path",
224            rebase_path(filter_path),
225          ]
226        }
227        foreach(ignore_filename_pattern, _arguments.ignore_filename_patterns) {
228          args += [
229            "--ignore-filename-pattern",
230            ignore_filename_pattern,
231          ]
232        }
233
234        inputs = [
235          _test_metadata,
236          _profdata_file,
237        ]
238        sources = []
239        depfile = _depfile_path
240
241        outputs = []
242        if (format == "text") {
243          outputs += [ "$_output_dir/index.txt" ]
244        } else if (format == "html") {
245          outputs += [ "$_output_dir/index.html" ]
246        } else if (format == "lcov") {
247          outputs += [ "$_output_dir/report.lcov" ]
248        } else if (format == "json") {
249          outputs += [ "$_output_dir/report.json" ]
250        }
251
252        python_deps = [ "$dir_pw_build/py" ]
253        deps = [ ":_$_report_name.merge_profraws" ]
254      }
255    }
256  } else {
257    not_needed(invoker, "*")
258    foreach(format, _format_types) {
259      group("$_report_name.$format") {
260      }
261    }
262  }
263
264  group("$_report_name") {
265    deps = []
266    foreach(format, _format_types) {
267      deps += [ ":$_report_name.$format" ]
268    }
269  }
270}
271