123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- #!/usr/bin/env vpython3
- # Copyright 2013 The Chromium Authors. All rights reserved.
- # 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."""
- from __future__ import print_function
- 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())
|