xref: /aosp_15_r20/external/cronet/build/android/generate_jacoco_report.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env vpython3
2
3# Copyright 2013 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Aggregates Jacoco coverage files to produce output."""
8
9
10import argparse
11import fnmatch
12import json
13import os
14import sys
15
16import devil_chromium
17from devil.utils import cmd_helper
18from pylib.constants import host_paths
19
20# Source paths should be passed to Jacoco in a way that the relative file paths
21# reflect the class package name.
22_PARTIAL_PACKAGE_NAMES = ['com/google', 'org/chromium']
23
24# The sources_json_file is generated by jacoco_instr.py with source directories
25# and input path to non-instrumented jars.
26# e.g.
27# 'source_dirs': [
28#   "chrome/android/java/src/org/chromium/chrome/browser/toolbar/bottom",
29#   "chrome/android/java/src/org/chromium/chrome/browser/ui/system",
30# ...]
31# 'input_path':
32#   '$CHROMIUM_OUTPUT_DIR/\
33#    obj/chrome/android/features/tab_ui/java__process_prebuilt-filtered.jar'
34
35_SOURCES_JSON_FILES_SUFFIX = '__jacoco_sources.json'
36
37
38def _CreateClassfileArgs(class_files, report_type, include_substr=None):
39  """Returns a filtered list of files with classfile option.
40
41  Args:
42    class_files: A list of class files.
43    report_type: A string indicating if device or host files are desired.
44    include_substr: A substring that must be present to include the file.
45
46  Returns:
47    A list of files that don't use the suffix.
48  """
49  # These should match the jar class files generated in internal_rules.gni
50  search_jar_suffix = '%s.filter.jar' % report_type
51  result_class_files = []
52  for f in class_files:
53    include_file = False
54    if f.endswith(search_jar_suffix):
55      include_file = True
56
57    # If include_substr is specified, remove files that don't have the
58    # required substring.
59    if include_file and include_substr and include_substr not in f:
60      include_file = False
61    if include_file:
62      result_class_files += ['--classfiles', f]
63
64  return result_class_files
65
66
67def _GenerateReportOutputArgs(args, class_files, report_type):
68  cmd = _CreateClassfileArgs(class_files, report_type,
69                             args.include_substr_filter)
70  if args.format == 'html':
71    report_dir = os.path.join(args.output_dir, report_type)
72    if not os.path.exists(report_dir):
73      os.makedirs(report_dir)
74    cmd += ['--html', report_dir]
75  elif args.format == 'xml':
76    cmd += ['--xml', args.output_file]
77  elif args.format == 'csv':
78    cmd += ['--csv', args.output_file]
79
80  return cmd
81
82
83def _GetFilesWithSuffix(root_dir, suffix):
84  """Gets all files with a given suffix.
85
86  Args:
87    root_dir: Directory in which to search for files.
88    suffix: Suffix to look for.
89
90  Returns:
91    A list of absolute paths to files that match.
92  """
93  files = []
94  for root, _, filenames in os.walk(root_dir):
95    basenames = fnmatch.filter(filenames, '*' + suffix)
96    files.extend([os.path.join(root, basename) for basename in basenames])
97
98  return files
99
100
101def _GetExecFiles(root_dir, exclude_substr=None):
102  """ Gets all .exec files
103
104  Args:
105    root_dir: Root directory in which to search for files.
106    exclude_substr: Substring which should be absent in filename. If None, all
107      files are selected.
108
109  Returns:
110    A list of absolute paths to .exec files
111
112  """
113  all_exec_files = _GetFilesWithSuffix(root_dir, ".exec")
114  valid_exec_files = []
115  for exec_file in all_exec_files:
116    if not exclude_substr or exclude_substr not in exec_file:
117      valid_exec_files.append(exec_file)
118  return valid_exec_files
119
120
121def _ParseArguments(parser):
122  """Parses the command line arguments.
123
124  Args:
125    parser: ArgumentParser object.
126
127  Returns:
128    The parsed arguments.
129  """
130  parser.add_argument(
131      '--format',
132      required=True,
133      choices=['html', 'xml', 'csv'],
134      help='Output report format. Choose one from html, xml and csv.')
135  parser.add_argument(
136      '--device-or-host',
137      choices=['device', 'host'],
138      help='Selection on whether to use the device classpath files or the '
139      'host classpath files. Host would typically be used for junit tests '
140      ' and device for tests that run on the device. Only used for xml and csv'
141      ' reports.')
142  parser.add_argument('--include-substr-filter',
143                      help='Substring that must be included in classjars.',
144                      type=str,
145                      default='')
146  parser.add_argument('--output-dir', help='html report output directory.')
147  parser.add_argument('--output-file',
148                      help='xml file to write device coverage results.')
149  parser.add_argument(
150      '--coverage-dir',
151      required=True,
152      help='Root of the directory in which to search for '
153      'coverage data (.exec) files.')
154  parser.add_argument('--exec-filename-excludes',
155                      required=False,
156                      help='Excludes .exec files which contain a particular '
157                      'substring in their name')
158  parser.add_argument(
159      '--sources-json-dir',
160      help='Root of the directory in which to search for '
161      '*__jacoco_sources.json files.')
162  parser.add_argument(
163      '--class-files',
164      nargs='+',
165      help='Location of Java non-instrumented class files. '
166      'Use non-instrumented jars instead of instrumented jars. '
167      'e.g. use chrome_java__process_prebuilt_(host/device)_filter.jar instead'
168      'of chrome_java__process_prebuilt-instrumented.jar')
169  parser.add_argument(
170      '--sources',
171      nargs='+',
172      help='Location of the source files. '
173      'Specified source folders must be the direct parent of the folders '
174      'that define the Java packages.'
175      'e.g. <src_dir>/chrome/android/java/src/')
176  parser.add_argument(
177      '--cleanup',
178      action='store_true',
179      help='If set, removes coverage files generated at '
180      'runtime.')
181  args = parser.parse_args()
182
183  if args.format == 'html' and not args.output_dir:
184    parser.error('--output-dir needed for report.')
185  if args.format in ('csv', 'xml'):
186    if not args.output_file:
187      parser.error('--output-file needed for xml/csv reports.')
188    if not args.device_or_host and args.sources_json_dir:
189      parser.error('--device-or-host selection needed with --sources-json-dir')
190  if not (args.sources_json_dir or args.class_files):
191    parser.error('At least either --sources-json-dir or --class-files needed.')
192  return args
193
194
195def main():
196  parser = argparse.ArgumentParser()
197  args = _ParseArguments(parser)
198
199  devil_chromium.Initialize()
200
201  coverage_files = _GetExecFiles(args.coverage_dir, args.exec_filename_excludes)
202  if not coverage_files:
203    parser.error('No coverage file found under %s' % args.coverage_dir)
204  print('Found coverage files: %s' % str(coverage_files))
205
206  class_files = []
207  source_dirs = []
208  if args.sources_json_dir:
209    sources_json_files = _GetFilesWithSuffix(args.sources_json_dir,
210                                             _SOURCES_JSON_FILES_SUFFIX)
211    for f in sources_json_files:
212      with open(f, 'r') as json_file:
213        data = json.load(json_file)
214        class_files.extend(data['input_path'])
215        source_dirs.extend(data['source_dirs'])
216
217  # Fix source directories as direct parent of Java packages.
218  fixed_source_dirs = set()
219  for path in source_dirs:
220    for partial in _PARTIAL_PACKAGE_NAMES:
221      if partial in path:
222        fixed_dir = os.path.join(host_paths.DIR_SOURCE_ROOT,
223                                 path[:path.index(partial)])
224        fixed_source_dirs.add(fixed_dir)
225        break
226
227  if args.class_files:
228    class_files += args.class_files
229  if args.sources:
230    fixed_source_dirs.update(args.sources)
231
232  cmd = [
233      'java', '-jar',
234      os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'jacoco', 'lib',
235                   'jacococli.jar'), 'report'
236  ] + coverage_files
237
238  for source in fixed_source_dirs:
239    cmd += ['--sourcefiles', source]
240
241  if args.format == 'html':
242    # Both reports are generated for html as the cq bot generates an html
243    # report and we wouldn't know which one a developer needed.
244    device_cmd = cmd + _GenerateReportOutputArgs(args, class_files, 'device')
245    host_cmd = cmd + _GenerateReportOutputArgs(args, class_files, 'host')
246
247    device_exit_code = cmd_helper.RunCmd(device_cmd)
248    host_exit_code = cmd_helper.RunCmd(host_cmd)
249    exit_code = device_exit_code or host_exit_code
250  else:
251    cmd = cmd + _GenerateReportOutputArgs(args, class_files,
252                                          args.device_or_host)
253    exit_code = cmd_helper.RunCmd(cmd)
254
255  if args.cleanup:
256    for f in coverage_files:
257      os.remove(f)
258
259  # Command tends to exit with status 0 when it actually failed.
260  if not exit_code:
261    if args.format == 'html':
262      if not os.path.isdir(args.output_dir) or not os.listdir(args.output_dir):
263        print('No report generated at %s' % args.output_dir)
264        exit_code = 1
265    elif not os.path.isfile(args.output_file):
266      print('No device coverage report generated at %s' % args.output_file)
267      exit_code = 1
268
269  return exit_code
270
271
272if __name__ == '__main__':
273  sys.exit(main())
274