blob: e3a9595a286c0322881deecce5d0b1a91c6dccc0 [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 Arya03911092018-05-21 16:42:3514 * Example usage:
Abhishek Arya1ec832c2017-12-05 18:06:5915
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
Abhishek Arya03911092018-05-21 16:42:3531 * Sample flow for running a test target with xvfb (e.g. unit_tests):
Yuke Liao545db322018-02-15 17:12:0132
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
Abhishek Arya03911092018-05-21 16:42:3539 * Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
Abhishek Arya1ec832c2017-12-05 18:06:5940
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
Abhishek Arya03911092018-05-21 16:42:3551 * Sample workflow for running Blink web tests:
52
53 python tools/code_coverage/coverage.py blink_tests \\
54 -wt -b out/coverage -o out/report -f third_party/blink
55
56 If you need to pass arguments to run_web_tests.py, use
57 -wt='arguments to run_web_tests.py e.g. test directories'
58
Abhishek Arya1ec832c2017-12-05 18:06:5959 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3860
61 For an overview of how code coverage works in Chromium, please refer to
62 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5463"""
64
65from __future__ import print_function
66
67import sys
68
69import argparse
Yuke Liaoea228d02018-01-05 19:10:3370import json
Yuke Liao481d3482018-01-29 19:17:1071import logging
Abhishek Arya03911092018-05-21 16:42:3572import multiprocessing
Yuke Liao506e8822017-12-04 16:52:5473import os
Yuke Liaob2926832018-03-02 17:34:2974import re
75import shlex
Max Moroz025d8952018-05-03 16:33:3476import shutil
Yuke Liao506e8822017-12-04 16:52:5477import subprocess
Yuke Liao506e8822017-12-04 16:52:5478import urllib2
79
Abhishek Arya1ec832c2017-12-05 18:06:5980sys.path.append(
81 os.path.join(
82 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
83 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5484import update as clang_update
85
Yuke Liaoea228d02018-01-05 19:10:3386sys.path.append(
87 os.path.join(
88 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
89 'third_party'))
90import jinja2
91from collections import defaultdict
92
Yuke Liao082e99632018-05-18 15:40:4093# Absolute path to the code coverage tools binary. These paths can be
94# overwritten by user specified coverage tool paths.
Yuke Liao506e8822017-12-04 16:52:5495LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:1996LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
97LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
98LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5499
Abhishek Arya03911092018-05-21 16:42:35100# Absolute path to the root of the checkout.
101SRC_ROOT_PATH = None
102
Yuke Liao506e8822017-12-04 16:52:54103# Build directory, the value is parsed from command line arguments.
104BUILD_DIR = None
105
106# Output directory for generated artifacts, the value is parsed from command
107# line arguemnts.
108OUTPUT_DIR = None
109
110# Default number of jobs used to build when goma is configured and enabled.
111DEFAULT_GOMA_JOBS = 100
112
113# Name of the file extension for profraw data files.
114PROFRAW_FILE_EXTENSION = 'profraw'
115
116# Name of the final profdata file, and this file needs to be passed to
117# "llvm-cov" command in order to call "llvm-cov show" to inspect the
118# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48119PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
120
121# Name of the file with summary information generated by llvm-cov export.
122SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54123
124# Build arg required for generating code coverage data.
125CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
126
Yuke Liaoea228d02018-01-05 19:10:33127# The default name of the html coverage report for a directory.
128DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
129
Yuke Liaodd1ec0592018-02-02 01:26:37130# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37131COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48132DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37133FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48134INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
135
136LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37137
138# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19139COMPONENT_MAPPING_URL = (
140 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37141
Yuke Liao80afff32018-03-07 01:26:20142# Caches the results returned by _GetBuildArgs, don't use this variable
143# directly, call _GetBuildArgs instead.
144_BUILD_ARGS = None
145
Abhishek Aryac19bc5ef2018-05-04 22:10:02146# Retry failed merges.
147MERGE_RETRIES = 3
148
Abhishek Aryad35de7e2018-05-10 22:23:04149# Message to guide user to file a bug when everything else fails.
150FILE_BUG_MESSAGE = (
151 'If it persists, please file a bug with the command you used, git revision '
152 'and args.gn config here: '
153 'https://bugs.chromium.org/p/chromium/issues/entry?'
154 'components=Tools%3ECodeCoverage')
155
Yuke Liaoea228d02018-01-05 19:10:33156
157class _CoverageSummary(object):
158 """Encapsulates coverage summary representation."""
159
Yuke Liaodd1ec0592018-02-02 01:26:37160 def __init__(self,
161 regions_total=0,
162 regions_covered=0,
163 functions_total=0,
164 functions_covered=0,
165 lines_total=0,
166 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33167 """Initializes _CoverageSummary object."""
168 self._summary = {
169 'regions': {
170 'total': regions_total,
171 'covered': regions_covered
172 },
173 'functions': {
174 'total': functions_total,
175 'covered': functions_covered
176 },
177 'lines': {
178 'total': lines_total,
179 'covered': lines_covered
180 }
181 }
182
183 def Get(self):
184 """Returns summary as a dictionary."""
185 return self._summary
186
187 def AddSummary(self, other_summary):
188 """Adds another summary to this one element-wise."""
189 for feature in self._summary:
190 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
191 self._summary[feature]['covered'] += other_summary.Get()[feature][
192 'covered']
193
194
Yuke Liaodd1ec0592018-02-02 01:26:37195class _CoverageReportHtmlGenerator(object):
196 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33197
Yuke Liaodd1ec0592018-02-02 01:26:37198 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33199 """
200
Yuke Liaodd1ec0592018-02-02 01:26:37201 def __init__(self, output_path, table_entry_type):
202 """Initializes _CoverageReportHtmlGenerator object.
203
204 Args:
205 output_path: Path to the html report that will be generated.
206 table_entry_type: Type of the table entries to be displayed in the table
207 header. For example: 'Path', 'Component'.
208 """
Yuke Liaoea228d02018-01-05 19:10:33209 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48210 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33211 assert os.path.exists(css_absolute_path), (
212 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40213 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33214 css_absolute_path)
215
216 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37217 self._output_path = output_path
218 self._table_entry_type = table_entry_type
219
Yuke Liaoea228d02018-01-05 19:10:33220 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12221 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23222
223 source_dir = os.path.dirname(os.path.realpath(__file__))
224 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33225
226 jinja_env = jinja2.Environment(
227 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
228 self._header_template = jinja_env.get_template('header.html')
229 self._table_template = jinja_env.get_template('table.html')
230 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23231
Abhishek Arya865fffd2018-05-08 22:16:01232 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23233 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33234
235 def AddLinkToAnotherReport(self, html_report_path, name, summary):
236 """Adds a link to another html report in this report.
237
238 The link to be added is assumed to be an entry in this directory.
239 """
Yuke Liaodd1ec0592018-02-02 01:26:37240 # Use relative paths instead of absolute paths to make the generated reports
241 # portable.
242 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
243 html_report_path, self._output_path)
244
Yuke Liaod54030e2018-01-08 17:34:12245 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37246 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12247 os.path.basename(html_report_path) ==
248 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
249 self._table_entries.append(table_entry)
250
251 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35252 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12253 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
254
255 def _CreateTableEntryFromCoverageSummary(self,
256 summary,
257 href=None,
258 name=None,
259 is_dir=None):
260 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37261 assert (href is None and name is None and is_dir is None) or (
262 href is not None and name is not None and is_dir is not None), (
263 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35264 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37265 'attributes must be None.')
266
Yuke Liaod54030e2018-01-08 17:34:12267 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37268 if href is not None:
269 entry['href'] = href
270 if name is not None:
271 entry['name'] = name
272 if is_dir is not None:
273 entry['is_dir'] = is_dir
274
Yuke Liaoea228d02018-01-05 19:10:33275 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12276 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37277 if summary_dict[feature]['total'] == 0:
278 percentage = 0.0
279 else:
Yuke Liao0e4c8682018-04-18 21:06:59280 percentage = float(summary_dict[feature]
281 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35282
Yuke Liaoea228d02018-01-05 19:10:33283 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12284 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33285 'total': summary_dict[feature]['total'],
286 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35287 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33288 'color_class': color_class
289 }
Yuke Liaod54030e2018-01-08 17:34:12290
Yuke Liaod54030e2018-01-08 17:34:12291 return entry
Yuke Liaoea228d02018-01-05 19:10:33292
293 def _GetColorClass(self, percentage):
294 """Returns the css color class based on coverage percentage."""
295 if percentage >= 0 and percentage < 80:
296 return 'red'
297 if percentage >= 80 and percentage < 100:
298 return 'yellow'
299 if percentage == 100:
300 return 'green'
301
Abhishek Aryafb70b532018-05-06 17:47:40302 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33303
Yuke Liao1b852fd2018-05-11 17:07:32304 def WriteHtmlCoverageReport(self, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37305 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33306
307 In the report, sub-directories are displayed before files and within each
308 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33309 """
310
311 def EntryCmp(left, right):
312 """Compare function for table entries."""
313 if left['is_dir'] != right['is_dir']:
314 return -1 if left['is_dir'] == True else 1
315
Yuke Liaodd1ec0592018-02-02 01:26:37316 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33317
318 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
319
320 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48321
322 directory_view_path = _GetDirectoryViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32323 directory_view_href = _GetRelativePathToDirectoryOfFile(
324 directory_view_path, self._output_path)
Max Moroz7c5354f2018-05-06 00:03:48325 component_view_path = _GetComponentViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32326 component_view_href = _GetRelativePathToDirectoryOfFile(
327 component_view_path, self._output_path)
328
329 # File view is optional in the report.
330 file_view_href = None
331 if not no_file_view:
332 file_view_path = _GetFileViewPath()
333 file_view_href = _GetRelativePathToDirectoryOfFile(
334 file_view_path, self._output_path)
Yuke Liaodd1ec0592018-02-02 01:26:37335
Yuke Liaoea228d02018-01-05 19:10:33336 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37337 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
Yuke Liao1b852fd2018-05-11 17:07:32338 directory_view_href=directory_view_href,
339 component_view_href=component_view_href,
340 file_view_href=file_view_href,
Abhishek Arya865fffd2018-05-08 22:16:01341 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37342
Yuke Liaod54030e2018-01-08 17:34:12343 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37344 entries=self._table_entries,
345 total_entry=self._total_entry,
346 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33347 html_footer = self._footer_template.render()
348
Yuke Liaodd1ec0592018-02-02 01:26:37349 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33350 html_file.write(html_header + html_table + html_footer)
351
Yuke Liao506e8822017-12-04 16:52:54352
Abhishek Arya64636af2018-05-04 14:42:13353def _ConfigureLogging(args):
354 """Configures logging settings for later use."""
355 log_level = logging.DEBUG if args.verbose else logging.INFO
356 log_format = '[%(asctime)s %(levelname)s] %(message)s'
357 log_file = args.log_file if args.log_file else None
358 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
359
360
Yuke Liao082e99632018-05-18 15:40:40361def _ConfigureLLVMCoverageTools(args):
362 """Configures llvm coverage tools."""
363 if args.coverage_tools_dir:
Abhishek Arya03911092018-05-21 16:42:35364 llvm_bin_dir = _GetFullPath(args.coverage_tools_dir)
Yuke Liao082e99632018-05-18 15:40:40365 global LLVM_COV_PATH
366 global LLVM_PROFDATA_PATH
367 LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
368 LLVM_PROFDATA_PATH = os.path.join(llvm_bin_dir, 'llvm-profdata')
369 else:
370 DownloadCoverageToolsIfNeeded()
371
372 coverage_tools_exist = (
373 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
374 assert coverage_tools_exist, ('Cannot find coverage tools, please make sure '
375 'both \'%s\' and \'%s\' exist.') % (
376 LLVM_COV_PATH, LLVM_PROFDATA_PATH)
377
378
Max Morozd73e45f2018-04-24 18:32:47379def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54380 """Returns list of shared libraries used by specified binaries."""
381 logging.info('Finding shared libraries for targets (if any).')
382 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47383 cmd = []
384 shared_library_re = None
385
386 if sys.platform.startswith('linux'):
387 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13388 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
389 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47390 elif sys.platform.startswith('darwin'):
391 cmd.extend(['otool', '-L'])
392 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
393 else:
Abhishek Aryafb70b532018-05-06 17:47:40394 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47395
396 assert shared_library_re is not None
397
398 cmd.extend(binary_paths)
399 output = subprocess.check_output(cmd)
400
401 for line in output.splitlines():
402 m = shared_library_re.match(line)
403 if not m:
404 continue
405
406 shared_library_path = m.group(1)
407 if sys.platform.startswith('darwin'):
408 # otool outputs "@rpath" macro instead of the dirname of the given binary.
409 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
410
Abhishek Arya78120bc2018-05-07 20:53:54411 if shared_library_path in shared_libraries:
412 continue
413
Max Morozd73e45f2018-04-24 18:32:47414 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
415 'the given target(s) does not '
416 'exist.' % shared_library_path)
417 with open(shared_library_path) as f:
418 data = f.read()
419
420 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
421 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54422 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47423
Abhishek Arya78120bc2018-05-07 20:53:54424 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
425 shared_libraries)
426 logging.info('Finished finding shared libraries for targets.')
427 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47428
429
Yuke Liaoc60b2d02018-03-02 21:40:43430def _GetHostPlatform():
431 """Returns the host platform.
432
433 This is separate from the target platform/os that coverage is running for.
434 """
Abhishek Arya1ec832c2017-12-05 18:06:59435 if sys.platform == 'win32' or sys.platform == 'cygwin':
436 return 'win'
437 if sys.platform.startswith('linux'):
438 return 'linux'
439 else:
440 assert sys.platform == 'darwin'
441 return 'mac'
442
443
Abhishek Arya1c97ea542018-05-10 03:53:19444def _GetPathWithLLVMSymbolizerDir():
445 """Add llvm-symbolizer directory to path for symbolized stacks."""
446 path = os.getenv('PATH')
447 dirs = path.split(os.pathsep)
448 if LLVM_BIN_DIR in dirs:
449 return path
450
451 return path + os.pathsep + LLVM_BIN_DIR
452
453
Yuke Liaoc60b2d02018-03-02 21:40:43454def _GetTargetOS():
455 """Returns the target os specified in args.gn file.
456
457 Returns an empty string is target_os is not specified.
458 """
Yuke Liao80afff32018-03-07 01:26:20459 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43460 return build_args['target_os'] if 'target_os' in build_args else ''
461
462
Yuke Liaob2926832018-03-02 17:34:29463def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10464 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43465 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10466
467
Yuke Liao506e8822017-12-04 16:52:54468# TODO(crbug.com/759794): remove this function once tools get included to
469# Clang bundle:
470# https://chromium-review.googlesource.com/c/chromium/src/+/688221
471def DownloadCoverageToolsIfNeeded():
472 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59473
Yuke Liaoc60b2d02018-03-02 21:40:43474 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54475 """Returns a pair of revision number by reading the build stamp file.
476
477 Args:
478 stamp_file_path: A path the build stamp file created by
479 tools/clang/scripts/update.py.
480 Returns:
481 A pair of integers represeting the main and sub revision respectively.
482 """
483 if not os.path.exists(stamp_file_path):
484 return 0, 0
485
486 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43487 stamp_file_line = stamp_file.readline()
488 if ',' in stamp_file_line:
489 package_version = stamp_file_line.rstrip().split(',')[0]
490 else:
491 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54492
Yuke Liaoc60b2d02018-03-02 21:40:43493 clang_revision_str, clang_sub_revision_str = package_version.split('-')
494 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59495
Yuke Liaoc60b2d02018-03-02 21:40:43496 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54497 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43498 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54499
500 coverage_revision_stamp_file = os.path.join(
501 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
502 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43503 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54504
Yuke Liaoea228d02018-01-05 19:10:33505 has_coverage_tools = (
506 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32507
Yuke Liaoea228d02018-01-05 19:10:33508 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54509 coverage_sub_revision == clang_sub_revision):
510 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43511 return
Yuke Liao506e8822017-12-04 16:52:54512
513 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
514 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
515
516 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43517 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54518 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43519 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54520 coverage_tools_url = (
521 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43522 else:
523 assert host_platform == 'win'
524 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54525
526 try:
527 clang_update.DownloadAndUnpack(coverage_tools_url,
528 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54529 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43530 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54531 file_handle.write('\n')
532 except urllib2.URLError:
533 raise Exception(
534 'Failed to download coverage tools: %s.' % coverage_tools_url)
535
536
Yuke Liaodd1ec0592018-02-02 01:26:37537def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59538 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54539 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
540
541 For a file with absolute path /a/b/x.cc, a html report is generated as:
542 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
543 OUTPUT_DIR/index.html.
544
545 Args:
546 binary_paths: A list of paths to the instrumented binaries.
547 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42548 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54549 """
Yuke Liao506e8822017-12-04 16:52:54550 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
551 # [[-object BIN]] [SOURCES]
552 # NOTE: For object files, the first one is specified as a positional argument,
553 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10554 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40555 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59556 subprocess_cmd = [
557 LLVM_COV_PATH, 'show', '-format=html',
558 '-output-dir={}'.format(OUTPUT_DIR),
559 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
560 ]
561 subprocess_cmd.extend(
562 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29563 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Ryan Sleeviae19b2c32018-05-15 22:36:17564 if _GetHostPlatform() in ['linux', 'mac']:
565 subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
Yuke Liao66da1732017-12-05 22:19:42566 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59567 if ignore_filename_regex:
568 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
569
Yuke Liao506e8822017-12-04 16:52:54570 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34571
572 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
573 # the platform name instead, as it simplifies the report dir structure when
574 # the same report is generated for different platforms.
575 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48576 platform_report_subdir_path = _GetCoverageReportRootDirPath()
577 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34578
Abhishek Aryafb70b532018-05-06 17:47:40579 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54580
581
Yuke Liaodd1ec0592018-02-02 01:26:37582def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
583 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48584 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37585 logging.debug('Generating file view html index file as: "%s".',
586 file_view_index_file_path)
587 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
588 'Path')
589 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33590
Yuke Liaodd1ec0592018-02-02 01:26:37591 for file_path in per_file_coverage_summary:
592 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
593
594 html_generator.AddLinkToAnotherReport(
595 _GetCoverageHtmlReportPathForFile(file_path),
596 os.path.relpath(file_path, SRC_ROOT_PATH),
597 per_file_coverage_summary[file_path])
598
599 html_generator.CreateTotalsEntry(totals_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:32600 html_generator.WriteHtmlCoverageReport(no_file_view=False)
Yuke Liaodd1ec0592018-02-02 01:26:37601 logging.debug('Finished generating file view html index file.')
602
603
604def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
605 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40606 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37607 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
608
Yuke Liaoea228d02018-01-05 19:10:33609 for file_path in per_file_coverage_summary:
610 summary = per_file_coverage_summary[file_path]
611 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40612
Yuke Liaoea228d02018-01-05 19:10:33613 while True:
614 per_directory_coverage_summary[parent_dir].AddSummary(summary)
615
616 if parent_dir == SRC_ROOT_PATH:
617 break
618 parent_dir = os.path.dirname(parent_dir)
619
Abhishek Aryafb70b532018-05-06 17:47:40620 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37621 return per_directory_coverage_summary
622
623
Yuke Liao1b852fd2018-05-11 17:07:32624def _GeneratePerDirectoryCoverageInHtml(
625 per_directory_coverage_summary, per_file_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37626 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40627 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33628 for dir_path in per_directory_coverage_summary:
Yuke Liao1b852fd2018-05-11 17:07:32629 _GenerateCoverageInHtmlForDirectory(dir_path,
630 per_directory_coverage_summary,
631 per_file_coverage_summary, no_file_view)
Yuke Liaoea228d02018-01-05 19:10:33632
Abhishek Aryafb70b532018-05-06 17:47:40633 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10634
Yuke Liaoea228d02018-01-05 19:10:33635
636def _GenerateCoverageInHtmlForDirectory(
Yuke Liao1b852fd2018-05-11 17:07:32637 dir_path, per_directory_coverage_summary, per_file_coverage_summary,
638 no_file_view):
Yuke Liaoea228d02018-01-05 19:10:33639 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37640 html_generator = _CoverageReportHtmlGenerator(
641 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33642
643 for entry_name in os.listdir(dir_path):
644 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33645
Yuke Liaodd1ec0592018-02-02 01:26:37646 if entry_path in per_file_coverage_summary:
647 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
648 entry_coverage_summary = per_file_coverage_summary[entry_path]
649 elif entry_path in per_directory_coverage_summary:
650 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
651 entry_path)
652 entry_coverage_summary = per_directory_coverage_summary[entry_path]
653 else:
Yuke Liaoc7e607142018-02-05 20:26:14654 # Any file without executable lines shouldn't be included into the report.
655 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37656 continue
Yuke Liaoea228d02018-01-05 19:10:33657
Yuke Liaodd1ec0592018-02-02 01:26:37658 html_generator.AddLinkToAnotherReport(entry_html_report_path,
659 os.path.basename(entry_path),
660 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33661
Yuke Liaod54030e2018-01-08 17:34:12662 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liao1b852fd2018-05-11 17:07:32663 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37664
665
666def _GenerateDirectoryViewHtmlIndexFile():
667 """Generates the html index file for directory view.
668
669 Note that the index file is already generated under SRC_ROOT_PATH, so this
670 file simply redirects to it, and the reason of this extra layer is for
671 structural consistency with other views.
672 """
Max Moroz7c5354f2018-05-06 00:03:48673 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37674 logging.debug('Generating directory view html index file as: "%s".',
675 directory_view_index_file_path)
676 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
677 SRC_ROOT_PATH)
678 _WriteRedirectHtmlFile(directory_view_index_file_path,
679 src_root_html_report_path)
680 logging.debug('Finished generating directory view html index file.')
681
682
683def _CalculatePerComponentCoverageSummary(component_to_directories,
684 per_directory_coverage_summary):
685 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40686 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37687 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
688
689 for component in component_to_directories:
690 for directory in component_to_directories[component]:
Abhishek Arya03911092018-05-21 16:42:35691 absolute_directory_path = _GetFullPath(directory)
Yuke Liaodd1ec0592018-02-02 01:26:37692 if absolute_directory_path in per_directory_coverage_summary:
693 per_component_coverage_summary[component].AddSummary(
694 per_directory_coverage_summary[absolute_directory_path])
695
Abhishek Aryafb70b532018-05-06 17:47:40696 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37697 return per_component_coverage_summary
698
699
700def _ExtractComponentToDirectoriesMapping():
701 """Returns a mapping from components to directories."""
702 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
703 directory_to_component = component_mappings['dir-to-component']
704
705 component_to_directories = defaultdict(list)
Abhishek Arya8c3a1ce322018-05-13 04:14:01706 for directory in sorted(directory_to_component):
Yuke Liaodd1ec0592018-02-02 01:26:37707 component = directory_to_component[directory]
Abhishek Arya8c3a1ce322018-05-13 04:14:01708
709 # Check if we already added the parent directory of this directory. If yes,
710 # skip this sub-directory to avoid double-counting.
711 found_parent_directory = False
712 for component_directory in component_to_directories[component]:
713 if directory.startswith(component_directory + '/'):
714 found_parent_directory = True
715 break
716
717 if not found_parent_directory:
718 component_to_directories[component].append(directory)
Yuke Liaodd1ec0592018-02-02 01:26:37719
720 return component_to_directories
721
722
Yuke Liao1b852fd2018-05-11 17:07:32723def _GeneratePerComponentCoverageInHtml(
724 per_component_coverage_summary, component_to_directories,
725 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37726 """Generates per-component coverage reports in html."""
727 logging.debug('Writing per-component coverage html reports.')
728 for component in per_component_coverage_summary:
729 _GenerateCoverageInHtmlForComponent(
730 component, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32731 per_directory_coverage_summary, no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37732
733 logging.debug('Finished writing per-component coverage html reports.')
734
735
736def _GenerateCoverageInHtmlForComponent(
737 component_name, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32738 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37739 """Generates coverage html report for a component."""
740 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
741 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14742 component_html_report_dir = os.path.dirname(component_html_report_path)
743 if not os.path.exists(component_html_report_dir):
744 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37745
746 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
747 'Path')
748
749 for dir_path in component_to_directories[component_name]:
Abhishek Arya03911092018-05-21 16:42:35750 dir_absolute_path = _GetFullPath(dir_path)
Yuke Liaodd1ec0592018-02-02 01:26:37751 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14752 # Any directory without an excercised file shouldn't be included into the
753 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37754 continue
755
756 html_generator.AddLinkToAnotherReport(
757 _GetCoverageHtmlReportPathForDirectory(dir_path),
758 os.path.relpath(dir_path, SRC_ROOT_PATH),
759 per_directory_coverage_summary[dir_absolute_path])
760
761 html_generator.CreateTotalsEntry(
762 per_component_coverage_summary[component_name])
Yuke Liao1b852fd2018-05-11 17:07:32763 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37764
765
Yuke Liao1b852fd2018-05-11 17:07:32766def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
767 no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37768 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48769 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37770 logging.debug('Generating component view html index file as: "%s".',
771 component_view_index_file_path)
772 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
773 'Component')
Yuke Liaodd1ec0592018-02-02 01:26:37774 for component in per_component_coverage_summary:
Yuke Liaodd1ec0592018-02-02 01:26:37775 html_generator.AddLinkToAnotherReport(
776 _GetCoverageHtmlReportPathForComponent(component), component,
777 per_component_coverage_summary[component])
778
Abhishek Aryaefbe1df2018-05-14 20:19:48779 # Do not create a totals row for the component view as the value is incorrect
780 # due to failure to account for UNKNOWN component and some paths belonging to
781 # multiple components.
782
Yuke Liao1b852fd2018-05-11 17:07:32783 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaoc7e607142018-02-05 20:26:14784 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33785
786
Max Moroz7c5354f2018-05-06 00:03:48787def _MergeTwoDirectories(src_path, dst_path):
788 """Merge src_path directory into dst_path directory."""
789 for filename in os.listdir(src_path):
790 dst_path = os.path.join(dst_path, filename)
791 if os.path.exists(dst_path):
792 shutil.rmtree(dst_path)
793 os.rename(os.path.join(src_path, filename), dst_path)
794 shutil.rmtree(src_path)
795
796
Yuke Liaoea228d02018-01-05 19:10:33797def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37798 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48799 html_index_file_path = _GetHtmlIndexPath()
800 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37801 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
802
803
804def _WriteRedirectHtmlFile(from_html_path, to_html_path):
805 """Writes a html file that redirects to another html file."""
806 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
807 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33808 content = ("""
809 <!DOCTYPE html>
810 <html>
811 <head>
812 <!-- HTML meta refresh URL redirection -->
813 <meta http-equiv="refresh" content="0; url=%s">
814 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37815 </html>""" % to_html_relative_path)
816 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33817 f.write(content)
818
819
Max Moroz7c5354f2018-05-06 00:03:48820def _CleanUpOutputDir():
821 """Perform a cleanup of the output dir."""
822 # Remove the default index.html file produced by llvm-cov.
823 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
824 if os.path.exists(index_path):
825 os.remove(index_path)
826
827
Yuke Liaodd1ec0592018-02-02 01:26:37828def _GetCoverageHtmlReportPathForFile(file_path):
829 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40830 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Abhishek Arya03911092018-05-21 16:42:35831 html_report_path = os.extsep.join([_GetFullPath(file_path), 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37832
833 # '+' is used instead of os.path.join because both of them are absolute paths
834 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14835 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37836 return _GetCoverageReportRootDirPath() + html_report_path
837
838
839def _GetCoverageHtmlReportPathForDirectory(dir_path):
840 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40841 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37842 html_report_path = os.path.join(
Abhishek Arya03911092018-05-21 16:42:35843 _GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
Yuke Liaodd1ec0592018-02-02 01:26:37844
845 # '+' is used instead of os.path.join because both of them are absolute paths
846 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14847 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37848 return _GetCoverageReportRootDirPath() + html_report_path
849
850
851def _GetCoverageHtmlReportPathForComponent(component_name):
852 """Given a component, returns the corresponding html report path."""
853 component_file_name = component_name.lower().replace('>', '-')
854 html_report_name = os.extsep.join([component_file_name, 'html'])
855 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
856 html_report_name)
857
858
859def _GetCoverageReportRootDirPath():
860 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48861 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
862
863
864def _GetComponentViewPath():
865 """Path to the HTML file for the component view."""
866 return os.path.join(_GetCoverageReportRootDirPath(),
867 COMPONENT_VIEW_INDEX_FILE)
868
869
870def _GetDirectoryViewPath():
871 """Path to the HTML file for the directory view."""
872 return os.path.join(_GetCoverageReportRootDirPath(),
873 DIRECTORY_VIEW_INDEX_FILE)
874
875
876def _GetFileViewPath():
877 """Path to the HTML file for the file view."""
878 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
879
880
881def _GetLogsDirectoryPath():
882 """Path to the logs directory."""
883 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
884
885
886def _GetHtmlIndexPath():
887 """Path to the main HTML index file."""
888 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
889
890
891def _GetProfdataFilePath():
892 """Path to the resulting .profdata file."""
893 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
894
895
896def _GetSummaryFilePath():
897 """The JSON file that contains coverage summary written by llvm-cov export."""
898 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33899
900
Yuke Liao506e8822017-12-04 16:52:54901def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
902 """Builds and runs target to generate the coverage profile data.
903
904 Args:
905 targets: A list of targets to build with coverage instrumentation.
906 commands: A list of commands used to run the targets.
907 jobs_count: Number of jobs to run in parallel for building. If None, a
908 default value is derived based on CPUs availability.
909
910 Returns:
911 A relative path to the generated profdata file.
912 """
913 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02914 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59915 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02916 coverage_profdata_file_path = (
917 _CreateCoverageProfileDataFromTargetProfDataFiles(
918 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54919
Abhishek Aryac19bc5ef2018-05-04 22:10:02920 for target_profdata_file_path in target_profdata_file_paths:
921 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52922
Abhishek Aryac19bc5ef2018-05-04 22:10:02923 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54924
925
926def _BuildTargets(targets, jobs_count):
927 """Builds target with Clang coverage instrumentation.
928
929 This function requires current working directory to be the root of checkout.
930
931 Args:
932 targets: A list of targets to build with coverage instrumentation.
933 jobs_count: Number of jobs to run in parallel for compilation. If None, a
934 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54935 """
Abhishek Arya1ec832c2017-12-05 18:06:59936
Yuke Liao506e8822017-12-04 16:52:54937 def _IsGomaConfigured():
938 """Returns True if goma is enabled in the gn build args.
939
940 Returns:
941 A boolean indicates whether goma is configured for building or not.
942 """
Yuke Liao80afff32018-03-07 01:26:20943 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54944 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
945
Abhishek Aryafb70b532018-05-06 17:47:40946 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54947 if jobs_count is None and _IsGomaConfigured():
948 jobs_count = DEFAULT_GOMA_JOBS
949
950 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
951 if jobs_count is not None:
952 subprocess_cmd.append('-j' + str(jobs_count))
953
954 subprocess_cmd.extend(targets)
955 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40956 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54957
958
Abhishek Aryac19bc5ef2018-05-04 22:10:02959def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54960 """Runs commands and returns the relative paths to the profraw data files.
961
962 Args:
963 targets: A list of targets built with coverage instrumentation.
964 commands: A list of commands used to run the targets.
965
966 Returns:
967 A list of relative paths to the generated profraw data files.
968 """
Abhishek Aryafb70b532018-05-06 17:47:40969 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10970
Yuke Liao506e8822017-12-04 16:52:54971 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48972 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54973 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48974 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
975
976 # Ensure that logs directory exists.
977 if not os.path.exists(_GetLogsDirectoryPath()):
978 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54979
Abhishek Aryac19bc5ef2018-05-04 22:10:02980 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10981
Yuke Liaod4a9865202018-01-12 23:17:52982 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54983 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48984 output_file_name = os.extsep.join([target + '_output', 'log'])
985 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10986
Abhishek Aryac19bc5ef2018-05-04 22:10:02987 profdata_file_path = None
988 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40989 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02990 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10991
Abhishek Aryac19bc5ef2018-05-04 22:10:02992 if _IsIOSCommand(command):
993 # On iOS platform, due to lack of write permissions, profraw files are
994 # generated outside of the OUTPUT_DIR, and the exact paths are contained
995 # in the output of the command execution.
Abhishek Arya03911092018-05-21 16:42:35996 output = _ExecuteIOSCommand(command, output_file_path)
Abhishek Aryac19bc5ef2018-05-04 22:10:02997 else:
998 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
Abhishek Arya03911092018-05-21 16:42:35999 output = _ExecuteCommand(target, command, output_file_path)
Abhishek Aryac19bc5ef2018-05-04 22:10:021000
1001 profraw_file_paths = []
1002 if _IsIOS():
1003 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
1004 else:
Max Moroz7c5354f2018-05-06 00:03:481005 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:021006 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:481007 profraw_file_paths.append(
1008 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:021009
1010 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:401011 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryad35de7e2018-05-10 22:23:041012 'please make sure the binary exists, is properly instrumented and '
1013 'does not crash. %s' % (target, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:021014
1015 try:
1016 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
1017 target, profraw_file_paths)
1018 break
1019 except Exception:
Abhishek Aryad35de7e2018-05-10 22:23:041020 logging.info('Retrying...')
Abhishek Aryac19bc5ef2018-05-04 22:10:021021 finally:
1022 # Remove profraw files now so that they are not used in next iteration.
1023 for profraw_file_path in profraw_file_paths:
1024 os.remove(profraw_file_path)
1025
1026 assert profdata_file_path, (
Abhishek Aryad35de7e2018-05-10 22:23:041027 'Failed to merge target "%s" profraw files after %d retries. %s' %
1028 (target, MERGE_RETRIES, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:021029 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541030
Abhishek Aryafb70b532018-05-06 17:47:401031 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:101032
Abhishek Aryac19bc5ef2018-05-04 22:10:021033 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:541034
1035
Abhishek Arya03911092018-05-21 16:42:351036def _GetEnvironmentVars(profraw_file_path):
1037 """Return environment vars for subprocess, given a profraw file path."""
1038 env = os.environ.copy()
1039 env.update({
1040 'LLVM_PROFILE_FILE': profraw_file_path,
1041 'PATH': _GetPathWithLLVMSymbolizerDir()
1042 })
1043 return env
1044
1045
1046def _ExecuteCommand(target, command, output_file_path):
Yuke Liaoa0c8c2f2018-02-28 20:14:101047 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:521048 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:011049 #
Max Morozd73e45f2018-04-24 18:32:471050 # "%p" expands out to the process ID. It's not used by this scripts due to:
1051 # 1) If a target program spawns too many processess, it may exhaust all disk
1052 # space available. For example, unit_tests writes thousands of .profraw
1053 # files each of size 1GB+.
1054 # 2) If a target binary uses shared libraries, coverage profile data for them
1055 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011056 #
Yuke Liaod4a9865202018-01-12 23:17:521057 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1058 # is specified, the runtime creates a pool of N raw profiles which are used
1059 # for on-line profile merging. The runtime takes care of selecting a raw
1060 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521061 # N must be between 1 and 9. The merge pool specifier can only occur once per
1062 # filename pattern.
1063 #
Max Morozd73e45f2018-04-24 18:32:471064 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011065 #
Max Morozd73e45f2018-04-24 18:32:471066 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1067 # but it's not too big to consume too much computing resource or disk space.
1068 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591069 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011070 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481071 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541072 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541073
Yuke Liaoa0c8c2f2018-02-28 20:14:101074 try:
Max Moroz7c5354f2018-05-06 00:03:481075 # Some fuzz targets or tests may write into stderr, redirect it as well.
Abhishek Arya03911092018-05-21 16:42:351076 with open(output_file_path, 'wb') as output_file_handle:
1077 subprocess.check_call(
1078 shlex.split(command),
1079 stdout=output_file_handle,
1080 stderr=subprocess.STDOUT,
1081 env=_GetEnvironmentVars(expected_profraw_file_path))
Yuke Liaoa0c8c2f2018-02-28 20:14:101082 except subprocess.CalledProcessError as e:
Abhishek Arya03911092018-05-21 16:42:351083 logging.warning('Command: "%s" exited with non-zero return code.', command)
Yuke Liaoa0c8c2f2018-02-28 20:14:101084
Abhishek Arya03911092018-05-21 16:42:351085 return open(output_file_path, 'rb').read()
Yuke Liaoa0c8c2f2018-02-28 20:14:101086
1087
Yuke Liao27349c92018-03-22 21:10:011088def _IsFuzzerTarget(target):
1089 """Returns true if the target is a fuzzer target."""
1090 build_args = _GetBuildArgs()
1091 use_libfuzzer = ('use_libfuzzer' in build_args and
1092 build_args['use_libfuzzer'] == 'true')
1093 return use_libfuzzer and target.endswith('_fuzzer')
1094
1095
Abhishek Arya03911092018-05-21 16:42:351096def _ExecuteIOSCommand(command, output_file_path):
Yuke Liaoa0c8c2f2018-02-28 20:14:101097 """Runs a single iOS command and generates a profraw data file.
1098
1099 iOS application doesn't have write access to folders outside of the app, so
1100 it's impossible to instruct the app to flush the profraw data file to the
1101 desired location. The profraw data file will be generated somewhere within the
1102 application's Documents folder, and the full path can be obtained by parsing
1103 the output.
1104 """
Yuke Liaob2926832018-03-02 17:34:291105 assert _IsIOSCommand(command)
1106
1107 # After running tests, iossim generates a profraw data file, it won't be
1108 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1109 # checkout.
1110 iossim_profraw_file_path = os.path.join(
1111 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101112
1113 try:
Abhishek Arya03911092018-05-21 16:42:351114 with open(output_file_path, 'wb') as output_file_handle:
1115 subprocess.check_call(
1116 shlex.split(command),
1117 stdout=output_file_handle,
1118 stderr=subprocess.STDOUT,
1119 env=_GetEnvironmentVars(iossim_profraw_file_path))
Yuke Liaoa0c8c2f2018-02-28 20:14:101120 except subprocess.CalledProcessError as e:
1121 # iossim emits non-zero return code even if tests run successfully, so
1122 # ignore the return code.
Abhishek Arya03911092018-05-21 16:42:351123 pass
Yuke Liaoa0c8c2f2018-02-28 20:14:101124
Abhishek Arya03911092018-05-21 16:42:351125 return open(output_file_path, 'rb').read()
Yuke Liaoa0c8c2f2018-02-28 20:14:101126
1127
1128def _GetProfrawDataFileByParsingOutput(output):
1129 """Returns the path to the profraw data file obtained by parsing the output.
1130
1131 The output of running the test target has no format, but it is guaranteed to
1132 have a single line containing the path to the generated profraw data file.
1133 NOTE: This should only be called when target os is iOS.
1134 """
Yuke Liaob2926832018-03-02 17:34:291135 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101136
Yuke Liaob2926832018-03-02 17:34:291137 output_by_lines = ''.join(output).splitlines()
1138 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101139
1140 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291141 result = profraw_file_pattern.match(line)
1142 if result:
1143 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101144
1145 assert False, ('No profraw data file was generated, did you call '
1146 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1147 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541148
1149
Abhishek Aryac19bc5ef2018-05-04 22:10:021150def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1151 """Returns a relative path to coverage profdata file by merging target
1152 profdata files.
Yuke Liao506e8822017-12-04 16:52:541153
1154 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021155 profdata_file_paths: A list of relative paths to the profdata data files
1156 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541157
1158 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021159 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541160
1161 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021162 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541163 """
Abhishek Aryafb70b532018-05-06 17:47:401164 logging.info('Creating the coverage profile data file.')
1165 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481166 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541167 try:
Abhishek Arya1ec832c2017-12-05 18:06:591168 subprocess_cmd = [
1169 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1170 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021171 subprocess_cmd.extend(profdata_file_paths)
1172 subprocess.check_call(subprocess_cmd)
1173 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041174 logging.error(
1175 'Failed to merge target profdata files to create coverage profdata. %s',
1176 FILE_BUG_MESSAGE)
Abhishek Aryac19bc5ef2018-05-04 22:10:021177 raise error
1178
Abhishek Aryafb70b532018-05-06 17:47:401179 logging.debug('Finished merging target profdata files.')
1180 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021181 profdata_file_path)
1182 return profdata_file_path
1183
1184
1185def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1186 """Returns a relative path to target profdata file by merging target
1187 profraw files.
1188
1189 Args:
1190 profraw_file_paths: A list of relative paths to the profdata data files
1191 that are to be merged.
1192
1193 Returns:
1194 A relative path to the merged coverage profdata file.
1195
1196 Raises:
1197 CalledProcessError: An error occurred merging profdata files.
1198 """
Abhishek Aryafb70b532018-05-06 17:47:401199 logging.info('Creating target profile data file.')
1200 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021201 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1202
1203 try:
1204 subprocess_cmd = [
1205 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1206 ]
Yuke Liao506e8822017-12-04 16:52:541207 subprocess_cmd.extend(profraw_file_paths)
1208 subprocess.check_call(subprocess_cmd)
1209 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041210 logging.error(
1211 'Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541212 raise error
1213
Abhishek Aryafb70b532018-05-06 17:47:401214 logging.debug('Finished merging target profraw files.')
1215 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101216 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541217 return profdata_file_path
1218
1219
Yuke Liao0e4c8682018-04-18 21:06:591220def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1221 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331222 """Generates per file coverage summary using "llvm-cov export" command."""
1223 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1224 # [[-object BIN]] [SOURCES].
1225 # NOTE: For object files, the first one is specified as a positional argument,
1226 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101227 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401228 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331229 subprocess_cmd = [
1230 LLVM_COV_PATH, 'export', '-summary-only',
1231 '-instr-profile=' + profdata_file_path, binary_paths[0]
1232 ]
1233 subprocess_cmd.extend(
1234 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291235 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331236 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591237 if ignore_filename_regex:
1238 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331239
Max Moroz7c5354f2018-05-06 00:03:481240 export_output = subprocess.check_output(subprocess_cmd)
1241
1242 # Write output on the disk to be used by code coverage bot.
1243 with open(_GetSummaryFilePath(), 'w') as f:
1244 f.write(export_output)
1245
1246 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331247 assert len(json_output['data']) == 1
1248 files_coverage_data = json_output['data'][0]['files']
1249
1250 per_file_coverage_summary = {}
1251 for file_coverage_data in files_coverage_data:
1252 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401253 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1254 'File path "%s" in coverage summary is outside source checkout.' %
1255 file_path)
Yuke Liaoea228d02018-01-05 19:10:331256
Abhishek Aryafb70b532018-05-06 17:47:401257 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331258 if summary['lines']['count'] == 0:
1259 continue
1260
1261 per_file_coverage_summary[file_path] = _CoverageSummary(
1262 regions_total=summary['regions']['count'],
1263 regions_covered=summary['regions']['covered'],
1264 functions_total=summary['functions']['count'],
1265 functions_covered=summary['functions']['covered'],
1266 lines_total=summary['lines']['count'],
1267 lines_covered=summary['lines']['covered'])
1268
Abhishek Aryafb70b532018-05-06 17:47:401269 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331270 return per_file_coverage_summary
1271
1272
Yuke Liaob2926832018-03-02 17:34:291273def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1274 """Appends -arch arguments to the command list if it's ios platform.
1275
1276 iOS binaries are universal binaries, and require specifying the architecture
1277 to use, and one architecture needs to be specified for each binary.
1278 """
1279 if _IsIOS():
1280 cmd_list.extend(['-arch=x86_64'] * num_archs)
1281
1282
Yuke Liao506e8822017-12-04 16:52:541283def _GetBinaryPath(command):
1284 """Returns a relative path to the binary to be run by the command.
1285
Yuke Liao545db322018-02-15 17:12:011286 Currently, following types of commands are supported (e.g. url_unittests):
1287 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1288 2. Use xvfb.
1289 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1290 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371291 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1292 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101293 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371294 <iossim_arguments> -c <app_arguments>
1295 out/Coverage-iphonesimulator/url_unittests.app"
1296
Yuke Liao506e8822017-12-04 16:52:541297 Args:
1298 command: A command used to run a target.
1299
1300 Returns:
1301 A relative path to the binary.
1302 """
Yuke Liao545db322018-02-15 17:12:011303 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1304
Yuke Liaob2926832018-03-02 17:34:291305 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011306 if os.path.basename(command_parts[0]) == 'python':
1307 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401308 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011309 return command_parts[2]
1310
1311 if os.path.basename(command_parts[0]) == xvfb_script_name:
1312 return command_parts[1]
1313
Yuke Liaob2926832018-03-02 17:34:291314 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101315 # For a given application bundle, the binary resides in the bundle and has
1316 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371317 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101318 app_name = os.path.splitext(os.path.basename(app_path))[0]
1319 return os.path.join(app_path, app_name)
1320
Yuke Liaob2926832018-03-02 17:34:291321 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541322
1323
Yuke Liaob2926832018-03-02 17:34:291324def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101325 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291326 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101327
1328
Yuke Liao95d13d72017-12-07 18:18:501329def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1330 """Verifies that the target executables specified in the commands are inside
1331 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541332 for command in commands:
1333 binary_path = _GetBinaryPath(command)
Abhishek Arya03911092018-05-21 16:42:351334 binary_absolute_path = _GetFullPath(binary_path)
1335 assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
Yuke Liao95d13d72017-12-07 18:18:501336 'Target executable "%s" in command: "%s" is outside of '
1337 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541338
1339
1340def _ValidateBuildingWithClangCoverage():
1341 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201342 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541343
1344 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1345 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591346 assert False, ('\'{} = true\' is required in args.gn.'
1347 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541348
1349
Yuke Liaoc60b2d02018-03-02 21:40:431350def _ValidateCurrentPlatformIsSupported():
1351 """Asserts that this script suports running on the current platform"""
1352 target_os = _GetTargetOS()
1353 if target_os:
1354 current_platform = target_os
1355 else:
1356 current_platform = _GetHostPlatform()
1357
1358 assert current_platform in [
1359 'linux', 'mac', 'chromeos', 'ios'
1360 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1361
1362
Yuke Liao80afff32018-03-07 01:26:201363def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541364 """Parses args.gn file and returns results as a dictionary.
1365
1366 Returns:
1367 A dictionary representing the build args.
1368 """
Yuke Liao80afff32018-03-07 01:26:201369 global _BUILD_ARGS
1370 if _BUILD_ARGS is not None:
1371 return _BUILD_ARGS
1372
1373 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541374 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1375 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1376 'missing args.gn file.' % BUILD_DIR)
1377 with open(build_args_path) as build_args_file:
1378 build_args_lines = build_args_file.readlines()
1379
Yuke Liao506e8822017-12-04 16:52:541380 for build_arg_line in build_args_lines:
1381 build_arg_without_comments = build_arg_line.split('#')[0]
1382 key_value_pair = build_arg_without_comments.split('=')
1383 if len(key_value_pair) != 2:
1384 continue
1385
1386 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431387
1388 # Values are wrapped within a pair of double-quotes, so remove the leading
1389 # and trailing double-quotes.
1390 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201391 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541392
Yuke Liao80afff32018-03-07 01:26:201393 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541394
1395
Abhishek Arya16f059a2017-12-07 17:47:321396def _VerifyPathsAndReturnAbsolutes(paths):
1397 """Verifies that the paths specified in |paths| exist and returns absolute
1398 versions.
Yuke Liao66da1732017-12-05 22:19:421399
1400 Args:
1401 paths: A list of files or directories.
1402 """
Abhishek Arya16f059a2017-12-07 17:47:321403 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421404 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321405 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1406 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1407
1408 absolute_paths.append(absolute_path)
1409
1410 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421411
1412
Yuke Liaodd1ec0592018-02-02 01:26:371413def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1414 """Returns a target path relative to the directory of base_path.
1415
1416 This method requires base_path to be a file, otherwise, one should call
1417 os.path.relpath directly.
1418 """
1419 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141420 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371421 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141422 base_dir = os.path.dirname(base_path)
1423 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371424
1425
Abhishek Arya64636af2018-05-04 14:42:131426def _GetBinaryPathsFromTargets(targets, build_dir):
1427 """Return binary paths from target names."""
1428 # FIXME: Derive output binary from target build definitions rather than
1429 # assuming that it is always the same name.
1430 binary_paths = []
1431 for target in targets:
1432 binary_path = os.path.join(build_dir, target)
1433 if _GetHostPlatform() == 'win':
1434 binary_path += '.exe'
1435
1436 if os.path.exists(binary_path):
1437 binary_paths.append(binary_path)
1438 else:
1439 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401440 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131441 os.path.basename(binary_path))
1442
1443 return binary_paths
1444
1445
Abhishek Arya03911092018-05-21 16:42:351446def _GetFullPath(path):
1447 """Return full absolute path."""
1448 return (os.path.abspath(
1449 os.path.realpath(os.path.expandvars(os.path.expanduser(path)))))
1450
1451
1452def _GetCommandForWebTests(arguments):
1453 """Return command to run for blink web tests."""
1454 command_list = [
1455 'python', 'testing/xvfb.py', 'python',
1456 'third_party/blink/tools/run_web_tests.py',
1457 '--additional-driver-flag=--no-sandbox',
1458 '--disable-breakpad', '--no-show-results',
1459 '--child-processes=%d' % max(1, int(multiprocessing.cpu_count() / 2)),
1460 '--target=%s' % os.path.basename(BUILD_DIR), '--time-out-ms=30000'
1461 ]
1462 if arguments.strip():
1463 command_list.append(arguments)
1464 return ' '.join(command_list)
1465
1466
1467def _GetBinaryPathForWebTests():
1468 """Return binary path used to run blink web tests."""
1469 host_platform = _GetHostPlatform()
1470 if host_platform == 'win':
1471 return os.path.join(BUILD_DIR, 'content_shell.exe')
1472 elif host_platform == 'linux':
1473 return os.path.join(BUILD_DIR, 'content_shell')
1474 elif host_platform == 'mac':
1475 return os.path.join(BUILD_DIR, 'Content Shell.app', 'Contents', 'MacOS',
1476 'Content Shell')
1477 else:
1478 assert False, 'This platform is not supported for web tests.'
1479
1480
Yuke Liao506e8822017-12-04 16:52:541481def _ParseCommandArguments():
1482 """Adds and parses relevant arguments for tool comands.
1483
1484 Returns:
1485 A dictionary representing the arguments.
1486 """
1487 arg_parser = argparse.ArgumentParser()
1488 arg_parser.usage = __doc__
1489
Abhishek Arya1ec832c2017-12-05 18:06:591490 arg_parser.add_argument(
1491 '-b',
1492 '--build-dir',
1493 type=str,
1494 required=True,
1495 help='The build directory, the path needs to be relative to the root of '
1496 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541497
Abhishek Arya1ec832c2017-12-05 18:06:591498 arg_parser.add_argument(
1499 '-o',
1500 '--output-dir',
1501 type=str,
1502 required=True,
1503 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541504
Abhishek Arya1ec832c2017-12-05 18:06:591505 arg_parser.add_argument(
1506 '-c',
1507 '--command',
1508 action='append',
Abhishek Arya64636af2018-05-04 14:42:131509 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591510 help='Commands used to run test targets, one test target needs one and '
1511 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131512 'current working directory is the root of the checkout. This option is '
1513 'incompatible with -p/--profdata-file option.')
1514
1515 arg_parser.add_argument(
Abhishek Arya03911092018-05-21 16:42:351516 '-wt',
1517 '--web-tests',
1518 nargs='?',
1519 type=str,
1520 const=' ',
1521 required=False,
1522 help='Run blink web tests. Support passing arguments to run_web_tests.py')
1523
1524 arg_parser.add_argument(
Abhishek Arya64636af2018-05-04 14:42:131525 '-p',
1526 '--profdata-file',
1527 type=str,
1528 required=False,
1529 help='Path to profdata file to use for generating code coverage reports. '
1530 'This can be useful if you generated the profdata file seperately in '
1531 'your own test harness. This option is ignored if run command(s) are '
1532 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541533
Abhishek Arya1ec832c2017-12-05 18:06:591534 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421535 '-f',
1536 '--filters',
1537 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321538 required=False,
Yuke Liao66da1732017-12-05 22:19:421539 help='Directories or files to get code coverage for, and all files under '
1540 'the directories are included recursively.')
1541
1542 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591543 '-i',
1544 '--ignore-filename-regex',
1545 type=str,
1546 help='Skip source code files with file paths that match the given '
1547 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1548 'to exclude files in third_party/ and out/ folders from the report.')
1549
1550 arg_parser.add_argument(
Yuke Liao1b852fd2018-05-11 17:07:321551 '--no-file-view',
1552 action='store_true',
1553 help='Don\'t generate the file view in the coverage report. When there '
1554 'are large number of html files, the file view becomes heavy and may '
1555 'cause the browser to freeze, and this argument comes handy.')
1556
1557 arg_parser.add_argument(
Yuke Liao082e99632018-05-18 15:40:401558 '--coverage-tools-dir',
1559 type=str,
1560 help='Path of the directory where LLVM coverage tools (llvm-cov, '
1561 'llvm-profdata) exist. This should be only needed if you are testing '
1562 'against a custom built clang revision. Otherwise, we pick coverage '
1563 'tools automatically from your current source checkout.')
1564
1565 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591566 '-j',
1567 '--jobs',
1568 type=int,
1569 default=None,
1570 help='Run N jobs to build in parallel. If not specified, a default value '
1571 'will be derived based on CPUs availability. Please refer to '
1572 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541573
Abhishek Arya1ec832c2017-12-05 18:06:591574 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101575 '-v',
1576 '--verbose',
1577 action='store_true',
1578 help='Prints additional output for diagnostics.')
1579
1580 arg_parser.add_argument(
1581 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1582
1583 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021584 'targets',
1585 nargs='+',
1586 help='The names of the test targets to run. If multiple run commands are '
1587 'specified using the -c/--command option, then the order of targets and '
1588 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541589
1590 args = arg_parser.parse_args()
1591 return args
1592
1593
1594def Main():
1595 """Execute tool commands."""
Yuke Liao082e99632018-05-18 15:40:401596 # Setup coverage binaries even when script is called with empty params. This
1597 # is used by coverage bot for initial setup.
1598 if len(sys.argv) == 1:
1599 DownloadCoverageToolsIfNeeded()
1600 print(__doc__)
1601 return
1602
Abhishek Arya64636af2018-05-04 14:42:131603 # Change directory to source root to aid in relative paths calculations.
Abhishek Arya03911092018-05-21 16:42:351604 global SRC_ROOT_PATH
1605 SRC_ROOT_PATH = _GetFullPath(
1606 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Abhishek Arya64636af2018-05-04 14:42:131607 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111608
Yuke Liao506e8822017-12-04 16:52:541609 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131610 _ConfigureLogging(args)
Yuke Liao082e99632018-05-18 15:40:401611 _ConfigureLLVMCoverageTools(args)
Abhishek Arya64636af2018-05-04 14:42:131612
Yuke Liao506e8822017-12-04 16:52:541613 global BUILD_DIR
Abhishek Arya03911092018-05-21 16:42:351614 BUILD_DIR = _GetFullPath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541615 global OUTPUT_DIR
Abhishek Arya03911092018-05-21 16:42:351616 OUTPUT_DIR = _GetFullPath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541617
Abhishek Arya03911092018-05-21 16:42:351618 assert args.web_tests or args.command or args.profdata_file, (
Abhishek Arya64636af2018-05-04 14:42:131619 'Need to either provide commands to run using -c/--command option OR '
Abhishek Arya03911092018-05-21 16:42:351620 'provide prof-data file as input using -p/--profdata-file option OR '
1621 'run web tests using -wt/--run-web-tests.')
Yuke Liaoc60b2d02018-03-02 21:40:431622
Abhishek Arya64636af2018-05-04 14:42:131623 assert not args.command or (len(args.targets) == len(args.command)), (
1624 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431625
Abhishek Arya1ec832c2017-12-05 18:06:591626 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401627 'Build directory: "%s" doesn\'t exist. '
1628 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131629
Yuke Liaoc60b2d02018-03-02 21:40:431630 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541631 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321632
1633 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421634 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321635 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421636
Max Moroz7c5354f2018-05-06 00:03:481637 if not os.path.exists(_GetCoverageReportRootDirPath()):
1638 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541639
Abhishek Arya03911092018-05-21 16:42:351640 # Get .profdata file and list of binary paths.
1641 if args.web_tests:
1642 commands = [_GetCommandForWebTests(args.web_tests)]
1643 profdata_file_path = _CreateCoverageProfileDataForTargets(
1644 args.targets, commands, args.jobs)
1645 binary_paths = [_GetBinaryPathForWebTests()]
1646 elif args.command:
1647 for i in range(len(args.command)):
1648 assert not 'run_web_tests.py' in args.command[i], (
1649 'run_web_tests.py is not supported via --command argument. '
1650 'Please use --run-web-tests argument instead.')
1651
Abhishek Arya64636af2018-05-04 14:42:131652 # A list of commands are provided. Run them to generate profdata file, and
1653 # create a list of binary paths from parsing commands.
1654 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1655 profdata_file_path = _CreateCoverageProfileDataForTargets(
1656 args.targets, args.command, args.jobs)
1657 binary_paths = [_GetBinaryPath(command) for command in args.command]
1658 else:
1659 # An input prof-data file is already provided. Just calculate binary paths.
1660 profdata_file_path = args.profdata_file
1661 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331662
Abhishek Arya78120bc2018-05-07 20:53:541663 binary_paths.extend(_GetSharedLibraries(binary_paths))
1664
Yuke Liao481d3482018-01-29 19:17:101665 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401666 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371667 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591668 binary_paths, profdata_file_path, absolute_filter_paths,
1669 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371670 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591671 absolute_filter_paths,
1672 args.ignore_filename_regex)
Yuke Liao1b852fd2018-05-11 17:07:321673 if not args.no_file_view:
1674 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
Yuke Liaodd1ec0592018-02-02 01:26:371675
1676 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1677 per_file_coverage_summary)
1678 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
Yuke Liao1b852fd2018-05-11 17:07:321679 per_file_coverage_summary,
1680 args.no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:371681 _GenerateDirectoryViewHtmlIndexFile()
1682
1683 component_to_directories = _ExtractComponentToDirectoriesMapping()
1684 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1685 component_to_directories, per_directory_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:321686 _GeneratePerComponentCoverageInHtml(
1687 per_component_coverage_summary, component_to_directories,
1688 per_directory_coverage_summary, args.no_file_view)
1689 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
1690 args.no_file_view)
Yuke Liaoea228d02018-01-05 19:10:331691
1692 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371693 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331694 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481695 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331696
Abhishek Arya03911092018-05-21 16:42:351697 html_index_file_path = 'file://' + _GetFullPath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401698 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101699 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541700
Abhishek Arya1ec832c2017-12-05 18:06:591701
Yuke Liao506e8822017-12-04 16:52:541702if __name__ == '__main__':
1703 sys.exit(Main())