xref: /aosp_15_r20/external/cronet/testing/merge_scripts/code_coverage/merge_results.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python
2# Copyright 2019 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Merge results from code-coverage/pgo swarming runs.
6
7This script merges code-coverage/pgo profiles from multiple shards. It also
8merges the test results of the shards.
9
10It is functionally similar to merge_steps.py but it accepts the parameters
11passed by swarming api.
12"""
13
14import argparse
15import json
16import logging
17import os
18import subprocess
19import sys
20
21import merge_lib as profile_merger
22
23def _MergeAPIArgumentParser(*args, **kwargs):
24  """Parameters passed to this merge script, as per:
25  https://chromium.googlesource.com/chromium/tools/build/+/main/scripts/slave/recipe_modules/swarming/resources/merge_api.py
26  """
27  parser = argparse.ArgumentParser(*args, **kwargs)
28  parser.add_argument('--build-properties', help=argparse.SUPPRESS,
29                      default='{}')
30  parser.add_argument('--summary-json', help=argparse.SUPPRESS)
31  parser.add_argument('--task-output-dir', help=argparse.SUPPRESS)
32  parser.add_argument(
33      '-o', '--output-json', required=True, help=argparse.SUPPRESS)
34  parser.add_argument('jsons_to_merge', nargs='*', help=argparse.SUPPRESS)
35
36  # Custom arguments for this merge script.
37  parser.add_argument(
38      '--additional-merge-script', help='additional merge script to run')
39  parser.add_argument(
40      '--additional-merge-script-args',
41      help='JSON serialized string of args for the additional merge script')
42  parser.add_argument(
43      '--profdata-dir', required=True, help='where to store the merged data')
44  parser.add_argument(
45      '--llvm-profdata', required=True, help='path to llvm-profdata executable')
46  parser.add_argument('--test-target-name', help='test target name')
47  parser.add_argument(
48      '--java-coverage-dir', help='directory for Java coverage data')
49  parser.add_argument(
50      '--jacococli-path', help='path to jacococli.jar.')
51  parser.add_argument(
52      '--merged-jacoco-filename',
53      help='filename used to uniquely name the merged exec file.')
54  parser.add_argument(
55      '--javascript-coverage-dir',
56      help='directory for JavaScript coverage data')
57  parser.add_argument(
58      '--chromium-src-dir',
59      help='directory for chromium/src checkout')
60  parser.add_argument(
61      '--build-dir',
62      help='directory for the build directory in chromium/src')
63  parser.add_argument(
64      '--per-cl-coverage',
65      action='store_true',
66      help='set to indicate that this is a per-CL coverage build')
67  parser.add_argument(
68      '--sparse',
69      action='store_true',
70      dest='sparse',
71      help='run llvm-profdata with the sparse flag.')
72  # (crbug.com/1091310) - IR PGO is incompatible with the initial conversion
73  # of .profraw -> .profdata that's run to detect validation errors.
74  # Introducing a bypass flag that'll merge all .profraw directly to .profdata
75  parser.add_argument(
76      '--skip-validation',
77      action='store_true',
78      help='skip validation for good raw profile data. this will pass all '
79           'raw profiles found to llvm-profdata to be merged. only applicable '
80           'when input extension is .profraw.')
81  return parser
82
83
84def main():
85  desc = "Merge profraw files in <--task-output-dir> into a single profdata."
86  parser = _MergeAPIArgumentParser(description=desc)
87  params = parser.parse_args()
88
89  if params.java_coverage_dir:
90    if not params.jacococli_path:
91      parser.error('--jacococli-path required when merging Java coverage')
92    if not params.merged_jacoco_filename:
93      parser.error(
94          '--merged-jacoco-filename required when merging Java coverage')
95
96    output_path = os.path.join(
97        params.java_coverage_dir, '%s.exec' % params.merged_jacoco_filename)
98    logging.info('Merging JaCoCo .exec files to %s', output_path)
99    profile_merger.merge_java_exec_files(
100        params.task_output_dir, output_path, params.jacococli_path)
101
102  failed = False
103
104  if params.javascript_coverage_dir and params.chromium_src_dir \
105      and params.build_dir:
106    current_dir = os.path.dirname(__file__)
107    merge_js_results_script = os.path.join(current_dir, 'merge_js_results.py')
108    args = [
109      sys.executable,
110      merge_js_results_script,
111      '--task-output-dir',
112      params.task_output_dir,
113      '--javascript-coverage-dir',
114      params.javascript_coverage_dir,
115      '--chromium-src-dir',
116      params.chromium_src_dir,
117      '--build-dir',
118      params.build_dir,
119    ]
120
121    rc = subprocess.call(args)
122    if rc != 0:
123      failed = True
124      logging.warning('%s exited with %s' %
125                      (merge_js_results_script, rc))
126
127  # Name the output profdata file name as {test_target}.profdata or
128  # default.profdata.
129  output_prodata_filename = (params.test_target_name or 'default') + '.profdata'
130
131  # NOTE: The profile data merge script must make sure that the profraw files
132  # are deleted from the task output directory after merging, otherwise, other
133  # test results merge script such as layout tests will treat them as json test
134  # results files and result in errors.
135  invalid_profiles, counter_overflows = profile_merger.merge_profiles(
136      params.task_output_dir,
137      os.path.join(params.profdata_dir, output_prodata_filename), '.profraw',
138      params.llvm_profdata,
139      sparse=params.sparse,
140      skip_validation=params.skip_validation)
141
142  # At the moment counter overflows overlap with invalid profiles, but this is
143  # not guaranteed to remain the case indefinitely. To avoid future conflicts
144  # treat these separately.
145  if counter_overflows:
146    with open(
147        os.path.join(params.profdata_dir, 'profiles_with_overflows.json'),
148        'w') as f:
149      json.dump(counter_overflows, f)
150
151  if invalid_profiles:
152    with open(os.path.join(params.profdata_dir, 'invalid_profiles.json'),
153              'w') as f:
154      json.dump(invalid_profiles, f)
155
156  # If given, always run the additional merge script, even if we only have one
157  # output json. Merge scripts sometimes upload artifacts to cloud storage, or
158  # do other processing which can be needed even if there's only one output.
159  if params.additional_merge_script:
160    new_args = [
161        '--build-properties',
162        params.build_properties,
163        '--summary-json',
164        params.summary_json,
165        '--task-output-dir',
166        params.task_output_dir,
167        '--output-json',
168        params.output_json,
169    ]
170
171    if params.additional_merge_script_args:
172      new_args += json.loads(params.additional_merge_script_args)
173
174    new_args += params.jsons_to_merge
175
176    args = [sys.executable, params.additional_merge_script] + new_args
177    rc = subprocess.call(args)
178    if rc != 0:
179      failed = True
180      logging.warning('Additional merge script %s exited with %s' %
181                      (params.additional_merge_script, rc))
182  elif len(params.jsons_to_merge) == 1:
183    logging.info("Only one output needs to be merged; directly copying it.")
184    with open(params.jsons_to_merge[0]) as f_read:
185      with open(params.output_json, 'w') as f_write:
186        f_write.write(f_read.read())
187  else:
188    logging.warning(
189        'This script was told to merge test results, but no additional merge '
190        'script was given.')
191
192  # TODO(crbug.com/1369158): Return non-zero if invalid_profiles is not None
193  return 1 if failed else 0
194
195
196if __name__ == '__main__':
197  logging.basicConfig(
198      format='[%(asctime)s %(levelname)s] %(message)s', level=logging.INFO)
199  sys.exit(main())
200