xref: /aosp_15_r20/external/angle/build/android/generate_jacoco_report.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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 
10 import argparse
11 import fnmatch
12 import json
13 import os
14 import sys
15 
16 import devil_chromium
17 from devil.utils import cmd_helper
18 from 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 
38 def _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 
67 def _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 
83 def _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 
101 def _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 
121 def _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 
195 def 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', 'cipd',
235                    'lib', '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 
272 if __name__ == '__main__':
273   sys.exit(main())
274