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