| #!/usr/bin/env vpython3 |
| |
| # Copyright 2013 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Aggregates Jacoco coverage files to produce output.""" |
| |
| |
| import argparse |
| import fnmatch |
| import json |
| import os |
| import sys |
| |
| import devil_chromium |
| from devil.utils import cmd_helper |
| from pylib.constants import host_paths |
| |
| # Source paths should be passed to Jacoco in a way that the relative file paths |
| # reflect the class package name. |
| _PARTIAL_PACKAGE_NAMES = ['com/google', 'org/chromium'] |
| |
| # The sources_json_file is generated by jacoco_instr.py with source directories |
| # and input path to non-instrumented jars. |
| # e.g. |
| # 'source_dirs': [ |
| # "chrome/android/java/src/org/chromium/chrome/browser/toolbar/bottom", |
| # "chrome/android/java/src/org/chromium/chrome/browser/ui/system", |
| # ...] |
| # 'input_path': |
| # '$CHROMIUM_OUTPUT_DIR/\ |
| # obj/chrome/android/features/tab_ui/java__process_prebuilt-filtered.jar' |
| |
| _SOURCES_JSON_FILES_SUFFIX = '__jacoco_sources.json' |
| |
| |
| def _CreateClassfileArgs(class_files, report_type, include_substr=None): |
| """Returns a filtered list of files with classfile option. |
| |
| Args: |
| class_files: A list of class files. |
| report_type: A string indicating if device or host files are desired. |
| include_substr: A substring that must be present to include the file. |
| |
| Returns: |
| A list of files that don't use the suffix. |
| """ |
| # These should match the jar class files generated in internal_rules.gni |
| search_jar_suffix = '%s.filter.jar' % report_type |
| result_class_files = [] |
| for f in class_files: |
| include_file = False |
| if f.endswith(search_jar_suffix): |
| include_file = True |
| |
| # If include_substr is specified, remove files that don't have the |
| # required substring. |
| if include_file and include_substr and include_substr not in f: |
| include_file = False |
| if include_file: |
| result_class_files += ['--classfiles', f] |
| |
| return result_class_files |
| |
| |
| def _GenerateReportOutputArgs(args, class_files, report_type): |
| cmd = _CreateClassfileArgs(class_files, report_type, |
| args.include_substr_filter) |
| if args.format == 'html': |
| report_dir = os.path.join(args.output_dir, report_type) |
| if not os.path.exists(report_dir): |
| os.makedirs(report_dir) |
| cmd += ['--html', report_dir] |
| elif args.format == 'xml': |
| cmd += ['--xml', args.output_file] |
| elif args.format == 'csv': |
| cmd += ['--csv', args.output_file] |
| |
| return cmd |
| |
| |
| def _GetFilesWithSuffix(root_dir, suffix): |
| """Gets all files with a given suffix. |
| |
| Args: |
| root_dir: Directory in which to search for files. |
| suffix: Suffix to look for. |
| |
| Returns: |
| A list of absolute paths to files that match. |
| """ |
| files = [] |
| for root, _, filenames in os.walk(root_dir): |
| basenames = fnmatch.filter(filenames, '*' + suffix) |
| files.extend([os.path.join(root, basename) for basename in basenames]) |
| |
| return files |
| |
| |
| def _GetExecFiles(root_dir, exclude_substr=None): |
| """ Gets all .exec files |
| |
| Args: |
| root_dir: Root directory in which to search for files. |
| exclude_substr: Substring which should be absent in filename. If None, all |
| files are selected. |
| |
| Returns: |
| A list of absolute paths to .exec files |
| |
| """ |
| all_exec_files = _GetFilesWithSuffix(root_dir, ".exec") |
| valid_exec_files = [] |
| for exec_file in all_exec_files: |
| if not exclude_substr or exclude_substr not in exec_file: |
| valid_exec_files.append(exec_file) |
| return valid_exec_files |
| |
| |
| def _ParseArguments(parser): |
| """Parses the command line arguments. |
| |
| Args: |
| parser: ArgumentParser object. |
| |
| Returns: |
| The parsed arguments. |
| """ |
| parser.add_argument( |
| '--format', |
| required=True, |
| choices=['html', 'xml', 'csv'], |
| help='Output report format. Choose one from html, xml and csv.') |
| parser.add_argument( |
| '--device-or-host', |
| choices=['device', 'host'], |
| help='Selection on whether to use the device classpath files or the ' |
| 'host classpath files. Host would typically be used for junit tests ' |
| ' and device for tests that run on the device. Only used for xml and csv' |
| ' reports.') |
| parser.add_argument('--include-substr-filter', |
| help='Substring that must be included in classjars.', |
| type=str, |
| default='') |
| parser.add_argument('--output-dir', help='html report output directory.') |
| parser.add_argument('--output-file', |
| help='xml file to write device coverage results.') |
| parser.add_argument( |
| '--coverage-dir', |
| required=True, |
| help='Root of the directory in which to search for ' |
| 'coverage data (.exec) files.') |
| parser.add_argument('--exec-filename-excludes', |
| required=False, |
| help='Excludes .exec files which contain a particular ' |
| 'substring in their name') |
| parser.add_argument( |
| '--sources-json-dir', |
| help='Root of the directory in which to search for ' |
| '*__jacoco_sources.json files.') |
| parser.add_argument( |
| '--class-files', |
| nargs='+', |
| help='Location of Java non-instrumented class files. ' |
| 'Use non-instrumented jars instead of instrumented jars. ' |
| 'e.g. use chrome_java__process_prebuilt_(host/device)_filter.jar instead' |
| 'of chrome_java__process_prebuilt-instrumented.jar') |
| parser.add_argument( |
| '--sources', |
| nargs='+', |
| help='Location of the source files. ' |
| 'Specified source folders must be the direct parent of the folders ' |
| 'that define the Java packages.' |
| 'e.g. <src_dir>/chrome/android/java/src/') |
| parser.add_argument( |
| '--cleanup', |
| action='store_true', |
| help='If set, removes coverage files generated at ' |
| 'runtime.') |
| args = parser.parse_args() |
| |
| if args.format == 'html' and not args.output_dir: |
| parser.error('--output-dir needed for report.') |
| if args.format in ('csv', 'xml'): |
| if not args.output_file: |
| parser.error('--output-file needed for xml/csv reports.') |
| if not args.device_or_host and args.sources_json_dir: |
| parser.error('--device-or-host selection needed with --sources-json-dir') |
| if not (args.sources_json_dir or args.class_files): |
| parser.error('At least either --sources-json-dir or --class-files needed.') |
| return args |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| args = _ParseArguments(parser) |
| |
| devil_chromium.Initialize() |
| |
| coverage_files = _GetExecFiles(args.coverage_dir, args.exec_filename_excludes) |
| if not coverage_files: |
| parser.error('No coverage file found under %s' % args.coverage_dir) |
| print('Found coverage files: %s' % str(coverage_files)) |
| |
| class_files = [] |
| source_dirs = [] |
| if args.sources_json_dir: |
| sources_json_files = _GetFilesWithSuffix(args.sources_json_dir, |
| _SOURCES_JSON_FILES_SUFFIX) |
| for f in sources_json_files: |
| with open(f, 'r') as json_file: |
| data = json.load(json_file) |
| class_files.extend(data['input_path']) |
| source_dirs.extend(data['source_dirs']) |
| |
| # Fix source directories as direct parent of Java packages. |
| fixed_source_dirs = set() |
| for path in source_dirs: |
| for partial in _PARTIAL_PACKAGE_NAMES: |
| if partial in path: |
| fixed_dir = os.path.join(host_paths.DIR_SOURCE_ROOT, |
| path[:path.index(partial)]) |
| fixed_source_dirs.add(fixed_dir) |
| break |
| |
| if args.class_files: |
| class_files += args.class_files |
| if args.sources: |
| fixed_source_dirs.update(args.sources) |
| |
| cmd = [ |
| 'java', '-jar', |
| os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'jacoco', 'lib', |
| 'jacococli.jar'), 'report' |
| ] + coverage_files |
| |
| for source in fixed_source_dirs: |
| cmd += ['--sourcefiles', source] |
| |
| if args.format == 'html': |
| # Both reports are generated for html as the cq bot generates an html |
| # report and we wouldn't know which one a developer needed. |
| device_cmd = cmd + _GenerateReportOutputArgs(args, class_files, 'device') |
| host_cmd = cmd + _GenerateReportOutputArgs(args, class_files, 'host') |
| |
| device_exit_code = cmd_helper.RunCmd(device_cmd) |
| host_exit_code = cmd_helper.RunCmd(host_cmd) |
| exit_code = device_exit_code or host_exit_code |
| else: |
| cmd = cmd + _GenerateReportOutputArgs(args, class_files, |
| args.device_or_host) |
| exit_code = cmd_helper.RunCmd(cmd) |
| |
| if args.cleanup: |
| for f in coverage_files: |
| os.remove(f) |
| |
| # Command tends to exit with status 0 when it actually failed. |
| if not exit_code: |
| if args.format == 'html': |
| if not os.path.isdir(args.output_dir) or not os.listdir(args.output_dir): |
| print('No report generated at %s' % args.output_dir) |
| exit_code = 1 |
| elif not os.path.isfile(args.output_file): |
| print('No device coverage report generated at %s' % args.output_file) |
| exit_code = 1 |
| |
| return exit_code |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |