blob: d95a129539f7bcb75aa8ae7e460dbdec79767ad7 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 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.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3856
57 For an overview of how code coverage works in Chromium, please refer to
58 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5459"""
60
61from __future__ import print_function
62
63import sys
64
65import argparse
Yuke Liaoea228d02018-01-05 19:10:3366import json
Yuke Liao481d3482018-01-29 19:17:1067import logging
Yuke Liao506e8822017-12-04 16:52:5468import os
Yuke Liaob2926832018-03-02 17:34:2969import re
70import shlex
Max Moroz025d8952018-05-03 16:33:3471import shutil
Yuke Liao506e8822017-12-04 16:52:5472import subprocess
Yuke Liao506e8822017-12-04 16:52:5473import urllib2
74
Abhishek Arya1ec832c2017-12-05 18:06:5975sys.path.append(
76 os.path.join(
77 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
78 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5479import update as clang_update
80
Yuke Liaoea228d02018-01-05 19:10:3381sys.path.append(
82 os.path.join(
83 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
84 'third_party'))
85import jinja2
86from collections import defaultdict
87
Yuke Liao506e8822017-12-04 16:52:5488# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5989SRC_ROOT_PATH = os.path.abspath(
90 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5491
92# Absolute path to the code coverage tools binary.
93LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:1994LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
95LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
96LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5497
98# Build directory, the value is parsed from command line arguments.
99BUILD_DIR = None
100
101# Output directory for generated artifacts, the value is parsed from command
102# line arguemnts.
103OUTPUT_DIR = None
104
105# Default number of jobs used to build when goma is configured and enabled.
106DEFAULT_GOMA_JOBS = 100
107
108# Name of the file extension for profraw data files.
109PROFRAW_FILE_EXTENSION = 'profraw'
110
111# Name of the final profdata file, and this file needs to be passed to
112# "llvm-cov" command in order to call "llvm-cov show" to inspect the
113# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48114PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
115
116# Name of the file with summary information generated by llvm-cov export.
117SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54118
119# Build arg required for generating code coverage data.
120CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
121
Yuke Liaoea228d02018-01-05 19:10:33122# The default name of the html coverage report for a directory.
123DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
124
Yuke Liaodd1ec0592018-02-02 01:26:37125# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37126COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48127DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37128FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48129INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
130
131LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37132
133# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19134COMPONENT_MAPPING_URL = (
135 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37136
Yuke Liao80afff32018-03-07 01:26:20137# Caches the results returned by _GetBuildArgs, don't use this variable
138# directly, call _GetBuildArgs instead.
139_BUILD_ARGS = None
140
Abhishek Aryac19bc5ef2018-05-04 22:10:02141# Retry failed merges.
142MERGE_RETRIES = 3
143
Yuke Liaoea228d02018-01-05 19:10:33144
145class _CoverageSummary(object):
146 """Encapsulates coverage summary representation."""
147
Yuke Liaodd1ec0592018-02-02 01:26:37148 def __init__(self,
149 regions_total=0,
150 regions_covered=0,
151 functions_total=0,
152 functions_covered=0,
153 lines_total=0,
154 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33155 """Initializes _CoverageSummary object."""
156 self._summary = {
157 'regions': {
158 'total': regions_total,
159 'covered': regions_covered
160 },
161 'functions': {
162 'total': functions_total,
163 'covered': functions_covered
164 },
165 'lines': {
166 'total': lines_total,
167 'covered': lines_covered
168 }
169 }
170
171 def Get(self):
172 """Returns summary as a dictionary."""
173 return self._summary
174
175 def AddSummary(self, other_summary):
176 """Adds another summary to this one element-wise."""
177 for feature in self._summary:
178 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
179 self._summary[feature]['covered'] += other_summary.Get()[feature][
180 'covered']
181
182
Yuke Liaodd1ec0592018-02-02 01:26:37183class _CoverageReportHtmlGenerator(object):
184 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33185
Yuke Liaodd1ec0592018-02-02 01:26:37186 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33187 """
188
Yuke Liaodd1ec0592018-02-02 01:26:37189 def __init__(self, output_path, table_entry_type):
190 """Initializes _CoverageReportHtmlGenerator object.
191
192 Args:
193 output_path: Path to the html report that will be generated.
194 table_entry_type: Type of the table entries to be displayed in the table
195 header. For example: 'Path', 'Component'.
196 """
Yuke Liaoea228d02018-01-05 19:10:33197 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48198 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33199 assert os.path.exists(css_absolute_path), (
200 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40201 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33202 css_absolute_path)
203
204 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37205 self._output_path = output_path
206 self._table_entry_type = table_entry_type
207
Yuke Liaoea228d02018-01-05 19:10:33208 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12209 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23210
211 source_dir = os.path.dirname(os.path.realpath(__file__))
212 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33213
214 jinja_env = jinja2.Environment(
215 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
216 self._header_template = jinja_env.get_template('header.html')
217 self._table_template = jinja_env.get_template('table.html')
218 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23219
Abhishek Arya865fffd2018-05-08 22:16:01220 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23221 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33222
223 def AddLinkToAnotherReport(self, html_report_path, name, summary):
224 """Adds a link to another html report in this report.
225
226 The link to be added is assumed to be an entry in this directory.
227 """
Yuke Liaodd1ec0592018-02-02 01:26:37228 # Use relative paths instead of absolute paths to make the generated reports
229 # portable.
230 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
231 html_report_path, self._output_path)
232
Yuke Liaod54030e2018-01-08 17:34:12233 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37234 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12235 os.path.basename(html_report_path) ==
236 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
237 self._table_entries.append(table_entry)
238
239 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35240 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12241 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
242
243 def _CreateTableEntryFromCoverageSummary(self,
244 summary,
245 href=None,
246 name=None,
247 is_dir=None):
248 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37249 assert (href is None and name is None and is_dir is None) or (
250 href is not None and name is not None and is_dir is not None), (
251 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35252 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37253 'attributes must be None.')
254
Yuke Liaod54030e2018-01-08 17:34:12255 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37256 if href is not None:
257 entry['href'] = href
258 if name is not None:
259 entry['name'] = name
260 if is_dir is not None:
261 entry['is_dir'] = is_dir
262
Yuke Liaoea228d02018-01-05 19:10:33263 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12264 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37265 if summary_dict[feature]['total'] == 0:
266 percentage = 0.0
267 else:
Yuke Liao0e4c8682018-04-18 21:06:59268 percentage = float(summary_dict[feature]
269 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35270
Yuke Liaoea228d02018-01-05 19:10:33271 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12272 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33273 'total': summary_dict[feature]['total'],
274 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35275 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33276 'color_class': color_class
277 }
Yuke Liaod54030e2018-01-08 17:34:12278
Yuke Liaod54030e2018-01-08 17:34:12279 return entry
Yuke Liaoea228d02018-01-05 19:10:33280
281 def _GetColorClass(self, percentage):
282 """Returns the css color class based on coverage percentage."""
283 if percentage >= 0 and percentage < 80:
284 return 'red'
285 if percentage >= 80 and percentage < 100:
286 return 'yellow'
287 if percentage == 100:
288 return 'green'
289
Abhishek Aryafb70b532018-05-06 17:47:40290 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33291
Yuke Liaodd1ec0592018-02-02 01:26:37292 def WriteHtmlCoverageReport(self):
293 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33294
295 In the report, sub-directories are displayed before files and within each
296 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33297 """
298
299 def EntryCmp(left, right):
300 """Compare function for table entries."""
301 if left['is_dir'] != right['is_dir']:
302 return -1 if left['is_dir'] == True else 1
303
Yuke Liaodd1ec0592018-02-02 01:26:37304 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33305
306 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
307
308 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48309
310 directory_view_path = _GetDirectoryViewPath()
311 component_view_path = _GetComponentViewPath()
312 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37313
Yuke Liaoea228d02018-01-05 19:10:33314 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37315 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
316 directory_view_href=_GetRelativePathToDirectoryOfFile(
317 directory_view_path, self._output_path),
318 component_view_href=_GetRelativePathToDirectoryOfFile(
319 component_view_path, self._output_path),
320 file_view_href=_GetRelativePathToDirectoryOfFile(
Abhishek Arya865fffd2018-05-08 22:16:01321 file_view_path, self._output_path),
322 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37323
Yuke Liaod54030e2018-01-08 17:34:12324 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37325 entries=self._table_entries,
326 total_entry=self._total_entry,
327 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33328 html_footer = self._footer_template.render()
329
Yuke Liaodd1ec0592018-02-02 01:26:37330 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33331 html_file.write(html_header + html_table + html_footer)
332
Yuke Liao506e8822017-12-04 16:52:54333
Abhishek Arya64636af2018-05-04 14:42:13334def _ConfigureLogging(args):
335 """Configures logging settings for later use."""
336 log_level = logging.DEBUG if args.verbose else logging.INFO
337 log_format = '[%(asctime)s %(levelname)s] %(message)s'
338 log_file = args.log_file if args.log_file else None
339 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
340
341
Max Morozd73e45f2018-04-24 18:32:47342def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54343 """Returns list of shared libraries used by specified binaries."""
344 logging.info('Finding shared libraries for targets (if any).')
345 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47346 cmd = []
347 shared_library_re = None
348
349 if sys.platform.startswith('linux'):
350 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13351 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
352 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47353 elif sys.platform.startswith('darwin'):
354 cmd.extend(['otool', '-L'])
355 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
356 else:
Abhishek Aryafb70b532018-05-06 17:47:40357 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47358
359 assert shared_library_re is not None
360
361 cmd.extend(binary_paths)
362 output = subprocess.check_output(cmd)
363
364 for line in output.splitlines():
365 m = shared_library_re.match(line)
366 if not m:
367 continue
368
369 shared_library_path = m.group(1)
370 if sys.platform.startswith('darwin'):
371 # otool outputs "@rpath" macro instead of the dirname of the given binary.
372 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
373
Abhishek Arya78120bc2018-05-07 20:53:54374 if shared_library_path in shared_libraries:
375 continue
376
Max Morozd73e45f2018-04-24 18:32:47377 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
378 'the given target(s) does not '
379 'exist.' % shared_library_path)
380 with open(shared_library_path) as f:
381 data = f.read()
382
383 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
384 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54385 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47386
Abhishek Arya78120bc2018-05-07 20:53:54387 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
388 shared_libraries)
389 logging.info('Finished finding shared libraries for targets.')
390 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47391
392
Yuke Liaoc60b2d02018-03-02 21:40:43393def _GetHostPlatform():
394 """Returns the host platform.
395
396 This is separate from the target platform/os that coverage is running for.
397 """
Abhishek Arya1ec832c2017-12-05 18:06:59398 if sys.platform == 'win32' or sys.platform == 'cygwin':
399 return 'win'
400 if sys.platform.startswith('linux'):
401 return 'linux'
402 else:
403 assert sys.platform == 'darwin'
404 return 'mac'
405
406
Abhishek Arya1c97ea542018-05-10 03:53:19407def _GetPathWithLLVMSymbolizerDir():
408 """Add llvm-symbolizer directory to path for symbolized stacks."""
409 path = os.getenv('PATH')
410 dirs = path.split(os.pathsep)
411 if LLVM_BIN_DIR in dirs:
412 return path
413
414 return path + os.pathsep + LLVM_BIN_DIR
415
416
Yuke Liaoc60b2d02018-03-02 21:40:43417def _GetTargetOS():
418 """Returns the target os specified in args.gn file.
419
420 Returns an empty string is target_os is not specified.
421 """
Yuke Liao80afff32018-03-07 01:26:20422 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43423 return build_args['target_os'] if 'target_os' in build_args else ''
424
425
Yuke Liaob2926832018-03-02 17:34:29426def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10427 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43428 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10429
430
Yuke Liao506e8822017-12-04 16:52:54431# TODO(crbug.com/759794): remove this function once tools get included to
432# Clang bundle:
433# https://chromium-review.googlesource.com/c/chromium/src/+/688221
434def DownloadCoverageToolsIfNeeded():
435 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59436
Yuke Liaoc60b2d02018-03-02 21:40:43437 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54438 """Returns a pair of revision number by reading the build stamp file.
439
440 Args:
441 stamp_file_path: A path the build stamp file created by
442 tools/clang/scripts/update.py.
443 Returns:
444 A pair of integers represeting the main and sub revision respectively.
445 """
446 if not os.path.exists(stamp_file_path):
447 return 0, 0
448
449 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43450 stamp_file_line = stamp_file.readline()
451 if ',' in stamp_file_line:
452 package_version = stamp_file_line.rstrip().split(',')[0]
453 else:
454 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54455
Yuke Liaoc60b2d02018-03-02 21:40:43456 clang_revision_str, clang_sub_revision_str = package_version.split('-')
457 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59458
Yuke Liaoc60b2d02018-03-02 21:40:43459 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54460 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43461 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54462
463 coverage_revision_stamp_file = os.path.join(
464 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
465 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43466 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54467
Yuke Liaoea228d02018-01-05 19:10:33468 has_coverage_tools = (
469 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32470
Yuke Liaoea228d02018-01-05 19:10:33471 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54472 coverage_sub_revision == clang_sub_revision):
473 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43474 return
Yuke Liao506e8822017-12-04 16:52:54475
476 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
477 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
478
479 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43480 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54481 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43482 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54483 coverage_tools_url = (
484 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43485 else:
486 assert host_platform == 'win'
487 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54488
489 try:
490 clang_update.DownloadAndUnpack(coverage_tools_url,
491 clang_update.LLVM_BUILD_DIR)
Abhishek Aryafb70b532018-05-06 17:47:40492 logging.info('Coverage tools %s unpacked.', package_version)
Yuke Liao506e8822017-12-04 16:52:54493 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43494 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54495 file_handle.write('\n')
496 except urllib2.URLError:
497 raise Exception(
498 'Failed to download coverage tools: %s.' % coverage_tools_url)
499
500
Yuke Liaodd1ec0592018-02-02 01:26:37501def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59502 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54503 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
504
505 For a file with absolute path /a/b/x.cc, a html report is generated as:
506 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
507 OUTPUT_DIR/index.html.
508
509 Args:
510 binary_paths: A list of paths to the instrumented binaries.
511 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42512 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54513 """
Yuke Liao506e8822017-12-04 16:52:54514 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
515 # [[-object BIN]] [SOURCES]
516 # NOTE: For object files, the first one is specified as a positional argument,
517 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10518 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40519 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59520 subprocess_cmd = [
521 LLVM_COV_PATH, 'show', '-format=html',
522 '-output-dir={}'.format(OUTPUT_DIR),
523 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
524 ]
525 subprocess_cmd.extend(
526 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29527 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42528 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59529 if ignore_filename_regex:
530 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
531
Yuke Liao506e8822017-12-04 16:52:54532 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34533
534 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
535 # the platform name instead, as it simplifies the report dir structure when
536 # the same report is generated for different platforms.
537 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48538 platform_report_subdir_path = _GetCoverageReportRootDirPath()
539 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34540
Abhishek Aryafb70b532018-05-06 17:47:40541 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54542
543
Yuke Liaodd1ec0592018-02-02 01:26:37544def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
545 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48546 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37547 logging.debug('Generating file view html index file as: "%s".',
548 file_view_index_file_path)
549 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
550 'Path')
551 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33552
Yuke Liaodd1ec0592018-02-02 01:26:37553 for file_path in per_file_coverage_summary:
554 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
555
556 html_generator.AddLinkToAnotherReport(
557 _GetCoverageHtmlReportPathForFile(file_path),
558 os.path.relpath(file_path, SRC_ROOT_PATH),
559 per_file_coverage_summary[file_path])
560
561 html_generator.CreateTotalsEntry(totals_coverage_summary)
562 html_generator.WriteHtmlCoverageReport()
563 logging.debug('Finished generating file view html index file.')
564
565
566def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
567 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40568 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37569 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
570
Yuke Liaoea228d02018-01-05 19:10:33571 for file_path in per_file_coverage_summary:
572 summary = per_file_coverage_summary[file_path]
573 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40574
Yuke Liaoea228d02018-01-05 19:10:33575 while True:
576 per_directory_coverage_summary[parent_dir].AddSummary(summary)
577
578 if parent_dir == SRC_ROOT_PATH:
579 break
580 parent_dir = os.path.dirname(parent_dir)
581
Abhishek Aryafb70b532018-05-06 17:47:40582 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37583 return per_directory_coverage_summary
584
585
586def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
587 per_file_coverage_summary):
588 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40589 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33590 for dir_path in per_directory_coverage_summary:
591 _GenerateCoverageInHtmlForDirectory(
592 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
593
Abhishek Aryafb70b532018-05-06 17:47:40594 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10595
Yuke Liaoea228d02018-01-05 19:10:33596
597def _GenerateCoverageInHtmlForDirectory(
598 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
599 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37600 html_generator = _CoverageReportHtmlGenerator(
601 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33602
603 for entry_name in os.listdir(dir_path):
604 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33605
Yuke Liaodd1ec0592018-02-02 01:26:37606 if entry_path in per_file_coverage_summary:
607 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
608 entry_coverage_summary = per_file_coverage_summary[entry_path]
609 elif entry_path in per_directory_coverage_summary:
610 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
611 entry_path)
612 entry_coverage_summary = per_directory_coverage_summary[entry_path]
613 else:
Yuke Liaoc7e607142018-02-05 20:26:14614 # Any file without executable lines shouldn't be included into the report.
615 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37616 continue
Yuke Liaoea228d02018-01-05 19:10:33617
Yuke Liaodd1ec0592018-02-02 01:26:37618 html_generator.AddLinkToAnotherReport(entry_html_report_path,
619 os.path.basename(entry_path),
620 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33621
Yuke Liaod54030e2018-01-08 17:34:12622 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37623 html_generator.WriteHtmlCoverageReport()
624
625
626def _GenerateDirectoryViewHtmlIndexFile():
627 """Generates the html index file for directory view.
628
629 Note that the index file is already generated under SRC_ROOT_PATH, so this
630 file simply redirects to it, and the reason of this extra layer is for
631 structural consistency with other views.
632 """
Max Moroz7c5354f2018-05-06 00:03:48633 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37634 logging.debug('Generating directory view html index file as: "%s".',
635 directory_view_index_file_path)
636 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
637 SRC_ROOT_PATH)
638 _WriteRedirectHtmlFile(directory_view_index_file_path,
639 src_root_html_report_path)
640 logging.debug('Finished generating directory view html index file.')
641
642
643def _CalculatePerComponentCoverageSummary(component_to_directories,
644 per_directory_coverage_summary):
645 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40646 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37647 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
648
649 for component in component_to_directories:
650 for directory in component_to_directories[component]:
651 absolute_directory_path = os.path.abspath(directory)
652 if absolute_directory_path in per_directory_coverage_summary:
653 per_component_coverage_summary[component].AddSummary(
654 per_directory_coverage_summary[absolute_directory_path])
655
Abhishek Aryafb70b532018-05-06 17:47:40656 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37657 return per_component_coverage_summary
658
659
660def _ExtractComponentToDirectoriesMapping():
661 """Returns a mapping from components to directories."""
662 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
663 directory_to_component = component_mappings['dir-to-component']
664
665 component_to_directories = defaultdict(list)
666 for directory in directory_to_component:
667 component = directory_to_component[directory]
668 component_to_directories[component].append(directory)
669
670 return component_to_directories
671
672
673def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
674 component_to_directories,
675 per_directory_coverage_summary):
676 """Generates per-component coverage reports in html."""
677 logging.debug('Writing per-component coverage html reports.')
678 for component in per_component_coverage_summary:
679 _GenerateCoverageInHtmlForComponent(
680 component, per_component_coverage_summary, component_to_directories,
681 per_directory_coverage_summary)
682
683 logging.debug('Finished writing per-component coverage html reports.')
684
685
686def _GenerateCoverageInHtmlForComponent(
687 component_name, per_component_coverage_summary, component_to_directories,
688 per_directory_coverage_summary):
689 """Generates coverage html report for a component."""
690 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
691 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14692 component_html_report_dir = os.path.dirname(component_html_report_path)
693 if not os.path.exists(component_html_report_dir):
694 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37695
696 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
697 'Path')
698
699 for dir_path in component_to_directories[component_name]:
700 dir_absolute_path = os.path.abspath(dir_path)
701 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14702 # Any directory without an excercised file shouldn't be included into the
703 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37704 continue
705
706 html_generator.AddLinkToAnotherReport(
707 _GetCoverageHtmlReportPathForDirectory(dir_path),
708 os.path.relpath(dir_path, SRC_ROOT_PATH),
709 per_directory_coverage_summary[dir_absolute_path])
710
711 html_generator.CreateTotalsEntry(
712 per_component_coverage_summary[component_name])
713 html_generator.WriteHtmlCoverageReport()
714
715
716def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
717 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48718 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37719 logging.debug('Generating component view html index file as: "%s".',
720 component_view_index_file_path)
721 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
722 'Component')
723 totals_coverage_summary = _CoverageSummary()
724
725 for component in per_component_coverage_summary:
726 totals_coverage_summary.AddSummary(
727 per_component_coverage_summary[component])
728
729 html_generator.AddLinkToAnotherReport(
730 _GetCoverageHtmlReportPathForComponent(component), component,
731 per_component_coverage_summary[component])
732
733 html_generator.CreateTotalsEntry(totals_coverage_summary)
734 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14735 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33736
737
Max Moroz7c5354f2018-05-06 00:03:48738def _MergeTwoDirectories(src_path, dst_path):
739 """Merge src_path directory into dst_path directory."""
740 for filename in os.listdir(src_path):
741 dst_path = os.path.join(dst_path, filename)
742 if os.path.exists(dst_path):
743 shutil.rmtree(dst_path)
744 os.rename(os.path.join(src_path, filename), dst_path)
745 shutil.rmtree(src_path)
746
747
Yuke Liaoea228d02018-01-05 19:10:33748def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37749 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48750 html_index_file_path = _GetHtmlIndexPath()
751 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37752 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
753
754
755def _WriteRedirectHtmlFile(from_html_path, to_html_path):
756 """Writes a html file that redirects to another html file."""
757 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
758 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33759 content = ("""
760 <!DOCTYPE html>
761 <html>
762 <head>
763 <!-- HTML meta refresh URL redirection -->
764 <meta http-equiv="refresh" content="0; url=%s">
765 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37766 </html>""" % to_html_relative_path)
767 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33768 f.write(content)
769
770
Max Moroz7c5354f2018-05-06 00:03:48771def _CleanUpOutputDir():
772 """Perform a cleanup of the output dir."""
773 # Remove the default index.html file produced by llvm-cov.
774 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
775 if os.path.exists(index_path):
776 os.remove(index_path)
777
778
Yuke Liaodd1ec0592018-02-02 01:26:37779def _GetCoverageHtmlReportPathForFile(file_path):
780 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40781 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37782 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
783
784 # '+' is used instead of os.path.join because both of them are absolute paths
785 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14786 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37787 return _GetCoverageReportRootDirPath() + html_report_path
788
789
790def _GetCoverageHtmlReportPathForDirectory(dir_path):
791 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40792 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37793 html_report_path = os.path.join(
794 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
795
796 # '+' is used instead of os.path.join because both of them are absolute paths
797 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14798 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37799 return _GetCoverageReportRootDirPath() + html_report_path
800
801
802def _GetCoverageHtmlReportPathForComponent(component_name):
803 """Given a component, returns the corresponding html report path."""
804 component_file_name = component_name.lower().replace('>', '-')
805 html_report_name = os.extsep.join([component_file_name, 'html'])
806 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
807 html_report_name)
808
809
810def _GetCoverageReportRootDirPath():
811 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48812 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
813
814
815def _GetComponentViewPath():
816 """Path to the HTML file for the component view."""
817 return os.path.join(_GetCoverageReportRootDirPath(),
818 COMPONENT_VIEW_INDEX_FILE)
819
820
821def _GetDirectoryViewPath():
822 """Path to the HTML file for the directory view."""
823 return os.path.join(_GetCoverageReportRootDirPath(),
824 DIRECTORY_VIEW_INDEX_FILE)
825
826
827def _GetFileViewPath():
828 """Path to the HTML file for the file view."""
829 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
830
831
832def _GetLogsDirectoryPath():
833 """Path to the logs directory."""
834 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
835
836
837def _GetHtmlIndexPath():
838 """Path to the main HTML index file."""
839 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
840
841
842def _GetProfdataFilePath():
843 """Path to the resulting .profdata file."""
844 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
845
846
847def _GetSummaryFilePath():
848 """The JSON file that contains coverage summary written by llvm-cov export."""
849 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33850
851
Yuke Liao506e8822017-12-04 16:52:54852def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
853 """Builds and runs target to generate the coverage profile data.
854
855 Args:
856 targets: A list of targets to build with coverage instrumentation.
857 commands: A list of commands used to run the targets.
858 jobs_count: Number of jobs to run in parallel for building. If None, a
859 default value is derived based on CPUs availability.
860
861 Returns:
862 A relative path to the generated profdata file.
863 """
864 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02865 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59866 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02867 coverage_profdata_file_path = (
868 _CreateCoverageProfileDataFromTargetProfDataFiles(
869 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54870
Abhishek Aryac19bc5ef2018-05-04 22:10:02871 for target_profdata_file_path in target_profdata_file_paths:
872 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52873
Abhishek Aryac19bc5ef2018-05-04 22:10:02874 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54875
876
877def _BuildTargets(targets, jobs_count):
878 """Builds target with Clang coverage instrumentation.
879
880 This function requires current working directory to be the root of checkout.
881
882 Args:
883 targets: A list of targets to build with coverage instrumentation.
884 jobs_count: Number of jobs to run in parallel for compilation. If None, a
885 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54886 """
Abhishek Arya1ec832c2017-12-05 18:06:59887
Yuke Liao506e8822017-12-04 16:52:54888 def _IsGomaConfigured():
889 """Returns True if goma is enabled in the gn build args.
890
891 Returns:
892 A boolean indicates whether goma is configured for building or not.
893 """
Yuke Liao80afff32018-03-07 01:26:20894 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54895 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
896
Abhishek Aryafb70b532018-05-06 17:47:40897 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54898 if jobs_count is None and _IsGomaConfigured():
899 jobs_count = DEFAULT_GOMA_JOBS
900
901 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
902 if jobs_count is not None:
903 subprocess_cmd.append('-j' + str(jobs_count))
904
905 subprocess_cmd.extend(targets)
906 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40907 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54908
909
Abhishek Aryac19bc5ef2018-05-04 22:10:02910def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54911 """Runs commands and returns the relative paths to the profraw data files.
912
913 Args:
914 targets: A list of targets built with coverage instrumentation.
915 commands: A list of commands used to run the targets.
916
917 Returns:
918 A list of relative paths to the generated profraw data files.
919 """
Abhishek Aryafb70b532018-05-06 17:47:40920 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10921
Yuke Liao506e8822017-12-04 16:52:54922 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48923 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54924 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48925 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
926
927 # Ensure that logs directory exists.
928 if not os.path.exists(_GetLogsDirectoryPath()):
929 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54930
Abhishek Aryac19bc5ef2018-05-04 22:10:02931 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10932
Yuke Liaod4a9865202018-01-12 23:17:52933 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54934 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48935 output_file_name = os.extsep.join([target + '_output', 'log'])
936 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10937
Abhishek Aryac19bc5ef2018-05-04 22:10:02938 profdata_file_path = None
939 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40940 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02941 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10942
Abhishek Aryac19bc5ef2018-05-04 22:10:02943 if _IsIOSCommand(command):
944 # On iOS platform, due to lack of write permissions, profraw files are
945 # generated outside of the OUTPUT_DIR, and the exact paths are contained
946 # in the output of the command execution.
947 output = _ExecuteIOSCommand(target, command)
948 else:
949 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
950 output = _ExecuteCommand(target, command)
951
952 with open(output_file_path, 'w') as output_file:
953 output_file.write(output)
954
955 profraw_file_paths = []
956 if _IsIOS():
957 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
958 else:
Max Moroz7c5354f2018-05-06 00:03:48959 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02960 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48961 profraw_file_paths.append(
962 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02963
964 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40965 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02966 'please make sure the binary exists and is properly '
967 'instrumented.' % target)
968
969 try:
970 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
971 target, profraw_file_paths)
972 break
973 except Exception:
974 print('Retrying...')
975 finally:
976 # Remove profraw files now so that they are not used in next iteration.
977 for profraw_file_path in profraw_file_paths:
978 os.remove(profraw_file_path)
979
980 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40981 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02982 'Please file a bug with command you used, commit position and args.gn '
983 'config here: '
984 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40985 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02986 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54987
Abhishek Aryafb70b532018-05-06 17:47:40988 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10989
Abhishek Aryac19bc5ef2018-05-04 22:10:02990 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54991
992
993def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10994 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52995 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01996 #
Max Morozd73e45f2018-04-24 18:32:47997 # "%p" expands out to the process ID. It's not used by this scripts due to:
998 # 1) If a target program spawns too many processess, it may exhaust all disk
999 # space available. For example, unit_tests writes thousands of .profraw
1000 # files each of size 1GB+.
1001 # 2) If a target binary uses shared libraries, coverage profile data for them
1002 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011003 #
Yuke Liaod4a9865202018-01-12 23:17:521004 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1005 # is specified, the runtime creates a pool of N raw profiles which are used
1006 # for on-line profile merging. The runtime takes care of selecting a raw
1007 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521008 # N must be between 1 and 9. The merge pool specifier can only occur once per
1009 # filename pattern.
1010 #
Max Morozd73e45f2018-04-24 18:32:471011 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011012 #
Max Morozd73e45f2018-04-24 18:32:471013 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1014 # but it's not too big to consume too much computing resource or disk space.
1015 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591016 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011017 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481018 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541019 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541020
Yuke Liaoa0c8c2f2018-02-28 20:14:101021 try:
Max Moroz7c5354f2018-05-06 00:03:481022 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101023 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291024 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481025 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191026 env={
1027 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1028 'PATH': _GetPathWithLLVMSymbolizerDir()
1029 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101030 except subprocess.CalledProcessError as e:
1031 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191032 logging.warning(
1033 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1034 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101035
1036 return output
1037
1038
Yuke Liao27349c92018-03-22 21:10:011039def _IsFuzzerTarget(target):
1040 """Returns true if the target is a fuzzer target."""
1041 build_args = _GetBuildArgs()
1042 use_libfuzzer = ('use_libfuzzer' in build_args and
1043 build_args['use_libfuzzer'] == 'true')
1044 return use_libfuzzer and target.endswith('_fuzzer')
1045
1046
Yuke Liaob2926832018-03-02 17:34:291047def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101048 """Runs a single iOS command and generates a profraw data file.
1049
1050 iOS application doesn't have write access to folders outside of the app, so
1051 it's impossible to instruct the app to flush the profraw data file to the
1052 desired location. The profraw data file will be generated somewhere within the
1053 application's Documents folder, and the full path can be obtained by parsing
1054 the output.
1055 """
Yuke Liaob2926832018-03-02 17:34:291056 assert _IsIOSCommand(command)
1057
1058 # After running tests, iossim generates a profraw data file, it won't be
1059 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1060 # checkout.
1061 iossim_profraw_file_path = os.path.join(
1062 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101063
1064 try:
Yuke Liaob2926832018-03-02 17:34:291065 output = subprocess.check_output(
1066 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191067 env={
1068 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1069 'PATH': _GetPathWithLLVMSymbolizerDir()
1070 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101071 except subprocess.CalledProcessError as e:
1072 # iossim emits non-zero return code even if tests run successfully, so
1073 # ignore the return code.
1074 output = e.output
1075
1076 return output
1077
1078
1079def _GetProfrawDataFileByParsingOutput(output):
1080 """Returns the path to the profraw data file obtained by parsing the output.
1081
1082 The output of running the test target has no format, but it is guaranteed to
1083 have a single line containing the path to the generated profraw data file.
1084 NOTE: This should only be called when target os is iOS.
1085 """
Yuke Liaob2926832018-03-02 17:34:291086 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101087
Yuke Liaob2926832018-03-02 17:34:291088 output_by_lines = ''.join(output).splitlines()
1089 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101090
1091 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291092 result = profraw_file_pattern.match(line)
1093 if result:
1094 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101095
1096 assert False, ('No profraw data file was generated, did you call '
1097 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1098 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541099
1100
Abhishek Aryac19bc5ef2018-05-04 22:10:021101def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1102 """Returns a relative path to coverage profdata file by merging target
1103 profdata files.
Yuke Liao506e8822017-12-04 16:52:541104
1105 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021106 profdata_file_paths: A list of relative paths to the profdata data files
1107 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541108
1109 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021110 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541111
1112 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021113 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541114 """
Abhishek Aryafb70b532018-05-06 17:47:401115 logging.info('Creating the coverage profile data file.')
1116 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481117 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541118 try:
Abhishek Arya1ec832c2017-12-05 18:06:591119 subprocess_cmd = [
1120 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1121 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021122 subprocess_cmd.extend(profdata_file_paths)
1123 subprocess.check_call(subprocess_cmd)
1124 except subprocess.CalledProcessError as error:
1125 print('Failed to merge target profdata files to create coverage profdata. '
1126 'Try again.')
1127 raise error
1128
Abhishek Aryafb70b532018-05-06 17:47:401129 logging.debug('Finished merging target profdata files.')
1130 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021131 profdata_file_path)
1132 return profdata_file_path
1133
1134
1135def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1136 """Returns a relative path to target profdata file by merging target
1137 profraw files.
1138
1139 Args:
1140 profraw_file_paths: A list of relative paths to the profdata data files
1141 that are to be merged.
1142
1143 Returns:
1144 A relative path to the merged coverage profdata file.
1145
1146 Raises:
1147 CalledProcessError: An error occurred merging profdata files.
1148 """
Abhishek Aryafb70b532018-05-06 17:47:401149 logging.info('Creating target profile data file.')
1150 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021151 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1152
1153 try:
1154 subprocess_cmd = [
1155 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1156 ]
Yuke Liao506e8822017-12-04 16:52:541157 subprocess_cmd.extend(profraw_file_paths)
1158 subprocess.check_call(subprocess_cmd)
1159 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021160 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541161 raise error
1162
Abhishek Aryafb70b532018-05-06 17:47:401163 logging.debug('Finished merging target profraw files.')
1164 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101165 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541166 return profdata_file_path
1167
1168
Yuke Liao0e4c8682018-04-18 21:06:591169def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1170 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331171 """Generates per file coverage summary using "llvm-cov export" command."""
1172 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1173 # [[-object BIN]] [SOURCES].
1174 # NOTE: For object files, the first one is specified as a positional argument,
1175 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101176 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401177 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331178 subprocess_cmd = [
1179 LLVM_COV_PATH, 'export', '-summary-only',
1180 '-instr-profile=' + profdata_file_path, binary_paths[0]
1181 ]
1182 subprocess_cmd.extend(
1183 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291184 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331185 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591186 if ignore_filename_regex:
1187 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331188
Max Moroz7c5354f2018-05-06 00:03:481189 export_output = subprocess.check_output(subprocess_cmd)
1190
1191 # Write output on the disk to be used by code coverage bot.
1192 with open(_GetSummaryFilePath(), 'w') as f:
1193 f.write(export_output)
1194
1195 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331196 assert len(json_output['data']) == 1
1197 files_coverage_data = json_output['data'][0]['files']
1198
1199 per_file_coverage_summary = {}
1200 for file_coverage_data in files_coverage_data:
1201 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401202 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1203 'File path "%s" in coverage summary is outside source checkout.' %
1204 file_path)
Yuke Liaoea228d02018-01-05 19:10:331205
Abhishek Aryafb70b532018-05-06 17:47:401206 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331207 if summary['lines']['count'] == 0:
1208 continue
1209
1210 per_file_coverage_summary[file_path] = _CoverageSummary(
1211 regions_total=summary['regions']['count'],
1212 regions_covered=summary['regions']['covered'],
1213 functions_total=summary['functions']['count'],
1214 functions_covered=summary['functions']['covered'],
1215 lines_total=summary['lines']['count'],
1216 lines_covered=summary['lines']['covered'])
1217
Abhishek Aryafb70b532018-05-06 17:47:401218 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331219 return per_file_coverage_summary
1220
1221
Yuke Liaob2926832018-03-02 17:34:291222def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1223 """Appends -arch arguments to the command list if it's ios platform.
1224
1225 iOS binaries are universal binaries, and require specifying the architecture
1226 to use, and one architecture needs to be specified for each binary.
1227 """
1228 if _IsIOS():
1229 cmd_list.extend(['-arch=x86_64'] * num_archs)
1230
1231
Yuke Liao506e8822017-12-04 16:52:541232def _GetBinaryPath(command):
1233 """Returns a relative path to the binary to be run by the command.
1234
Yuke Liao545db322018-02-15 17:12:011235 Currently, following types of commands are supported (e.g. url_unittests):
1236 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1237 2. Use xvfb.
1238 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1239 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371240 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1241 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101242 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371243 <iossim_arguments> -c <app_arguments>
1244 out/Coverage-iphonesimulator/url_unittests.app"
1245
Yuke Liao545db322018-02-15 17:12:011246
Yuke Liao506e8822017-12-04 16:52:541247 Args:
1248 command: A command used to run a target.
1249
1250 Returns:
1251 A relative path to the binary.
1252 """
Yuke Liao545db322018-02-15 17:12:011253 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1254
Yuke Liaob2926832018-03-02 17:34:291255 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011256 if os.path.basename(command_parts[0]) == 'python':
1257 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401258 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011259 return command_parts[2]
1260
1261 if os.path.basename(command_parts[0]) == xvfb_script_name:
1262 return command_parts[1]
1263
Yuke Liaob2926832018-03-02 17:34:291264 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101265 # For a given application bundle, the binary resides in the bundle and has
1266 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371267 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101268 app_name = os.path.splitext(os.path.basename(app_path))[0]
1269 return os.path.join(app_path, app_name)
1270
Yuke Liaob2926832018-03-02 17:34:291271 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541272
1273
Yuke Liaob2926832018-03-02 17:34:291274def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101275 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291276 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101277
1278
Yuke Liao95d13d72017-12-07 18:18:501279def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1280 """Verifies that the target executables specified in the commands are inside
1281 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541282 for command in commands:
1283 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501284 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481285 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501286 'Target executable "%s" in command: "%s" is outside of '
1287 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541288
1289
1290def _ValidateBuildingWithClangCoverage():
1291 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201292 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541293
1294 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1295 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591296 assert False, ('\'{} = true\' is required in args.gn.'
1297 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541298
1299
Yuke Liaoc60b2d02018-03-02 21:40:431300def _ValidateCurrentPlatformIsSupported():
1301 """Asserts that this script suports running on the current platform"""
1302 target_os = _GetTargetOS()
1303 if target_os:
1304 current_platform = target_os
1305 else:
1306 current_platform = _GetHostPlatform()
1307
1308 assert current_platform in [
1309 'linux', 'mac', 'chromeos', 'ios'
1310 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1311
1312
Yuke Liao80afff32018-03-07 01:26:201313def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541314 """Parses args.gn file and returns results as a dictionary.
1315
1316 Returns:
1317 A dictionary representing the build args.
1318 """
Yuke Liao80afff32018-03-07 01:26:201319 global _BUILD_ARGS
1320 if _BUILD_ARGS is not None:
1321 return _BUILD_ARGS
1322
1323 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541324 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1325 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1326 'missing args.gn file.' % BUILD_DIR)
1327 with open(build_args_path) as build_args_file:
1328 build_args_lines = build_args_file.readlines()
1329
Yuke Liao506e8822017-12-04 16:52:541330 for build_arg_line in build_args_lines:
1331 build_arg_without_comments = build_arg_line.split('#')[0]
1332 key_value_pair = build_arg_without_comments.split('=')
1333 if len(key_value_pair) != 2:
1334 continue
1335
1336 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431337
1338 # Values are wrapped within a pair of double-quotes, so remove the leading
1339 # and trailing double-quotes.
1340 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201341 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541342
Yuke Liao80afff32018-03-07 01:26:201343 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541344
1345
Abhishek Arya16f059a2017-12-07 17:47:321346def _VerifyPathsAndReturnAbsolutes(paths):
1347 """Verifies that the paths specified in |paths| exist and returns absolute
1348 versions.
Yuke Liao66da1732017-12-05 22:19:421349
1350 Args:
1351 paths: A list of files or directories.
1352 """
Abhishek Arya16f059a2017-12-07 17:47:321353 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421354 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321355 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1356 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1357
1358 absolute_paths.append(absolute_path)
1359
1360 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421361
1362
Yuke Liaodd1ec0592018-02-02 01:26:371363def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1364 """Returns a target path relative to the directory of base_path.
1365
1366 This method requires base_path to be a file, otherwise, one should call
1367 os.path.relpath directly.
1368 """
1369 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141370 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371371 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141372 base_dir = os.path.dirname(base_path)
1373 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371374
1375
Abhishek Arya64636af2018-05-04 14:42:131376def _GetBinaryPathsFromTargets(targets, build_dir):
1377 """Return binary paths from target names."""
1378 # FIXME: Derive output binary from target build definitions rather than
1379 # assuming that it is always the same name.
1380 binary_paths = []
1381 for target in targets:
1382 binary_path = os.path.join(build_dir, target)
1383 if _GetHostPlatform() == 'win':
1384 binary_path += '.exe'
1385
1386 if os.path.exists(binary_path):
1387 binary_paths.append(binary_path)
1388 else:
1389 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401390 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131391 os.path.basename(binary_path))
1392
1393 return binary_paths
1394
1395
Yuke Liao506e8822017-12-04 16:52:541396def _ParseCommandArguments():
1397 """Adds and parses relevant arguments for tool comands.
1398
1399 Returns:
1400 A dictionary representing the arguments.
1401 """
1402 arg_parser = argparse.ArgumentParser()
1403 arg_parser.usage = __doc__
1404
Abhishek Arya1ec832c2017-12-05 18:06:591405 arg_parser.add_argument(
1406 '-b',
1407 '--build-dir',
1408 type=str,
1409 required=True,
1410 help='The build directory, the path needs to be relative to the root of '
1411 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541412
Abhishek Arya1ec832c2017-12-05 18:06:591413 arg_parser.add_argument(
1414 '-o',
1415 '--output-dir',
1416 type=str,
1417 required=True,
1418 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541419
Abhishek Arya1ec832c2017-12-05 18:06:591420 arg_parser.add_argument(
1421 '-c',
1422 '--command',
1423 action='append',
Abhishek Arya64636af2018-05-04 14:42:131424 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591425 help='Commands used to run test targets, one test target needs one and '
1426 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131427 'current working directory is the root of the checkout. This option is '
1428 'incompatible with -p/--profdata-file option.')
1429
1430 arg_parser.add_argument(
1431 '-p',
1432 '--profdata-file',
1433 type=str,
1434 required=False,
1435 help='Path to profdata file to use for generating code coverage reports. '
1436 'This can be useful if you generated the profdata file seperately in '
1437 'your own test harness. This option is ignored if run command(s) are '
1438 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541439
Abhishek Arya1ec832c2017-12-05 18:06:591440 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421441 '-f',
1442 '--filters',
1443 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321444 required=False,
Yuke Liao66da1732017-12-05 22:19:421445 help='Directories or files to get code coverage for, and all files under '
1446 'the directories are included recursively.')
1447
1448 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591449 '-i',
1450 '--ignore-filename-regex',
1451 type=str,
1452 help='Skip source code files with file paths that match the given '
1453 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1454 'to exclude files in third_party/ and out/ folders from the report.')
1455
1456 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591457 '-j',
1458 '--jobs',
1459 type=int,
1460 default=None,
1461 help='Run N jobs to build in parallel. If not specified, a default value '
1462 'will be derived based on CPUs availability. Please refer to '
1463 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541464
Abhishek Arya1ec832c2017-12-05 18:06:591465 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101466 '-v',
1467 '--verbose',
1468 action='store_true',
1469 help='Prints additional output for diagnostics.')
1470
1471 arg_parser.add_argument(
1472 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1473
1474 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021475 'targets',
1476 nargs='+',
1477 help='The names of the test targets to run. If multiple run commands are '
1478 'specified using the -c/--command option, then the order of targets and '
1479 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541480
1481 args = arg_parser.parse_args()
1482 return args
1483
1484
1485def Main():
1486 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131487 # Change directory to source root to aid in relative paths calculations.
1488 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111489
Abhishek Arya64636af2018-05-04 14:42:131490 # Setup coverage binaries even when script is called with empty params. This
1491 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111492 DownloadCoverageToolsIfNeeded()
1493
Yuke Liao506e8822017-12-04 16:52:541494 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131495 _ConfigureLogging(args)
1496
Yuke Liao506e8822017-12-04 16:52:541497 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481498 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541499 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481500 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541501
Abhishek Arya64636af2018-05-04 14:42:131502 assert args.command or args.profdata_file, (
1503 'Need to either provide commands to run using -c/--command option OR '
1504 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431505
Abhishek Arya64636af2018-05-04 14:42:131506 assert not args.command or (len(args.targets) == len(args.command)), (
1507 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431508
Abhishek Arya1ec832c2017-12-05 18:06:591509 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401510 'Build directory: "%s" doesn\'t exist. '
1511 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131512
Yuke Liaoc60b2d02018-03-02 21:40:431513 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541514 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321515
1516 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421517 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321518 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421519
Max Moroz7c5354f2018-05-06 00:03:481520 if not os.path.exists(_GetCoverageReportRootDirPath()):
1521 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541522
Abhishek Arya64636af2018-05-04 14:42:131523 # Get profdate file and list of binary paths.
1524 if args.command:
1525 # A list of commands are provided. Run them to generate profdata file, and
1526 # create a list of binary paths from parsing commands.
1527 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1528 profdata_file_path = _CreateCoverageProfileDataForTargets(
1529 args.targets, args.command, args.jobs)
1530 binary_paths = [_GetBinaryPath(command) for command in args.command]
1531 else:
1532 # An input prof-data file is already provided. Just calculate binary paths.
1533 profdata_file_path = args.profdata_file
1534 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331535
Abhishek Arya78120bc2018-05-07 20:53:541536 binary_paths.extend(_GetSharedLibraries(binary_paths))
1537
Yuke Liao481d3482018-01-29 19:17:101538 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401539 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371540 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591541 binary_paths, profdata_file_path, absolute_filter_paths,
1542 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371543 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591544 absolute_filter_paths,
1545 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371546 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1547
1548 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1549 per_file_coverage_summary)
1550 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1551 per_file_coverage_summary)
1552 _GenerateDirectoryViewHtmlIndexFile()
1553
1554 component_to_directories = _ExtractComponentToDirectoriesMapping()
1555 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1556 component_to_directories, per_directory_coverage_summary)
1557 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1558 component_to_directories,
1559 per_directory_coverage_summary)
1560 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331561
1562 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371563 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331564 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481565 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331566
Max Moroz7c5354f2018-05-06 00:03:481567 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401568 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101569 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541570
Abhishek Arya1ec832c2017-12-05 18:06:591571
Yuke Liao506e8822017-12-04 16:52:541572if __name__ == '__main__':
1573 sys.exit(Main())