generate_jacoco_report.py 8.9 KB

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