blob: 7f63a02394457273b65bfd402a6dad66d6db8d9a [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
Abhishek Arya1ec832c2017-12-05 18:06:5914 Example usage:
15
Abhishek Arya16f059a2017-12-07 17:47:3216 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
17 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5918 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3219 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
20 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
21 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5922
Abhishek Arya16f059a2017-12-07 17:47:3223 The command above builds crypto_unittests and url_unittests targets and then
24 runs them with specified command line arguments. For url_unittests, it only
25 runs the test URLParser.PathURL. The coverage report is filtered to include
26 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5927
Yuke Liao545db322018-02-15 17:12:0128 If you want to run tests that try to draw to the screen but don't have a
29 display connected, you can run tests in headless mode with xvfb.
30
31 Sample flow for running a test target with xvfb (e.g. unit_tests):
32
33 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
34 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
35
Abhishek Arya1ec832c2017-12-05 18:06:5936 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
37 flag as well.
38
39 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
40
Abhishek Arya16f059a2017-12-07 17:47:3241 python tools/code_coverage/coverage.py pdfium_fuzzer \\
42 -b out/coverage -o out/report \\
43 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
44 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5945
46 where:
47 <corpus_dir> - directory containing samples files for this format.
48 <runs> - number of times to fuzz target function. Should be 0 when you just
49 want to see the coverage on corpus and don't want to fuzz at all.
50
51 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3852
53 For an overview of how code coverage works in Chromium, please refer to
54 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5455"""
56
57from __future__ import print_function
58
59import sys
60
61import argparse
Yuke Liaoea228d02018-01-05 19:10:3362import json
Yuke Liao481d3482018-01-29 19:17:1063import logging
Yuke Liao506e8822017-12-04 16:52:5464import os
Yuke Liaob2926832018-03-02 17:34:2965import re
66import shlex
Max Moroz025d8952018-05-03 16:33:3467import shutil
Yuke Liao506e8822017-12-04 16:52:5468import subprocess
Yuke Liao506e8822017-12-04 16:52:5469import urllib2
70
Abhishek Arya1ec832c2017-12-05 18:06:5971sys.path.append(
72 os.path.join(
73 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
74 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5475import update as clang_update
76
Yuke Liaoea228d02018-01-05 19:10:3377sys.path.append(
78 os.path.join(
79 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
80 'third_party'))
81import jinja2
82from collections import defaultdict
83
Yuke Liao506e8822017-12-04 16:52:5484# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5985SRC_ROOT_PATH = os.path.abspath(
86 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5487
88# Absolute path to the code coverage tools binary.
89LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:1990LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
91LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
92LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5493
94# Build directory, the value is parsed from command line arguments.
95BUILD_DIR = None
96
97# Output directory for generated artifacts, the value is parsed from command
98# line arguemnts.
99OUTPUT_DIR = None
100
101# Default number of jobs used to build when goma is configured and enabled.
102DEFAULT_GOMA_JOBS = 100
103
104# Name of the file extension for profraw data files.
105PROFRAW_FILE_EXTENSION = 'profraw'
106
107# Name of the final profdata file, and this file needs to be passed to
108# "llvm-cov" command in order to call "llvm-cov show" to inspect the
109# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48110PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
111
112# Name of the file with summary information generated by llvm-cov export.
113SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54114
115# Build arg required for generating code coverage data.
116CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
117
Yuke Liaoea228d02018-01-05 19:10:33118# The default name of the html coverage report for a directory.
119DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
120
Yuke Liaodd1ec0592018-02-02 01:26:37121# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37122COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48123DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37124FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48125INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
126
127LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37128
129# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19130COMPONENT_MAPPING_URL = (
131 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37132
Yuke Liao80afff32018-03-07 01:26:20133# Caches the results returned by _GetBuildArgs, don't use this variable
134# directly, call _GetBuildArgs instead.
135_BUILD_ARGS = None
136
Abhishek Aryac19bc5ef2018-05-04 22:10:02137# Retry failed merges.
138MERGE_RETRIES = 3
139
Abhishek Aryad35de7e2018-05-10 22:23:04140# Message to guide user to file a bug when everything else fails.
141FILE_BUG_MESSAGE = (
142 'If it persists, please file a bug with the command you used, git revision '
143 'and args.gn config here: '
144 'https://bugs.chromium.org/p/chromium/issues/entry?'
145 'components=Tools%3ECodeCoverage')
146
Yuke Liaoea228d02018-01-05 19:10:33147
148class _CoverageSummary(object):
149 """Encapsulates coverage summary representation."""
150
Yuke Liaodd1ec0592018-02-02 01:26:37151 def __init__(self,
152 regions_total=0,
153 regions_covered=0,
154 functions_total=0,
155 functions_covered=0,
156 lines_total=0,
157 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33158 """Initializes _CoverageSummary object."""
159 self._summary = {
160 'regions': {
161 'total': regions_total,
162 'covered': regions_covered
163 },
164 'functions': {
165 'total': functions_total,
166 'covered': functions_covered
167 },
168 'lines': {
169 'total': lines_total,
170 'covered': lines_covered
171 }
172 }
173
174 def Get(self):
175 """Returns summary as a dictionary."""
176 return self._summary
177
178 def AddSummary(self, other_summary):
179 """Adds another summary to this one element-wise."""
180 for feature in self._summary:
181 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
182 self._summary[feature]['covered'] += other_summary.Get()[feature][
183 'covered']
184
185
Yuke Liaodd1ec0592018-02-02 01:26:37186class _CoverageReportHtmlGenerator(object):
187 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33188
Yuke Liaodd1ec0592018-02-02 01:26:37189 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33190 """
191
Yuke Liaodd1ec0592018-02-02 01:26:37192 def __init__(self, output_path, table_entry_type):
193 """Initializes _CoverageReportHtmlGenerator object.
194
195 Args:
196 output_path: Path to the html report that will be generated.
197 table_entry_type: Type of the table entries to be displayed in the table
198 header. For example: 'Path', 'Component'.
199 """
Yuke Liaoea228d02018-01-05 19:10:33200 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48201 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33202 assert os.path.exists(css_absolute_path), (
203 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40204 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33205 css_absolute_path)
206
207 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37208 self._output_path = output_path
209 self._table_entry_type = table_entry_type
210
Yuke Liaoea228d02018-01-05 19:10:33211 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12212 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23213
214 source_dir = os.path.dirname(os.path.realpath(__file__))
215 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33216
217 jinja_env = jinja2.Environment(
218 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
219 self._header_template = jinja_env.get_template('header.html')
220 self._table_template = jinja_env.get_template('table.html')
221 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23222
Abhishek Arya865fffd2018-05-08 22:16:01223 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23224 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33225
226 def AddLinkToAnotherReport(self, html_report_path, name, summary):
227 """Adds a link to another html report in this report.
228
229 The link to be added is assumed to be an entry in this directory.
230 """
Yuke Liaodd1ec0592018-02-02 01:26:37231 # Use relative paths instead of absolute paths to make the generated reports
232 # portable.
233 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
234 html_report_path, self._output_path)
235
Yuke Liaod54030e2018-01-08 17:34:12236 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37237 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12238 os.path.basename(html_report_path) ==
239 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
240 self._table_entries.append(table_entry)
241
242 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35243 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12244 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
245
246 def _CreateTableEntryFromCoverageSummary(self,
247 summary,
248 href=None,
249 name=None,
250 is_dir=None):
251 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37252 assert (href is None and name is None and is_dir is None) or (
253 href is not None and name is not None and is_dir is not None), (
254 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35255 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37256 'attributes must be None.')
257
Yuke Liaod54030e2018-01-08 17:34:12258 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37259 if href is not None:
260 entry['href'] = href
261 if name is not None:
262 entry['name'] = name
263 if is_dir is not None:
264 entry['is_dir'] = is_dir
265
Yuke Liaoea228d02018-01-05 19:10:33266 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12267 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37268 if summary_dict[feature]['total'] == 0:
269 percentage = 0.0
270 else:
Yuke Liao0e4c8682018-04-18 21:06:59271 percentage = float(summary_dict[feature]
272 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35273
Yuke Liaoea228d02018-01-05 19:10:33274 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12275 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33276 'total': summary_dict[feature]['total'],
277 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35278 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33279 'color_class': color_class
280 }
Yuke Liaod54030e2018-01-08 17:34:12281
Yuke Liaod54030e2018-01-08 17:34:12282 return entry
Yuke Liaoea228d02018-01-05 19:10:33283
284 def _GetColorClass(self, percentage):
285 """Returns the css color class based on coverage percentage."""
286 if percentage >= 0 and percentage < 80:
287 return 'red'
288 if percentage >= 80 and percentage < 100:
289 return 'yellow'
290 if percentage == 100:
291 return 'green'
292
Abhishek Aryafb70b532018-05-06 17:47:40293 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33294
Yuke Liao1b852fd2018-05-11 17:07:32295 def WriteHtmlCoverageReport(self, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37296 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33297
298 In the report, sub-directories are displayed before files and within each
299 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33300 """
301
302 def EntryCmp(left, right):
303 """Compare function for table entries."""
304 if left['is_dir'] != right['is_dir']:
305 return -1 if left['is_dir'] == True else 1
306
Yuke Liaodd1ec0592018-02-02 01:26:37307 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33308
309 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
310
311 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48312
313 directory_view_path = _GetDirectoryViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32314 directory_view_href = _GetRelativePathToDirectoryOfFile(
315 directory_view_path, self._output_path)
Max Moroz7c5354f2018-05-06 00:03:48316 component_view_path = _GetComponentViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32317 component_view_href = _GetRelativePathToDirectoryOfFile(
318 component_view_path, self._output_path)
319
320 # File view is optional in the report.
321 file_view_href = None
322 if not no_file_view:
323 file_view_path = _GetFileViewPath()
324 file_view_href = _GetRelativePathToDirectoryOfFile(
325 file_view_path, self._output_path)
Yuke Liaodd1ec0592018-02-02 01:26:37326
Yuke Liaoea228d02018-01-05 19:10:33327 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37328 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
Yuke Liao1b852fd2018-05-11 17:07:32329 directory_view_href=directory_view_href,
330 component_view_href=component_view_href,
331 file_view_href=file_view_href,
Abhishek Arya865fffd2018-05-08 22:16:01332 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37333
Yuke Liaod54030e2018-01-08 17:34:12334 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37335 entries=self._table_entries,
336 total_entry=self._total_entry,
337 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33338 html_footer = self._footer_template.render()
339
Yuke Liaodd1ec0592018-02-02 01:26:37340 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33341 html_file.write(html_header + html_table + html_footer)
342
Yuke Liao506e8822017-12-04 16:52:54343
Abhishek Arya64636af2018-05-04 14:42:13344def _ConfigureLogging(args):
345 """Configures logging settings for later use."""
346 log_level = logging.DEBUG if args.verbose else logging.INFO
347 log_format = '[%(asctime)s %(levelname)s] %(message)s'
348 log_file = args.log_file if args.log_file else None
349 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
350
351
Max Morozd73e45f2018-04-24 18:32:47352def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54353 """Returns list of shared libraries used by specified binaries."""
354 logging.info('Finding shared libraries for targets (if any).')
355 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47356 cmd = []
357 shared_library_re = None
358
359 if sys.platform.startswith('linux'):
360 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13361 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
362 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47363 elif sys.platform.startswith('darwin'):
364 cmd.extend(['otool', '-L'])
365 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
366 else:
Abhishek Aryafb70b532018-05-06 17:47:40367 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47368
369 assert shared_library_re is not None
370
371 cmd.extend(binary_paths)
372 output = subprocess.check_output(cmd)
373
374 for line in output.splitlines():
375 m = shared_library_re.match(line)
376 if not m:
377 continue
378
379 shared_library_path = m.group(1)
380 if sys.platform.startswith('darwin'):
381 # otool outputs "@rpath" macro instead of the dirname of the given binary.
382 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
383
Abhishek Arya78120bc2018-05-07 20:53:54384 if shared_library_path in shared_libraries:
385 continue
386
Max Morozd73e45f2018-04-24 18:32:47387 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
388 'the given target(s) does not '
389 'exist.' % shared_library_path)
390 with open(shared_library_path) as f:
391 data = f.read()
392
393 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
394 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54395 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47396
Abhishek Arya78120bc2018-05-07 20:53:54397 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
398 shared_libraries)
399 logging.info('Finished finding shared libraries for targets.')
400 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47401
402
Yuke Liaoc60b2d02018-03-02 21:40:43403def _GetHostPlatform():
404 """Returns the host platform.
405
406 This is separate from the target platform/os that coverage is running for.
407 """
Abhishek Arya1ec832c2017-12-05 18:06:59408 if sys.platform == 'win32' or sys.platform == 'cygwin':
409 return 'win'
410 if sys.platform.startswith('linux'):
411 return 'linux'
412 else:
413 assert sys.platform == 'darwin'
414 return 'mac'
415
416
Abhishek Arya1c97ea542018-05-10 03:53:19417def _GetPathWithLLVMSymbolizerDir():
418 """Add llvm-symbolizer directory to path for symbolized stacks."""
419 path = os.getenv('PATH')
420 dirs = path.split(os.pathsep)
421 if LLVM_BIN_DIR in dirs:
422 return path
423
424 return path + os.pathsep + LLVM_BIN_DIR
425
426
Yuke Liaoc60b2d02018-03-02 21:40:43427def _GetTargetOS():
428 """Returns the target os specified in args.gn file.
429
430 Returns an empty string is target_os is not specified.
431 """
Yuke Liao80afff32018-03-07 01:26:20432 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43433 return build_args['target_os'] if 'target_os' in build_args else ''
434
435
Yuke Liaob2926832018-03-02 17:34:29436def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10437 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43438 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10439
440
Yuke Liao506e8822017-12-04 16:52:54441# TODO(crbug.com/759794): remove this function once tools get included to
442# Clang bundle:
443# https://chromium-review.googlesource.com/c/chromium/src/+/688221
444def DownloadCoverageToolsIfNeeded():
445 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59446
Yuke Liaoc60b2d02018-03-02 21:40:43447 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54448 """Returns a pair of revision number by reading the build stamp file.
449
450 Args:
451 stamp_file_path: A path the build stamp file created by
452 tools/clang/scripts/update.py.
453 Returns:
454 A pair of integers represeting the main and sub revision respectively.
455 """
456 if not os.path.exists(stamp_file_path):
457 return 0, 0
458
459 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43460 stamp_file_line = stamp_file.readline()
461 if ',' in stamp_file_line:
462 package_version = stamp_file_line.rstrip().split(',')[0]
463 else:
464 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54465
Yuke Liaoc60b2d02018-03-02 21:40:43466 clang_revision_str, clang_sub_revision_str = package_version.split('-')
467 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59468
Yuke Liaoc60b2d02018-03-02 21:40:43469 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54470 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43471 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54472
473 coverage_revision_stamp_file = os.path.join(
474 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
475 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43476 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54477
Yuke Liaoea228d02018-01-05 19:10:33478 has_coverage_tools = (
479 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32480
Yuke Liaoea228d02018-01-05 19:10:33481 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54482 coverage_sub_revision == clang_sub_revision):
483 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43484 return
Yuke Liao506e8822017-12-04 16:52:54485
486 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
487 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
488
489 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43490 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54491 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43492 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54493 coverage_tools_url = (
494 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43495 else:
496 assert host_platform == 'win'
497 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54498
499 try:
500 clang_update.DownloadAndUnpack(coverage_tools_url,
501 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54502 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43503 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54504 file_handle.write('\n')
505 except urllib2.URLError:
506 raise Exception(
507 'Failed to download coverage tools: %s.' % coverage_tools_url)
508
509
Yuke Liaodd1ec0592018-02-02 01:26:37510def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59511 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54512 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
513
514 For a file with absolute path /a/b/x.cc, a html report is generated as:
515 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
516 OUTPUT_DIR/index.html.
517
518 Args:
519 binary_paths: A list of paths to the instrumented binaries.
520 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42521 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54522 """
Yuke Liao506e8822017-12-04 16:52:54523 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
524 # [[-object BIN]] [SOURCES]
525 # NOTE: For object files, the first one is specified as a positional argument,
526 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10527 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40528 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59529 subprocess_cmd = [
530 LLVM_COV_PATH, 'show', '-format=html',
531 '-output-dir={}'.format(OUTPUT_DIR),
532 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
533 ]
534 subprocess_cmd.extend(
535 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29536 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Ryan Sleeviae19b2c32018-05-15 22:36:17537 if _GetHostPlatform() in ['linux', 'mac']:
538 subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
Yuke Liao66da1732017-12-05 22:19:42539 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59540 if ignore_filename_regex:
541 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
542
Yuke Liao506e8822017-12-04 16:52:54543 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34544
545 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
546 # the platform name instead, as it simplifies the report dir structure when
547 # the same report is generated for different platforms.
548 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48549 platform_report_subdir_path = _GetCoverageReportRootDirPath()
550 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34551
Abhishek Aryafb70b532018-05-06 17:47:40552 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54553
554
Yuke Liaodd1ec0592018-02-02 01:26:37555def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
556 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48557 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37558 logging.debug('Generating file view html index file as: "%s".',
559 file_view_index_file_path)
560 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
561 'Path')
562 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33563
Yuke Liaodd1ec0592018-02-02 01:26:37564 for file_path in per_file_coverage_summary:
565 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
566
567 html_generator.AddLinkToAnotherReport(
568 _GetCoverageHtmlReportPathForFile(file_path),
569 os.path.relpath(file_path, SRC_ROOT_PATH),
570 per_file_coverage_summary[file_path])
571
572 html_generator.CreateTotalsEntry(totals_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:32573 html_generator.WriteHtmlCoverageReport(no_file_view=False)
Yuke Liaodd1ec0592018-02-02 01:26:37574 logging.debug('Finished generating file view html index file.')
575
576
577def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
578 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40579 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37580 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
581
Yuke Liaoea228d02018-01-05 19:10:33582 for file_path in per_file_coverage_summary:
583 summary = per_file_coverage_summary[file_path]
584 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40585
Yuke Liaoea228d02018-01-05 19:10:33586 while True:
587 per_directory_coverage_summary[parent_dir].AddSummary(summary)
588
589 if parent_dir == SRC_ROOT_PATH:
590 break
591 parent_dir = os.path.dirname(parent_dir)
592
Abhishek Aryafb70b532018-05-06 17:47:40593 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37594 return per_directory_coverage_summary
595
596
Yuke Liao1b852fd2018-05-11 17:07:32597def _GeneratePerDirectoryCoverageInHtml(
598 per_directory_coverage_summary, per_file_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37599 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40600 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33601 for dir_path in per_directory_coverage_summary:
Yuke Liao1b852fd2018-05-11 17:07:32602 _GenerateCoverageInHtmlForDirectory(dir_path,
603 per_directory_coverage_summary,
604 per_file_coverage_summary, no_file_view)
Yuke Liaoea228d02018-01-05 19:10:33605
Abhishek Aryafb70b532018-05-06 17:47:40606 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10607
Yuke Liaoea228d02018-01-05 19:10:33608
609def _GenerateCoverageInHtmlForDirectory(
Yuke Liao1b852fd2018-05-11 17:07:32610 dir_path, per_directory_coverage_summary, per_file_coverage_summary,
611 no_file_view):
Yuke Liaoea228d02018-01-05 19:10:33612 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37613 html_generator = _CoverageReportHtmlGenerator(
614 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33615
616 for entry_name in os.listdir(dir_path):
617 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33618
Yuke Liaodd1ec0592018-02-02 01:26:37619 if entry_path in per_file_coverage_summary:
620 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
621 entry_coverage_summary = per_file_coverage_summary[entry_path]
622 elif entry_path in per_directory_coverage_summary:
623 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
624 entry_path)
625 entry_coverage_summary = per_directory_coverage_summary[entry_path]
626 else:
Yuke Liaoc7e607142018-02-05 20:26:14627 # Any file without executable lines shouldn't be included into the report.
628 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37629 continue
Yuke Liaoea228d02018-01-05 19:10:33630
Yuke Liaodd1ec0592018-02-02 01:26:37631 html_generator.AddLinkToAnotherReport(entry_html_report_path,
632 os.path.basename(entry_path),
633 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33634
Yuke Liaod54030e2018-01-08 17:34:12635 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liao1b852fd2018-05-11 17:07:32636 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37637
638
639def _GenerateDirectoryViewHtmlIndexFile():
640 """Generates the html index file for directory view.
641
642 Note that the index file is already generated under SRC_ROOT_PATH, so this
643 file simply redirects to it, and the reason of this extra layer is for
644 structural consistency with other views.
645 """
Max Moroz7c5354f2018-05-06 00:03:48646 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37647 logging.debug('Generating directory view html index file as: "%s".',
648 directory_view_index_file_path)
649 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
650 SRC_ROOT_PATH)
651 _WriteRedirectHtmlFile(directory_view_index_file_path,
652 src_root_html_report_path)
653 logging.debug('Finished generating directory view html index file.')
654
655
656def _CalculatePerComponentCoverageSummary(component_to_directories,
657 per_directory_coverage_summary):
658 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40659 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37660 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
661
662 for component in component_to_directories:
663 for directory in component_to_directories[component]:
664 absolute_directory_path = os.path.abspath(directory)
665 if absolute_directory_path in per_directory_coverage_summary:
666 per_component_coverage_summary[component].AddSummary(
667 per_directory_coverage_summary[absolute_directory_path])
668
Abhishek Aryafb70b532018-05-06 17:47:40669 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37670 return per_component_coverage_summary
671
672
673def _ExtractComponentToDirectoriesMapping():
674 """Returns a mapping from components to directories."""
675 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
676 directory_to_component = component_mappings['dir-to-component']
677
678 component_to_directories = defaultdict(list)
Abhishek Arya8c3a1ce322018-05-13 04:14:01679 for directory in sorted(directory_to_component):
Yuke Liaodd1ec0592018-02-02 01:26:37680 component = directory_to_component[directory]
Abhishek Arya8c3a1ce322018-05-13 04:14:01681
682 # Check if we already added the parent directory of this directory. If yes,
683 # skip this sub-directory to avoid double-counting.
684 found_parent_directory = False
685 for component_directory in component_to_directories[component]:
686 if directory.startswith(component_directory + '/'):
687 found_parent_directory = True
688 break
689
690 if not found_parent_directory:
691 component_to_directories[component].append(directory)
Yuke Liaodd1ec0592018-02-02 01:26:37692
693 return component_to_directories
694
695
Yuke Liao1b852fd2018-05-11 17:07:32696def _GeneratePerComponentCoverageInHtml(
697 per_component_coverage_summary, component_to_directories,
698 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37699 """Generates per-component coverage reports in html."""
700 logging.debug('Writing per-component coverage html reports.')
701 for component in per_component_coverage_summary:
702 _GenerateCoverageInHtmlForComponent(
703 component, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32704 per_directory_coverage_summary, no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37705
706 logging.debug('Finished writing per-component coverage html reports.')
707
708
709def _GenerateCoverageInHtmlForComponent(
710 component_name, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32711 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37712 """Generates coverage html report for a component."""
713 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
714 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14715 component_html_report_dir = os.path.dirname(component_html_report_path)
716 if not os.path.exists(component_html_report_dir):
717 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37718
719 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
720 'Path')
721
722 for dir_path in component_to_directories[component_name]:
723 dir_absolute_path = os.path.abspath(dir_path)
724 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14725 # Any directory without an excercised file shouldn't be included into the
726 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37727 continue
728
729 html_generator.AddLinkToAnotherReport(
730 _GetCoverageHtmlReportPathForDirectory(dir_path),
731 os.path.relpath(dir_path, SRC_ROOT_PATH),
732 per_directory_coverage_summary[dir_absolute_path])
733
734 html_generator.CreateTotalsEntry(
735 per_component_coverage_summary[component_name])
Yuke Liao1b852fd2018-05-11 17:07:32736 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37737
738
Yuke Liao1b852fd2018-05-11 17:07:32739def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
740 no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37741 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48742 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37743 logging.debug('Generating component view html index file as: "%s".',
744 component_view_index_file_path)
745 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
746 'Component')
Yuke Liaodd1ec0592018-02-02 01:26:37747 for component in per_component_coverage_summary:
Yuke Liaodd1ec0592018-02-02 01:26:37748 html_generator.AddLinkToAnotherReport(
749 _GetCoverageHtmlReportPathForComponent(component), component,
750 per_component_coverage_summary[component])
751
Abhishek Aryaefbe1df2018-05-14 20:19:48752 # Do not create a totals row for the component view as the value is incorrect
753 # due to failure to account for UNKNOWN component and some paths belonging to
754 # multiple components.
755
Yuke Liao1b852fd2018-05-11 17:07:32756 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaoc7e607142018-02-05 20:26:14757 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33758
759
Max Moroz7c5354f2018-05-06 00:03:48760def _MergeTwoDirectories(src_path, dst_path):
761 """Merge src_path directory into dst_path directory."""
762 for filename in os.listdir(src_path):
763 dst_path = os.path.join(dst_path, filename)
764 if os.path.exists(dst_path):
765 shutil.rmtree(dst_path)
766 os.rename(os.path.join(src_path, filename), dst_path)
767 shutil.rmtree(src_path)
768
769
Yuke Liaoea228d02018-01-05 19:10:33770def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37771 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48772 html_index_file_path = _GetHtmlIndexPath()
773 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37774 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
775
776
777def _WriteRedirectHtmlFile(from_html_path, to_html_path):
778 """Writes a html file that redirects to another html file."""
779 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
780 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33781 content = ("""
782 <!DOCTYPE html>
783 <html>
784 <head>
785 <!-- HTML meta refresh URL redirection -->
786 <meta http-equiv="refresh" content="0; url=%s">
787 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37788 </html>""" % to_html_relative_path)
789 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33790 f.write(content)
791
792
Max Moroz7c5354f2018-05-06 00:03:48793def _CleanUpOutputDir():
794 """Perform a cleanup of the output dir."""
795 # Remove the default index.html file produced by llvm-cov.
796 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
797 if os.path.exists(index_path):
798 os.remove(index_path)
799
800
Yuke Liaodd1ec0592018-02-02 01:26:37801def _GetCoverageHtmlReportPathForFile(file_path):
802 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40803 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37804 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
805
806 # '+' is used instead of os.path.join because both of them are absolute paths
807 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14808 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37809 return _GetCoverageReportRootDirPath() + html_report_path
810
811
812def _GetCoverageHtmlReportPathForDirectory(dir_path):
813 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40814 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37815 html_report_path = os.path.join(
816 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
817
818 # '+' is used instead of os.path.join because both of them are absolute paths
819 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14820 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37821 return _GetCoverageReportRootDirPath() + html_report_path
822
823
824def _GetCoverageHtmlReportPathForComponent(component_name):
825 """Given a component, returns the corresponding html report path."""
826 component_file_name = component_name.lower().replace('>', '-')
827 html_report_name = os.extsep.join([component_file_name, 'html'])
828 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
829 html_report_name)
830
831
832def _GetCoverageReportRootDirPath():
833 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48834 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
835
836
837def _GetComponentViewPath():
838 """Path to the HTML file for the component view."""
839 return os.path.join(_GetCoverageReportRootDirPath(),
840 COMPONENT_VIEW_INDEX_FILE)
841
842
843def _GetDirectoryViewPath():
844 """Path to the HTML file for the directory view."""
845 return os.path.join(_GetCoverageReportRootDirPath(),
846 DIRECTORY_VIEW_INDEX_FILE)
847
848
849def _GetFileViewPath():
850 """Path to the HTML file for the file view."""
851 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
852
853
854def _GetLogsDirectoryPath():
855 """Path to the logs directory."""
856 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
857
858
859def _GetHtmlIndexPath():
860 """Path to the main HTML index file."""
861 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
862
863
864def _GetProfdataFilePath():
865 """Path to the resulting .profdata file."""
866 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
867
868
869def _GetSummaryFilePath():
870 """The JSON file that contains coverage summary written by llvm-cov export."""
871 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33872
873
Yuke Liao506e8822017-12-04 16:52:54874def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
875 """Builds and runs target to generate the coverage profile data.
876
877 Args:
878 targets: A list of targets to build with coverage instrumentation.
879 commands: A list of commands used to run the targets.
880 jobs_count: Number of jobs to run in parallel for building. If None, a
881 default value is derived based on CPUs availability.
882
883 Returns:
884 A relative path to the generated profdata file.
885 """
886 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02887 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59888 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02889 coverage_profdata_file_path = (
890 _CreateCoverageProfileDataFromTargetProfDataFiles(
891 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54892
Abhishek Aryac19bc5ef2018-05-04 22:10:02893 for target_profdata_file_path in target_profdata_file_paths:
894 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52895
Abhishek Aryac19bc5ef2018-05-04 22:10:02896 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54897
898
899def _BuildTargets(targets, jobs_count):
900 """Builds target with Clang coverage instrumentation.
901
902 This function requires current working directory to be the root of checkout.
903
904 Args:
905 targets: A list of targets to build with coverage instrumentation.
906 jobs_count: Number of jobs to run in parallel for compilation. If None, a
907 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54908 """
Abhishek Arya1ec832c2017-12-05 18:06:59909
Yuke Liao506e8822017-12-04 16:52:54910 def _IsGomaConfigured():
911 """Returns True if goma is enabled in the gn build args.
912
913 Returns:
914 A boolean indicates whether goma is configured for building or not.
915 """
Yuke Liao80afff32018-03-07 01:26:20916 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54917 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
918
Abhishek Aryafb70b532018-05-06 17:47:40919 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54920 if jobs_count is None and _IsGomaConfigured():
921 jobs_count = DEFAULT_GOMA_JOBS
922
923 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
924 if jobs_count is not None:
925 subprocess_cmd.append('-j' + str(jobs_count))
926
927 subprocess_cmd.extend(targets)
928 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40929 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54930
931
Abhishek Aryac19bc5ef2018-05-04 22:10:02932def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54933 """Runs commands and returns the relative paths to the profraw data files.
934
935 Args:
936 targets: A list of targets built with coverage instrumentation.
937 commands: A list of commands used to run the targets.
938
939 Returns:
940 A list of relative paths to the generated profraw data files.
941 """
Abhishek Aryafb70b532018-05-06 17:47:40942 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10943
Yuke Liao506e8822017-12-04 16:52:54944 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48945 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54946 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48947 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
948
949 # Ensure that logs directory exists.
950 if not os.path.exists(_GetLogsDirectoryPath()):
951 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54952
Abhishek Aryac19bc5ef2018-05-04 22:10:02953 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10954
Yuke Liaod4a9865202018-01-12 23:17:52955 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54956 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48957 output_file_name = os.extsep.join([target + '_output', 'log'])
958 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10959
Abhishek Aryac19bc5ef2018-05-04 22:10:02960 profdata_file_path = None
961 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40962 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02963 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10964
Abhishek Aryac19bc5ef2018-05-04 22:10:02965 if _IsIOSCommand(command):
966 # On iOS platform, due to lack of write permissions, profraw files are
967 # generated outside of the OUTPUT_DIR, and the exact paths are contained
968 # in the output of the command execution.
969 output = _ExecuteIOSCommand(target, command)
970 else:
971 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
972 output = _ExecuteCommand(target, command)
973
974 with open(output_file_path, 'w') as output_file:
975 output_file.write(output)
976
977 profraw_file_paths = []
978 if _IsIOS():
979 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
980 else:
Max Moroz7c5354f2018-05-06 00:03:48981 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02982 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48983 profraw_file_paths.append(
984 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02985
986 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40987 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryad35de7e2018-05-10 22:23:04988 'please make sure the binary exists, is properly instrumented and '
989 'does not crash. %s' % (target, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:02990
991 try:
992 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
993 target, profraw_file_paths)
994 break
995 except Exception:
Abhishek Aryad35de7e2018-05-10 22:23:04996 logging.info('Retrying...')
Abhishek Aryac19bc5ef2018-05-04 22:10:02997 finally:
998 # Remove profraw files now so that they are not used in next iteration.
999 for profraw_file_path in profraw_file_paths:
1000 os.remove(profraw_file_path)
1001
1002 assert profdata_file_path, (
Abhishek Aryad35de7e2018-05-10 22:23:041003 'Failed to merge target "%s" profraw files after %d retries. %s' %
1004 (target, MERGE_RETRIES, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:021005 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541006
Abhishek Aryafb70b532018-05-06 17:47:401007 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:101008
Abhishek Aryac19bc5ef2018-05-04 22:10:021009 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:541010
1011
1012def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101013 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:521014 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:011015 #
Max Morozd73e45f2018-04-24 18:32:471016 # "%p" expands out to the process ID. It's not used by this scripts due to:
1017 # 1) If a target program spawns too many processess, it may exhaust all disk
1018 # space available. For example, unit_tests writes thousands of .profraw
1019 # files each of size 1GB+.
1020 # 2) If a target binary uses shared libraries, coverage profile data for them
1021 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011022 #
Yuke Liaod4a9865202018-01-12 23:17:521023 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1024 # is specified, the runtime creates a pool of N raw profiles which are used
1025 # for on-line profile merging. The runtime takes care of selecting a raw
1026 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521027 # N must be between 1 and 9. The merge pool specifier can only occur once per
1028 # filename pattern.
1029 #
Max Morozd73e45f2018-04-24 18:32:471030 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011031 #
Max Morozd73e45f2018-04-24 18:32:471032 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1033 # but it's not too big to consume too much computing resource or disk space.
1034 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591035 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011036 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481037 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541038 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541039
Yuke Liaoa0c8c2f2018-02-28 20:14:101040 try:
Max Moroz7c5354f2018-05-06 00:03:481041 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101042 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291043 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481044 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191045 env={
1046 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1047 'PATH': _GetPathWithLLVMSymbolizerDir()
1048 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101049 except subprocess.CalledProcessError as e:
1050 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191051 logging.warning(
1052 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1053 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101054
1055 return output
1056
1057
Yuke Liao27349c92018-03-22 21:10:011058def _IsFuzzerTarget(target):
1059 """Returns true if the target is a fuzzer target."""
1060 build_args = _GetBuildArgs()
1061 use_libfuzzer = ('use_libfuzzer' in build_args and
1062 build_args['use_libfuzzer'] == 'true')
1063 return use_libfuzzer and target.endswith('_fuzzer')
1064
1065
Yuke Liaob2926832018-03-02 17:34:291066def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101067 """Runs a single iOS command and generates a profraw data file.
1068
1069 iOS application doesn't have write access to folders outside of the app, so
1070 it's impossible to instruct the app to flush the profraw data file to the
1071 desired location. The profraw data file will be generated somewhere within the
1072 application's Documents folder, and the full path can be obtained by parsing
1073 the output.
1074 """
Yuke Liaob2926832018-03-02 17:34:291075 assert _IsIOSCommand(command)
1076
1077 # After running tests, iossim generates a profraw data file, it won't be
1078 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1079 # checkout.
1080 iossim_profraw_file_path = os.path.join(
1081 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101082
1083 try:
Yuke Liaob2926832018-03-02 17:34:291084 output = subprocess.check_output(
1085 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191086 env={
1087 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1088 'PATH': _GetPathWithLLVMSymbolizerDir()
1089 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101090 except subprocess.CalledProcessError as e:
1091 # iossim emits non-zero return code even if tests run successfully, so
1092 # ignore the return code.
1093 output = e.output
1094
1095 return output
1096
1097
1098def _GetProfrawDataFileByParsingOutput(output):
1099 """Returns the path to the profraw data file obtained by parsing the output.
1100
1101 The output of running the test target has no format, but it is guaranteed to
1102 have a single line containing the path to the generated profraw data file.
1103 NOTE: This should only be called when target os is iOS.
1104 """
Yuke Liaob2926832018-03-02 17:34:291105 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101106
Yuke Liaob2926832018-03-02 17:34:291107 output_by_lines = ''.join(output).splitlines()
1108 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101109
1110 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291111 result = profraw_file_pattern.match(line)
1112 if result:
1113 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101114
1115 assert False, ('No profraw data file was generated, did you call '
1116 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1117 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541118
1119
Abhishek Aryac19bc5ef2018-05-04 22:10:021120def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1121 """Returns a relative path to coverage profdata file by merging target
1122 profdata files.
Yuke Liao506e8822017-12-04 16:52:541123
1124 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021125 profdata_file_paths: A list of relative paths to the profdata data files
1126 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541127
1128 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021129 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541130
1131 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021132 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541133 """
Abhishek Aryafb70b532018-05-06 17:47:401134 logging.info('Creating the coverage profile data file.')
1135 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481136 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541137 try:
Abhishek Arya1ec832c2017-12-05 18:06:591138 subprocess_cmd = [
1139 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1140 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021141 subprocess_cmd.extend(profdata_file_paths)
1142 subprocess.check_call(subprocess_cmd)
1143 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041144 logging.error(
1145 'Failed to merge target profdata files to create coverage profdata. %s',
1146 FILE_BUG_MESSAGE)
Abhishek Aryac19bc5ef2018-05-04 22:10:021147 raise error
1148
Abhishek Aryafb70b532018-05-06 17:47:401149 logging.debug('Finished merging target profdata files.')
1150 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021151 profdata_file_path)
1152 return profdata_file_path
1153
1154
1155def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1156 """Returns a relative path to target profdata file by merging target
1157 profraw files.
1158
1159 Args:
1160 profraw_file_paths: A list of relative paths to the profdata data files
1161 that are to be merged.
1162
1163 Returns:
1164 A relative path to the merged coverage profdata file.
1165
1166 Raises:
1167 CalledProcessError: An error occurred merging profdata files.
1168 """
Abhishek Aryafb70b532018-05-06 17:47:401169 logging.info('Creating target profile data file.')
1170 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021171 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1172
1173 try:
1174 subprocess_cmd = [
1175 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1176 ]
Yuke Liao506e8822017-12-04 16:52:541177 subprocess_cmd.extend(profraw_file_paths)
1178 subprocess.check_call(subprocess_cmd)
1179 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041180 logging.error(
1181 'Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541182 raise error
1183
Abhishek Aryafb70b532018-05-06 17:47:401184 logging.debug('Finished merging target profraw files.')
1185 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101186 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541187 return profdata_file_path
1188
1189
Yuke Liao0e4c8682018-04-18 21:06:591190def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1191 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331192 """Generates per file coverage summary using "llvm-cov export" command."""
1193 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1194 # [[-object BIN]] [SOURCES].
1195 # NOTE: For object files, the first one is specified as a positional argument,
1196 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101197 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401198 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331199 subprocess_cmd = [
1200 LLVM_COV_PATH, 'export', '-summary-only',
1201 '-instr-profile=' + profdata_file_path, binary_paths[0]
1202 ]
1203 subprocess_cmd.extend(
1204 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291205 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331206 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591207 if ignore_filename_regex:
1208 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331209
Max Moroz7c5354f2018-05-06 00:03:481210 export_output = subprocess.check_output(subprocess_cmd)
1211
1212 # Write output on the disk to be used by code coverage bot.
1213 with open(_GetSummaryFilePath(), 'w') as f:
1214 f.write(export_output)
1215
1216 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331217 assert len(json_output['data']) == 1
1218 files_coverage_data = json_output['data'][0]['files']
1219
1220 per_file_coverage_summary = {}
1221 for file_coverage_data in files_coverage_data:
1222 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401223 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1224 'File path "%s" in coverage summary is outside source checkout.' %
1225 file_path)
Yuke Liaoea228d02018-01-05 19:10:331226
Abhishek Aryafb70b532018-05-06 17:47:401227 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331228 if summary['lines']['count'] == 0:
1229 continue
1230
1231 per_file_coverage_summary[file_path] = _CoverageSummary(
1232 regions_total=summary['regions']['count'],
1233 regions_covered=summary['regions']['covered'],
1234 functions_total=summary['functions']['count'],
1235 functions_covered=summary['functions']['covered'],
1236 lines_total=summary['lines']['count'],
1237 lines_covered=summary['lines']['covered'])
1238
Abhishek Aryafb70b532018-05-06 17:47:401239 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331240 return per_file_coverage_summary
1241
1242
Yuke Liaob2926832018-03-02 17:34:291243def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1244 """Appends -arch arguments to the command list if it's ios platform.
1245
1246 iOS binaries are universal binaries, and require specifying the architecture
1247 to use, and one architecture needs to be specified for each binary.
1248 """
1249 if _IsIOS():
1250 cmd_list.extend(['-arch=x86_64'] * num_archs)
1251
1252
Yuke Liao506e8822017-12-04 16:52:541253def _GetBinaryPath(command):
1254 """Returns a relative path to the binary to be run by the command.
1255
Yuke Liao545db322018-02-15 17:12:011256 Currently, following types of commands are supported (e.g. url_unittests):
1257 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1258 2. Use xvfb.
1259 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1260 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371261 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1262 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101263 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371264 <iossim_arguments> -c <app_arguments>
1265 out/Coverage-iphonesimulator/url_unittests.app"
1266
Yuke Liao545db322018-02-15 17:12:011267
Yuke Liao506e8822017-12-04 16:52:541268 Args:
1269 command: A command used to run a target.
1270
1271 Returns:
1272 A relative path to the binary.
1273 """
Yuke Liao545db322018-02-15 17:12:011274 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1275
Yuke Liaob2926832018-03-02 17:34:291276 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011277 if os.path.basename(command_parts[0]) == 'python':
1278 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401279 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011280 return command_parts[2]
1281
1282 if os.path.basename(command_parts[0]) == xvfb_script_name:
1283 return command_parts[1]
1284
Yuke Liaob2926832018-03-02 17:34:291285 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101286 # For a given application bundle, the binary resides in the bundle and has
1287 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371288 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101289 app_name = os.path.splitext(os.path.basename(app_path))[0]
1290 return os.path.join(app_path, app_name)
1291
Yuke Liaob2926832018-03-02 17:34:291292 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541293
1294
Yuke Liaob2926832018-03-02 17:34:291295def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101296 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291297 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101298
1299
Yuke Liao95d13d72017-12-07 18:18:501300def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1301 """Verifies that the target executables specified in the commands are inside
1302 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541303 for command in commands:
1304 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501305 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481306 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501307 'Target executable "%s" in command: "%s" is outside of '
1308 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541309
1310
1311def _ValidateBuildingWithClangCoverage():
1312 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201313 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541314
1315 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1316 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591317 assert False, ('\'{} = true\' is required in args.gn.'
1318 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541319
1320
Yuke Liaoc60b2d02018-03-02 21:40:431321def _ValidateCurrentPlatformIsSupported():
1322 """Asserts that this script suports running on the current platform"""
1323 target_os = _GetTargetOS()
1324 if target_os:
1325 current_platform = target_os
1326 else:
1327 current_platform = _GetHostPlatform()
1328
1329 assert current_platform in [
1330 'linux', 'mac', 'chromeos', 'ios'
1331 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1332
1333
Yuke Liao80afff32018-03-07 01:26:201334def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541335 """Parses args.gn file and returns results as a dictionary.
1336
1337 Returns:
1338 A dictionary representing the build args.
1339 """
Yuke Liao80afff32018-03-07 01:26:201340 global _BUILD_ARGS
1341 if _BUILD_ARGS is not None:
1342 return _BUILD_ARGS
1343
1344 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541345 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1346 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1347 'missing args.gn file.' % BUILD_DIR)
1348 with open(build_args_path) as build_args_file:
1349 build_args_lines = build_args_file.readlines()
1350
Yuke Liao506e8822017-12-04 16:52:541351 for build_arg_line in build_args_lines:
1352 build_arg_without_comments = build_arg_line.split('#')[0]
1353 key_value_pair = build_arg_without_comments.split('=')
1354 if len(key_value_pair) != 2:
1355 continue
1356
1357 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431358
1359 # Values are wrapped within a pair of double-quotes, so remove the leading
1360 # and trailing double-quotes.
1361 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201362 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541363
Yuke Liao80afff32018-03-07 01:26:201364 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541365
1366
Abhishek Arya16f059a2017-12-07 17:47:321367def _VerifyPathsAndReturnAbsolutes(paths):
1368 """Verifies that the paths specified in |paths| exist and returns absolute
1369 versions.
Yuke Liao66da1732017-12-05 22:19:421370
1371 Args:
1372 paths: A list of files or directories.
1373 """
Abhishek Arya16f059a2017-12-07 17:47:321374 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421375 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321376 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1377 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1378
1379 absolute_paths.append(absolute_path)
1380
1381 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421382
1383
Yuke Liaodd1ec0592018-02-02 01:26:371384def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1385 """Returns a target path relative to the directory of base_path.
1386
1387 This method requires base_path to be a file, otherwise, one should call
1388 os.path.relpath directly.
1389 """
1390 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141391 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371392 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141393 base_dir = os.path.dirname(base_path)
1394 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371395
1396
Abhishek Arya64636af2018-05-04 14:42:131397def _GetBinaryPathsFromTargets(targets, build_dir):
1398 """Return binary paths from target names."""
1399 # FIXME: Derive output binary from target build definitions rather than
1400 # assuming that it is always the same name.
1401 binary_paths = []
1402 for target in targets:
1403 binary_path = os.path.join(build_dir, target)
1404 if _GetHostPlatform() == 'win':
1405 binary_path += '.exe'
1406
1407 if os.path.exists(binary_path):
1408 binary_paths.append(binary_path)
1409 else:
1410 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401411 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131412 os.path.basename(binary_path))
1413
1414 return binary_paths
1415
1416
Yuke Liao506e8822017-12-04 16:52:541417def _ParseCommandArguments():
1418 """Adds and parses relevant arguments for tool comands.
1419
1420 Returns:
1421 A dictionary representing the arguments.
1422 """
1423 arg_parser = argparse.ArgumentParser()
1424 arg_parser.usage = __doc__
1425
Abhishek Arya1ec832c2017-12-05 18:06:591426 arg_parser.add_argument(
1427 '-b',
1428 '--build-dir',
1429 type=str,
1430 required=True,
1431 help='The build directory, the path needs to be relative to the root of '
1432 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541433
Abhishek Arya1ec832c2017-12-05 18:06:591434 arg_parser.add_argument(
1435 '-o',
1436 '--output-dir',
1437 type=str,
1438 required=True,
1439 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541440
Abhishek Arya1ec832c2017-12-05 18:06:591441 arg_parser.add_argument(
1442 '-c',
1443 '--command',
1444 action='append',
Abhishek Arya64636af2018-05-04 14:42:131445 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591446 help='Commands used to run test targets, one test target needs one and '
1447 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131448 'current working directory is the root of the checkout. This option is '
1449 'incompatible with -p/--profdata-file option.')
1450
1451 arg_parser.add_argument(
1452 '-p',
1453 '--profdata-file',
1454 type=str,
1455 required=False,
1456 help='Path to profdata file to use for generating code coverage reports. '
1457 'This can be useful if you generated the profdata file seperately in '
1458 'your own test harness. This option is ignored if run command(s) are '
1459 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541460
Abhishek Arya1ec832c2017-12-05 18:06:591461 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421462 '-f',
1463 '--filters',
1464 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321465 required=False,
Yuke Liao66da1732017-12-05 22:19:421466 help='Directories or files to get code coverage for, and all files under '
1467 'the directories are included recursively.')
1468
1469 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591470 '-i',
1471 '--ignore-filename-regex',
1472 type=str,
1473 help='Skip source code files with file paths that match the given '
1474 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1475 'to exclude files in third_party/ and out/ folders from the report.')
1476
1477 arg_parser.add_argument(
Yuke Liao1b852fd2018-05-11 17:07:321478 '--no-file-view',
1479 action='store_true',
1480 help='Don\'t generate the file view in the coverage report. When there '
1481 'are large number of html files, the file view becomes heavy and may '
1482 'cause the browser to freeze, and this argument comes handy.')
1483
1484 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591485 '-j',
1486 '--jobs',
1487 type=int,
1488 default=None,
1489 help='Run N jobs to build in parallel. If not specified, a default value '
1490 'will be derived based on CPUs availability. Please refer to '
1491 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541492
Abhishek Arya1ec832c2017-12-05 18:06:591493 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101494 '-v',
1495 '--verbose',
1496 action='store_true',
1497 help='Prints additional output for diagnostics.')
1498
1499 arg_parser.add_argument(
1500 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1501
1502 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021503 'targets',
1504 nargs='+',
1505 help='The names of the test targets to run. If multiple run commands are '
1506 'specified using the -c/--command option, then the order of targets and '
1507 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541508
1509 args = arg_parser.parse_args()
1510 return args
1511
1512
1513def Main():
1514 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131515 # Change directory to source root to aid in relative paths calculations.
1516 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111517
Abhishek Arya64636af2018-05-04 14:42:131518 # Setup coverage binaries even when script is called with empty params. This
1519 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111520 DownloadCoverageToolsIfNeeded()
1521
Yuke Liao506e8822017-12-04 16:52:541522 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131523 _ConfigureLogging(args)
1524
Yuke Liao506e8822017-12-04 16:52:541525 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481526 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541527 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481528 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541529
Abhishek Arya64636af2018-05-04 14:42:131530 assert args.command or args.profdata_file, (
1531 'Need to either provide commands to run using -c/--command option OR '
1532 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431533
Abhishek Arya64636af2018-05-04 14:42:131534 assert not args.command or (len(args.targets) == len(args.command)), (
1535 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431536
Abhishek Arya1ec832c2017-12-05 18:06:591537 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401538 'Build directory: "%s" doesn\'t exist. '
1539 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131540
Yuke Liaoc60b2d02018-03-02 21:40:431541 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541542 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321543
1544 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421545 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321546 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421547
Max Moroz7c5354f2018-05-06 00:03:481548 if not os.path.exists(_GetCoverageReportRootDirPath()):
1549 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541550
Abhishek Arya64636af2018-05-04 14:42:131551 # Get profdate file and list of binary paths.
1552 if args.command:
1553 # A list of commands are provided. Run them to generate profdata file, and
1554 # create a list of binary paths from parsing commands.
1555 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1556 profdata_file_path = _CreateCoverageProfileDataForTargets(
1557 args.targets, args.command, args.jobs)
1558 binary_paths = [_GetBinaryPath(command) for command in args.command]
1559 else:
1560 # An input prof-data file is already provided. Just calculate binary paths.
1561 profdata_file_path = args.profdata_file
1562 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331563
Abhishek Arya78120bc2018-05-07 20:53:541564 binary_paths.extend(_GetSharedLibraries(binary_paths))
1565
Yuke Liao481d3482018-01-29 19:17:101566 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401567 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371568 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591569 binary_paths, profdata_file_path, absolute_filter_paths,
1570 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371571 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591572 absolute_filter_paths,
1573 args.ignore_filename_regex)
Yuke Liao1b852fd2018-05-11 17:07:321574 if not args.no_file_view:
1575 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
Yuke Liaodd1ec0592018-02-02 01:26:371576
1577 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1578 per_file_coverage_summary)
1579 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
Yuke Liao1b852fd2018-05-11 17:07:321580 per_file_coverage_summary,
1581 args.no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:371582 _GenerateDirectoryViewHtmlIndexFile()
1583
1584 component_to_directories = _ExtractComponentToDirectoriesMapping()
1585 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1586 component_to_directories, per_directory_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:321587 _GeneratePerComponentCoverageInHtml(
1588 per_component_coverage_summary, component_to_directories,
1589 per_directory_coverage_summary, args.no_file_view)
1590 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
1591 args.no_file_view)
Yuke Liaoea228d02018-01-05 19:10:331592
1593 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371594 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331595 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481596 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331597
Max Moroz7c5354f2018-05-06 00:03:481598 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401599 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101600 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541601
Abhishek Arya1ec832c2017-12-05 18:06:591602
Yuke Liao506e8822017-12-04 16:52:541603if __name__ == '__main__':
1604 sys.exit(Main())