blob: 843bacbc7e1a3d6ffa012d4bda4f924c5da5b014 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3856
57 For an overview of how code coverage works in Chromium, please refer to
58 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5459"""
60
61from __future__ import print_function
62
63import sys
64
65import argparse
Yuke Liaoea228d02018-01-05 19:10:3366import json
Yuke Liao481d3482018-01-29 19:17:1067import logging
Yuke Liao506e8822017-12-04 16:52:5468import os
Yuke Liaob2926832018-03-02 17:34:2969import re
70import shlex
Max Moroz025d8952018-05-03 16:33:3471import shutil
Yuke Liao506e8822017-12-04 16:52:5472import subprocess
Yuke Liao506e8822017-12-04 16:52:5473import urllib2
74
Abhishek Arya1ec832c2017-12-05 18:06:5975sys.path.append(
76 os.path.join(
77 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
78 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5479import update as clang_update
80
Yuke Liaoea228d02018-01-05 19:10:3381sys.path.append(
82 os.path.join(
83 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
84 'third_party'))
85import jinja2
86from collections import defaultdict
87
Yuke Liao506e8822017-12-04 16:52:5488# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5989SRC_ROOT_PATH = os.path.abspath(
90 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5491
92# Absolute path to the code coverage tools binary.
93LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
94LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
95LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
96
97# Build directory, the value is parsed from command line arguments.
98BUILD_DIR = None
99
100# Output directory for generated artifacts, the value is parsed from command
101# line arguemnts.
102OUTPUT_DIR = None
103
104# Default number of jobs used to build when goma is configured and enabled.
105DEFAULT_GOMA_JOBS = 100
106
107# Name of the file extension for profraw data files.
108PROFRAW_FILE_EXTENSION = 'profraw'
109
110# Name of the final profdata file, and this file needs to be passed to
111# "llvm-cov" command in order to call "llvm-cov show" to inspect the
112# line-by-line coverage of specific files.
113PROFDATA_FILE_NAME = 'coverage.profdata'
114
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.
122DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
123COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
124FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
125
126# Used to extract a mapping between directories and components.
127COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
128
Yuke Liao80afff32018-03-07 01:26:20129# Caches the results returned by _GetBuildArgs, don't use this variable
130# directly, call _GetBuildArgs instead.
131_BUILD_ARGS = None
132
Yuke Liaoea228d02018-01-05 19:10:33133
134class _CoverageSummary(object):
135 """Encapsulates coverage summary representation."""
136
Yuke Liaodd1ec0592018-02-02 01:26:37137 def __init__(self,
138 regions_total=0,
139 regions_covered=0,
140 functions_total=0,
141 functions_covered=0,
142 lines_total=0,
143 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33144 """Initializes _CoverageSummary object."""
145 self._summary = {
146 'regions': {
147 'total': regions_total,
148 'covered': regions_covered
149 },
150 'functions': {
151 'total': functions_total,
152 'covered': functions_covered
153 },
154 'lines': {
155 'total': lines_total,
156 'covered': lines_covered
157 }
158 }
159
160 def Get(self):
161 """Returns summary as a dictionary."""
162 return self._summary
163
164 def AddSummary(self, other_summary):
165 """Adds another summary to this one element-wise."""
166 for feature in self._summary:
167 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
168 self._summary[feature]['covered'] += other_summary.Get()[feature][
169 'covered']
170
171
Yuke Liaodd1ec0592018-02-02 01:26:37172class _CoverageReportHtmlGenerator(object):
173 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33174
Yuke Liaodd1ec0592018-02-02 01:26:37175 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33176 """
177
Yuke Liaodd1ec0592018-02-02 01:26:37178 def __init__(self, output_path, table_entry_type):
179 """Initializes _CoverageReportHtmlGenerator object.
180
181 Args:
182 output_path: Path to the html report that will be generated.
183 table_entry_type: Type of the table entries to be displayed in the table
184 header. For example: 'Path', 'Component'.
185 """
Yuke Liaoea228d02018-01-05 19:10:33186 css_file_name = os.extsep.join(['style', 'css'])
187 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
188 assert os.path.exists(css_absolute_path), (
189 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
190 'is called first, and the css file is generated at: "%s"' %
191 css_absolute_path)
192
193 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37194 self._output_path = output_path
195 self._table_entry_type = table_entry_type
196
Yuke Liaoea228d02018-01-05 19:10:33197 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12198 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33199 template_dir = os.path.join(
200 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
201
202 jinja_env = jinja2.Environment(
203 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
204 self._header_template = jinja_env.get_template('header.html')
205 self._table_template = jinja_env.get_template('table.html')
206 self._footer_template = jinja_env.get_template('footer.html')
207
208 def AddLinkToAnotherReport(self, html_report_path, name, summary):
209 """Adds a link to another html report in this report.
210
211 The link to be added is assumed to be an entry in this directory.
212 """
Yuke Liaodd1ec0592018-02-02 01:26:37213 # Use relative paths instead of absolute paths to make the generated reports
214 # portable.
215 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
216 html_report_path, self._output_path)
217
Yuke Liaod54030e2018-01-08 17:34:12218 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37219 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12220 os.path.basename(html_report_path) ==
221 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
222 self._table_entries.append(table_entry)
223
224 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35225 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12226 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
227
228 def _CreateTableEntryFromCoverageSummary(self,
229 summary,
230 href=None,
231 name=None,
232 is_dir=None):
233 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37234 assert (href is None and name is None and is_dir is None) or (
235 href is not None and name is not None and is_dir is not None), (
236 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35237 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37238 'attributes must be None.')
239
Yuke Liaod54030e2018-01-08 17:34:12240 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37241 if href is not None:
242 entry['href'] = href
243 if name is not None:
244 entry['name'] = name
245 if is_dir is not None:
246 entry['is_dir'] = is_dir
247
Yuke Liaoea228d02018-01-05 19:10:33248 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12249 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37250 if summary_dict[feature]['total'] == 0:
251 percentage = 0.0
252 else:
Yuke Liao0e4c8682018-04-18 21:06:59253 percentage = float(summary_dict[feature]
254 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35255
Yuke Liaoea228d02018-01-05 19:10:33256 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12257 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33258 'total': summary_dict[feature]['total'],
259 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35260 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33261 'color_class': color_class
262 }
Yuke Liaod54030e2018-01-08 17:34:12263
Yuke Liaod54030e2018-01-08 17:34:12264 return entry
Yuke Liaoea228d02018-01-05 19:10:33265
266 def _GetColorClass(self, percentage):
267 """Returns the css color class based on coverage percentage."""
268 if percentage >= 0 and percentage < 80:
269 return 'red'
270 if percentage >= 80 and percentage < 100:
271 return 'yellow'
272 if percentage == 100:
273 return 'green'
274
275 assert False, 'Invalid coverage percentage: "%d"' % percentage
276
Yuke Liaodd1ec0592018-02-02 01:26:37277 def WriteHtmlCoverageReport(self):
278 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33279
280 In the report, sub-directories are displayed before files and within each
281 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33282 """
283
284 def EntryCmp(left, right):
285 """Compare function for table entries."""
286 if left['is_dir'] != right['is_dir']:
287 return -1 if left['is_dir'] == True else 1
288
Yuke Liaodd1ec0592018-02-02 01:26:37289 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33290
291 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
292
293 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37294 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
295 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
296 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
297
Yuke Liaoea228d02018-01-05 19:10:33298 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37299 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
300 directory_view_href=_GetRelativePathToDirectoryOfFile(
301 directory_view_path, self._output_path),
302 component_view_href=_GetRelativePathToDirectoryOfFile(
303 component_view_path, self._output_path),
304 file_view_href=_GetRelativePathToDirectoryOfFile(
305 file_view_path, self._output_path))
306
Yuke Liaod54030e2018-01-08 17:34:12307 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37308 entries=self._table_entries,
309 total_entry=self._total_entry,
310 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33311 html_footer = self._footer_template.render()
312
Yuke Liaodd1ec0592018-02-02 01:26:37313 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33314 html_file.write(html_header + html_table + html_footer)
315
Yuke Liao506e8822017-12-04 16:52:54316
Abhishek Arya64636af2018-05-04 14:42:13317def _ConfigureLogging(args):
318 """Configures logging settings for later use."""
319 log_level = logging.DEBUG if args.verbose else logging.INFO
320 log_format = '[%(asctime)s %(levelname)s] %(message)s'
321 log_file = args.log_file if args.log_file else None
322 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
323
324
Max Morozd73e45f2018-04-24 18:32:47325def _GetSharedLibraries(binary_paths):
326 """Returns set of shared libraries used by specified binaries."""
327 libraries = set()
328 cmd = []
329 shared_library_re = None
330
331 if sys.platform.startswith('linux'):
332 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13333 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
334 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47335 elif sys.platform.startswith('darwin'):
336 cmd.extend(['otool', '-L'])
337 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
338 else:
339 assert False, ('Cannot detect shared libraries used by the given targets.')
340
341 assert shared_library_re is not None
342
343 cmd.extend(binary_paths)
344 output = subprocess.check_output(cmd)
345
346 for line in output.splitlines():
347 m = shared_library_re.match(line)
348 if not m:
349 continue
350
351 shared_library_path = m.group(1)
352 if sys.platform.startswith('darwin'):
353 # otool outputs "@rpath" macro instead of the dirname of the given binary.
354 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
355
356 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
357 'the given target(s) does not '
358 'exist.' % shared_library_path)
359 with open(shared_library_path) as f:
360 data = f.read()
361
362 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
363 if '__llvm_cov' in data:
364 libraries.add(shared_library_path)
365
366 return list(libraries)
367
368
Yuke Liaoc60b2d02018-03-02 21:40:43369def _GetHostPlatform():
370 """Returns the host platform.
371
372 This is separate from the target platform/os that coverage is running for.
373 """
Abhishek Arya1ec832c2017-12-05 18:06:59374 if sys.platform == 'win32' or sys.platform == 'cygwin':
375 return 'win'
376 if sys.platform.startswith('linux'):
377 return 'linux'
378 else:
379 assert sys.platform == 'darwin'
380 return 'mac'
381
382
Yuke Liaoc60b2d02018-03-02 21:40:43383def _GetTargetOS():
384 """Returns the target os specified in args.gn file.
385
386 Returns an empty string is target_os is not specified.
387 """
Yuke Liao80afff32018-03-07 01:26:20388 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43389 return build_args['target_os'] if 'target_os' in build_args else ''
390
391
Yuke Liaob2926832018-03-02 17:34:29392def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10393 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43394 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10395
396
Yuke Liao506e8822017-12-04 16:52:54397# TODO(crbug.com/759794): remove this function once tools get included to
398# Clang bundle:
399# https://chromium-review.googlesource.com/c/chromium/src/+/688221
400def DownloadCoverageToolsIfNeeded():
401 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59402
Yuke Liaoc60b2d02018-03-02 21:40:43403 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54404 """Returns a pair of revision number by reading the build stamp file.
405
406 Args:
407 stamp_file_path: A path the build stamp file created by
408 tools/clang/scripts/update.py.
409 Returns:
410 A pair of integers represeting the main and sub revision respectively.
411 """
412 if not os.path.exists(stamp_file_path):
413 return 0, 0
414
415 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43416 stamp_file_line = stamp_file.readline()
417 if ',' in stamp_file_line:
418 package_version = stamp_file_line.rstrip().split(',')[0]
419 else:
420 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54421
Yuke Liaoc60b2d02018-03-02 21:40:43422 clang_revision_str, clang_sub_revision_str = package_version.split('-')
423 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59424
Yuke Liaoc60b2d02018-03-02 21:40:43425 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54426 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43427 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54428
429 coverage_revision_stamp_file = os.path.join(
430 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
431 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43432 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54433
Yuke Liaoea228d02018-01-05 19:10:33434 has_coverage_tools = (
435 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32436
Yuke Liaoea228d02018-01-05 19:10:33437 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54438 coverage_sub_revision == clang_sub_revision):
439 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43440 return
Yuke Liao506e8822017-12-04 16:52:54441
442 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
443 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
444
445 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43446 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54447 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43448 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54449 coverage_tools_url = (
450 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43451 else:
452 assert host_platform == 'win'
453 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54454
455 try:
456 clang_update.DownloadAndUnpack(coverage_tools_url,
457 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10458 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54459 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43460 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54461 file_handle.write('\n')
462 except urllib2.URLError:
463 raise Exception(
464 'Failed to download coverage tools: %s.' % coverage_tools_url)
465
466
Yuke Liaodd1ec0592018-02-02 01:26:37467def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59468 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54469 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
470
471 For a file with absolute path /a/b/x.cc, a html report is generated as:
472 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
473 OUTPUT_DIR/index.html.
474
475 Args:
476 binary_paths: A list of paths to the instrumented binaries.
477 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42478 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54479 """
Yuke Liao506e8822017-12-04 16:52:54480 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
481 # [[-object BIN]] [SOURCES]
482 # NOTE: For object files, the first one is specified as a positional argument,
483 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10484 logging.debug('Generating per file line by line coverage reports using '
485 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59486 subprocess_cmd = [
487 LLVM_COV_PATH, 'show', '-format=html',
488 '-output-dir={}'.format(OUTPUT_DIR),
489 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
490 ]
491 subprocess_cmd.extend(
492 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29493 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42494 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59495 if ignore_filename_regex:
496 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
497
Yuke Liao506e8822017-12-04 16:52:54498 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34499
500 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
501 # the platform name instead, as it simplifies the report dir structure when
502 # the same report is generated for different platforms.
503 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
504 platform_report_subdir_path = os.path.join(OUTPUT_DIR, _GetHostPlatform())
505 if os.path.exists(platform_report_subdir_path):
506 shutil.rmtree(platform_report_subdir_path)
507 os.rename(default_report_subdir_path, platform_report_subdir_path)
508
Yuke Liao481d3482018-01-29 19:17:10509 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54510
511
Yuke Liaodd1ec0592018-02-02 01:26:37512def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
513 """Generates html index file for file view."""
514 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
515 logging.debug('Generating file view html index file as: "%s".',
516 file_view_index_file_path)
517 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
518 'Path')
519 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33520
Yuke Liaodd1ec0592018-02-02 01:26:37521 for file_path in per_file_coverage_summary:
522 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
523
524 html_generator.AddLinkToAnotherReport(
525 _GetCoverageHtmlReportPathForFile(file_path),
526 os.path.relpath(file_path, SRC_ROOT_PATH),
527 per_file_coverage_summary[file_path])
528
529 html_generator.CreateTotalsEntry(totals_coverage_summary)
530 html_generator.WriteHtmlCoverageReport()
531 logging.debug('Finished generating file view html index file.')
532
533
534def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
535 """Calculates per directory coverage summary."""
536 logging.debug('Calculating per-directory coverage summary')
537 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
538
Yuke Liaoea228d02018-01-05 19:10:33539 for file_path in per_file_coverage_summary:
540 summary = per_file_coverage_summary[file_path]
541 parent_dir = os.path.dirname(file_path)
542 while True:
543 per_directory_coverage_summary[parent_dir].AddSummary(summary)
544
545 if parent_dir == SRC_ROOT_PATH:
546 break
547 parent_dir = os.path.dirname(parent_dir)
548
Yuke Liaodd1ec0592018-02-02 01:26:37549 logging.debug('Finished calculating per-directory coverage summary')
550 return per_directory_coverage_summary
551
552
553def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
554 per_file_coverage_summary):
555 """Generates per directory coverage breakdown in html."""
556 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33557 for dir_path in per_directory_coverage_summary:
558 _GenerateCoverageInHtmlForDirectory(
559 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
560
Yuke Liaodd1ec0592018-02-02 01:26:37561 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10562
Yuke Liaoea228d02018-01-05 19:10:33563
564def _GenerateCoverageInHtmlForDirectory(
565 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
566 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37567 html_generator = _CoverageReportHtmlGenerator(
568 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33569
570 for entry_name in os.listdir(dir_path):
571 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33572
Yuke Liaodd1ec0592018-02-02 01:26:37573 if entry_path in per_file_coverage_summary:
574 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
575 entry_coverage_summary = per_file_coverage_summary[entry_path]
576 elif entry_path in per_directory_coverage_summary:
577 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
578 entry_path)
579 entry_coverage_summary = per_directory_coverage_summary[entry_path]
580 else:
Yuke Liaoc7e607142018-02-05 20:26:14581 # Any file without executable lines shouldn't be included into the report.
582 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37583 continue
Yuke Liaoea228d02018-01-05 19:10:33584
Yuke Liaodd1ec0592018-02-02 01:26:37585 html_generator.AddLinkToAnotherReport(entry_html_report_path,
586 os.path.basename(entry_path),
587 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33588
Yuke Liaod54030e2018-01-08 17:34:12589 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37590 html_generator.WriteHtmlCoverageReport()
591
592
593def _GenerateDirectoryViewHtmlIndexFile():
594 """Generates the html index file for directory view.
595
596 Note that the index file is already generated under SRC_ROOT_PATH, so this
597 file simply redirects to it, and the reason of this extra layer is for
598 structural consistency with other views.
599 """
600 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
601 DIRECTORY_VIEW_INDEX_FILE)
602 logging.debug('Generating directory view html index file as: "%s".',
603 directory_view_index_file_path)
604 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
605 SRC_ROOT_PATH)
606 _WriteRedirectHtmlFile(directory_view_index_file_path,
607 src_root_html_report_path)
608 logging.debug('Finished generating directory view html index file.')
609
610
611def _CalculatePerComponentCoverageSummary(component_to_directories,
612 per_directory_coverage_summary):
613 """Calculates per component coverage summary."""
614 logging.debug('Calculating per-component coverage summary')
615 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
616
617 for component in component_to_directories:
618 for directory in component_to_directories[component]:
619 absolute_directory_path = os.path.abspath(directory)
620 if absolute_directory_path in per_directory_coverage_summary:
621 per_component_coverage_summary[component].AddSummary(
622 per_directory_coverage_summary[absolute_directory_path])
623
624 logging.debug('Finished calculating per-component coverage summary')
625 return per_component_coverage_summary
626
627
628def _ExtractComponentToDirectoriesMapping():
629 """Returns a mapping from components to directories."""
630 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
631 directory_to_component = component_mappings['dir-to-component']
632
633 component_to_directories = defaultdict(list)
634 for directory in directory_to_component:
635 component = directory_to_component[directory]
636 component_to_directories[component].append(directory)
637
638 return component_to_directories
639
640
641def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
642 component_to_directories,
643 per_directory_coverage_summary):
644 """Generates per-component coverage reports in html."""
645 logging.debug('Writing per-component coverage html reports.')
646 for component in per_component_coverage_summary:
647 _GenerateCoverageInHtmlForComponent(
648 component, per_component_coverage_summary, component_to_directories,
649 per_directory_coverage_summary)
650
651 logging.debug('Finished writing per-component coverage html reports.')
652
653
654def _GenerateCoverageInHtmlForComponent(
655 component_name, per_component_coverage_summary, component_to_directories,
656 per_directory_coverage_summary):
657 """Generates coverage html report for a component."""
658 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
659 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14660 component_html_report_dir = os.path.dirname(component_html_report_path)
661 if not os.path.exists(component_html_report_dir):
662 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37663
664 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
665 'Path')
666
667 for dir_path in component_to_directories[component_name]:
668 dir_absolute_path = os.path.abspath(dir_path)
669 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14670 # Any directory without an excercised file shouldn't be included into the
671 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37672 continue
673
674 html_generator.AddLinkToAnotherReport(
675 _GetCoverageHtmlReportPathForDirectory(dir_path),
676 os.path.relpath(dir_path, SRC_ROOT_PATH),
677 per_directory_coverage_summary[dir_absolute_path])
678
679 html_generator.CreateTotalsEntry(
680 per_component_coverage_summary[component_name])
681 html_generator.WriteHtmlCoverageReport()
682
683
684def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
685 """Generates the html index file for component view."""
686 component_view_index_file_path = os.path.join(OUTPUT_DIR,
687 COMPONENT_VIEW_INDEX_FILE)
688 logging.debug('Generating component view html index file as: "%s".',
689 component_view_index_file_path)
690 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
691 'Component')
692 totals_coverage_summary = _CoverageSummary()
693
694 for component in per_component_coverage_summary:
695 totals_coverage_summary.AddSummary(
696 per_component_coverage_summary[component])
697
698 html_generator.AddLinkToAnotherReport(
699 _GetCoverageHtmlReportPathForComponent(component), component,
700 per_component_coverage_summary[component])
701
702 html_generator.CreateTotalsEntry(totals_coverage_summary)
703 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14704 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33705
706
707def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37708 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33709 html_index_file_path = os.path.join(OUTPUT_DIR,
710 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37711 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
712 DIRECTORY_VIEW_INDEX_FILE)
713 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
714
715
716def _WriteRedirectHtmlFile(from_html_path, to_html_path):
717 """Writes a html file that redirects to another html file."""
718 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
719 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33720 content = ("""
721 <!DOCTYPE html>
722 <html>
723 <head>
724 <!-- HTML meta refresh URL redirection -->
725 <meta http-equiv="refresh" content="0; url=%s">
726 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37727 </html>""" % to_html_relative_path)
728 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33729 f.write(content)
730
731
Yuke Liaodd1ec0592018-02-02 01:26:37732def _GetCoverageHtmlReportPathForFile(file_path):
733 """Given a file path, returns the corresponding html report path."""
734 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
735 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
736
737 # '+' is used instead of os.path.join because both of them are absolute paths
738 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14739 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37740 return _GetCoverageReportRootDirPath() + html_report_path
741
742
743def _GetCoverageHtmlReportPathForDirectory(dir_path):
744 """Given a directory path, returns the corresponding html report path."""
745 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
746 html_report_path = os.path.join(
747 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
748
749 # '+' is used instead of os.path.join because both of them are absolute paths
750 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14751 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37752 return _GetCoverageReportRootDirPath() + html_report_path
753
754
755def _GetCoverageHtmlReportPathForComponent(component_name):
756 """Given a component, returns the corresponding html report path."""
757 component_file_name = component_name.lower().replace('>', '-')
758 html_report_name = os.extsep.join([component_file_name, 'html'])
759 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
760 html_report_name)
761
762
763def _GetCoverageReportRootDirPath():
764 """The root directory that contains all generated coverage html reports."""
Max Moroz025d8952018-05-03 16:33:34765 return os.path.join(os.path.abspath(OUTPUT_DIR), _GetHostPlatform())
Yuke Liaoea228d02018-01-05 19:10:33766
767
Yuke Liao506e8822017-12-04 16:52:54768def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
769 """Builds and runs target to generate the coverage profile data.
770
771 Args:
772 targets: A list of targets to build with coverage instrumentation.
773 commands: A list of commands used to run the targets.
774 jobs_count: Number of jobs to run in parallel for building. If None, a
775 default value is derived based on CPUs availability.
776
777 Returns:
778 A relative path to the generated profdata file.
779 """
780 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59781 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
782 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54783 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
784 profraw_file_paths)
785
Yuke Liaod4a9865202018-01-12 23:17:52786 for profraw_file_path in profraw_file_paths:
787 os.remove(profraw_file_path)
788
Yuke Liao506e8822017-12-04 16:52:54789 return profdata_file_path
790
791
792def _BuildTargets(targets, jobs_count):
793 """Builds target with Clang coverage instrumentation.
794
795 This function requires current working directory to be the root of checkout.
796
797 Args:
798 targets: A list of targets to build with coverage instrumentation.
799 jobs_count: Number of jobs to run in parallel for compilation. If None, a
800 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54801 """
Abhishek Arya1ec832c2017-12-05 18:06:59802
Yuke Liao506e8822017-12-04 16:52:54803 def _IsGomaConfigured():
804 """Returns True if goma is enabled in the gn build args.
805
806 Returns:
807 A boolean indicates whether goma is configured for building or not.
808 """
Yuke Liao80afff32018-03-07 01:26:20809 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54810 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
811
Yuke Liao481d3482018-01-29 19:17:10812 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54813 if jobs_count is None and _IsGomaConfigured():
814 jobs_count = DEFAULT_GOMA_JOBS
815
816 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
817 if jobs_count is not None:
818 subprocess_cmd.append('-j' + str(jobs_count))
819
820 subprocess_cmd.extend(targets)
821 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10822 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54823
824
825def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
826 """Runs commands and returns the relative paths to the profraw data files.
827
828 Args:
829 targets: A list of targets built with coverage instrumentation.
830 commands: A list of commands used to run the targets.
831
832 Returns:
833 A list of relative paths to the generated profraw data files.
834 """
Yuke Liao481d3482018-01-29 19:17:10835 logging.debug('Executing the test commands')
836
Yuke Liao506e8822017-12-04 16:52:54837 # Remove existing profraw data files.
838 for file_or_dir in os.listdir(OUTPUT_DIR):
839 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
840 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
841
Yuke Liaoa0c8c2f2018-02-28 20:14:10842 profraw_file_paths = []
843
Yuke Liaod4a9865202018-01-12 23:17:52844 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54845 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10846 output_file_name = os.extsep.join([target + '_output', 'txt'])
847 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
848 logging.info('Running command: "%s", the output is redirected to "%s"',
849 command, output_file_path)
850
Yuke Liaob2926832018-03-02 17:34:29851 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10852 # On iOS platform, due to lack of write permissions, profraw files are
853 # generated outside of the OUTPUT_DIR, and the exact paths are contained
854 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29855 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10856 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
857 else:
858 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
859 output = _ExecuteCommand(target, command)
860
861 with open(output_file_path, 'w') as output_file:
862 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54863
Yuke Liao481d3482018-01-29 19:17:10864 logging.debug('Finished executing the test commands')
865
Yuke Liaob2926832018-03-02 17:34:29866 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10867 return profraw_file_paths
868
Yuke Liao506e8822017-12-04 16:52:54869 for file_or_dir in os.listdir(OUTPUT_DIR):
870 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
871 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
872
873 # Assert one target/command generates at least one profraw data file.
874 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59875 assert any(
876 os.path.basename(profraw_file).startswith(target)
877 for profraw_file in profraw_file_paths), (
878 'Running target: %s failed to generate any profraw data file, '
879 'please make sure the binary exists and is properly instrumented.' %
880 target)
Yuke Liao506e8822017-12-04 16:52:54881
882 return profraw_file_paths
883
884
885def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10886 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52887 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01888 #
Max Morozd73e45f2018-04-24 18:32:47889 # "%p" expands out to the process ID. It's not used by this scripts due to:
890 # 1) If a target program spawns too many processess, it may exhaust all disk
891 # space available. For example, unit_tests writes thousands of .profraw
892 # files each of size 1GB+.
893 # 2) If a target binary uses shared libraries, coverage profile data for them
894 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01895 #
Yuke Liaod4a9865202018-01-12 23:17:52896 # "%Nm" expands out to the instrumented binary's signature. When this pattern
897 # is specified, the runtime creates a pool of N raw profiles which are used
898 # for on-line profile merging. The runtime takes care of selecting a raw
899 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52900 # N must be between 1 and 9. The merge pool specifier can only occur once per
901 # filename pattern.
902 #
Max Morozd73e45f2018-04-24 18:32:47903 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01904 #
Max Morozd73e45f2018-04-24 18:32:47905 # For other cases, "%4m" is chosen as it creates some level of parallelism,
906 # but it's not too big to consume too much computing resource or disk space.
907 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59908 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01909 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54910 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
911 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54912
Yuke Liaoa0c8c2f2018-02-28 20:14:10913 try:
914 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29915 shlex.split(command),
916 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10917 except subprocess.CalledProcessError as e:
918 output = e.output
919 logging.warning('Command: "%s" exited with non-zero return code', command)
920
921 return output
922
923
Yuke Liao27349c92018-03-22 21:10:01924def _IsFuzzerTarget(target):
925 """Returns true if the target is a fuzzer target."""
926 build_args = _GetBuildArgs()
927 use_libfuzzer = ('use_libfuzzer' in build_args and
928 build_args['use_libfuzzer'] == 'true')
929 return use_libfuzzer and target.endswith('_fuzzer')
930
931
Yuke Liaob2926832018-03-02 17:34:29932def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10933 """Runs a single iOS command and generates a profraw data file.
934
935 iOS application doesn't have write access to folders outside of the app, so
936 it's impossible to instruct the app to flush the profraw data file to the
937 desired location. The profraw data file will be generated somewhere within the
938 application's Documents folder, and the full path can be obtained by parsing
939 the output.
940 """
Yuke Liaob2926832018-03-02 17:34:29941 assert _IsIOSCommand(command)
942
943 # After running tests, iossim generates a profraw data file, it won't be
944 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
945 # checkout.
946 iossim_profraw_file_path = os.path.join(
947 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10948
949 try:
Yuke Liaob2926832018-03-02 17:34:29950 output = subprocess.check_output(
951 shlex.split(command),
952 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10953 except subprocess.CalledProcessError as e:
954 # iossim emits non-zero return code even if tests run successfully, so
955 # ignore the return code.
956 output = e.output
957
958 return output
959
960
961def _GetProfrawDataFileByParsingOutput(output):
962 """Returns the path to the profraw data file obtained by parsing the output.
963
964 The output of running the test target has no format, but it is guaranteed to
965 have a single line containing the path to the generated profraw data file.
966 NOTE: This should only be called when target os is iOS.
967 """
Yuke Liaob2926832018-03-02 17:34:29968 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10969
Yuke Liaob2926832018-03-02 17:34:29970 output_by_lines = ''.join(output).splitlines()
971 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10972
973 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29974 result = profraw_file_pattern.match(line)
975 if result:
976 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10977
978 assert False, ('No profraw data file was generated, did you call '
979 'coverage_util::ConfigureCoverageReportPath() in test setup? '
980 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54981
982
983def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
984 """Returns a relative path to the profdata file by merging profraw data files.
985
986 Args:
987 profraw_file_paths: A list of relative paths to the profraw data files that
988 are to be merged.
989
990 Returns:
991 A relative path to the generated profdata file.
992
993 Raises:
994 CalledProcessError: An error occurred merging profraw data files.
995 """
Yuke Liao481d3482018-01-29 19:17:10996 logging.info('Creating the coverage profile data file')
997 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54998 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
999 try:
Abhishek Arya1ec832c2017-12-05 18:06:591000 subprocess_cmd = [
1001 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1002 ]
Yuke Liao506e8822017-12-04 16:52:541003 subprocess_cmd.extend(profraw_file_paths)
1004 subprocess.check_call(subprocess_cmd)
1005 except subprocess.CalledProcessError as error:
1006 print('Failed to merge profraw files to create profdata file')
1007 raise error
1008
Yuke Liao481d3482018-01-29 19:17:101009 logging.debug('Finished merging profraw files')
1010 logging.info('Code coverage profile data is created as: %s',
1011 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541012 return profdata_file_path
1013
1014
Yuke Liao0e4c8682018-04-18 21:06:591015def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1016 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331017 """Generates per file coverage summary using "llvm-cov export" command."""
1018 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1019 # [[-object BIN]] [SOURCES].
1020 # NOTE: For object files, the first one is specified as a positional argument,
1021 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101022 logging.debug('Generating per-file code coverage summary using "llvm-cov '
1023 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:331024 subprocess_cmd = [
1025 LLVM_COV_PATH, 'export', '-summary-only',
1026 '-instr-profile=' + profdata_file_path, binary_paths[0]
1027 ]
1028 subprocess_cmd.extend(
1029 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291030 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331031 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591032 if ignore_filename_regex:
1033 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331034
1035 json_output = json.loads(subprocess.check_output(subprocess_cmd))
1036 assert len(json_output['data']) == 1
1037 files_coverage_data = json_output['data'][0]['files']
1038
1039 per_file_coverage_summary = {}
1040 for file_coverage_data in files_coverage_data:
1041 file_path = file_coverage_data['filename']
1042 summary = file_coverage_data['summary']
1043
Yuke Liaoea228d02018-01-05 19:10:331044 if summary['lines']['count'] == 0:
1045 continue
1046
1047 per_file_coverage_summary[file_path] = _CoverageSummary(
1048 regions_total=summary['regions']['count'],
1049 regions_covered=summary['regions']['covered'],
1050 functions_total=summary['functions']['count'],
1051 functions_covered=summary['functions']['covered'],
1052 lines_total=summary['lines']['count'],
1053 lines_covered=summary['lines']['covered'])
1054
Yuke Liao481d3482018-01-29 19:17:101055 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:331056 return per_file_coverage_summary
1057
1058
Yuke Liaob2926832018-03-02 17:34:291059def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1060 """Appends -arch arguments to the command list if it's ios platform.
1061
1062 iOS binaries are universal binaries, and require specifying the architecture
1063 to use, and one architecture needs to be specified for each binary.
1064 """
1065 if _IsIOS():
1066 cmd_list.extend(['-arch=x86_64'] * num_archs)
1067
1068
Yuke Liao506e8822017-12-04 16:52:541069def _GetBinaryPath(command):
1070 """Returns a relative path to the binary to be run by the command.
1071
Yuke Liao545db322018-02-15 17:12:011072 Currently, following types of commands are supported (e.g. url_unittests):
1073 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1074 2. Use xvfb.
1075 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1076 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371077 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1078 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101079 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371080 <iossim_arguments> -c <app_arguments>
1081 out/Coverage-iphonesimulator/url_unittests.app"
1082
Yuke Liao545db322018-02-15 17:12:011083
Yuke Liao506e8822017-12-04 16:52:541084 Args:
1085 command: A command used to run a target.
1086
1087 Returns:
1088 A relative path to the binary.
1089 """
Yuke Liao545db322018-02-15 17:12:011090 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1091
Yuke Liaob2926832018-03-02 17:34:291092 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011093 if os.path.basename(command_parts[0]) == 'python':
1094 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1095 'This tool doesn\'t understand the command: "%s"' % command)
1096 return command_parts[2]
1097
1098 if os.path.basename(command_parts[0]) == xvfb_script_name:
1099 return command_parts[1]
1100
Yuke Liaob2926832018-03-02 17:34:291101 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101102 # For a given application bundle, the binary resides in the bundle and has
1103 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371104 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101105 app_name = os.path.splitext(os.path.basename(app_path))[0]
1106 return os.path.join(app_path, app_name)
1107
Yuke Liaob2926832018-03-02 17:34:291108 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541109
1110
Yuke Liaob2926832018-03-02 17:34:291111def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101112 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291113 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101114
1115
Yuke Liao95d13d72017-12-07 18:18:501116def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1117 """Verifies that the target executables specified in the commands are inside
1118 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541119 for command in commands:
1120 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501121 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1122 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1123 'Target executable "%s" in command: "%s" is outside of '
1124 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541125
1126
1127def _ValidateBuildingWithClangCoverage():
1128 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201129 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541130
1131 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1132 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591133 assert False, ('\'{} = true\' is required in args.gn.'
1134 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541135
1136
Yuke Liaoc60b2d02018-03-02 21:40:431137def _ValidateCurrentPlatformIsSupported():
1138 """Asserts that this script suports running on the current platform"""
1139 target_os = _GetTargetOS()
1140 if target_os:
1141 current_platform = target_os
1142 else:
1143 current_platform = _GetHostPlatform()
1144
1145 assert current_platform in [
1146 'linux', 'mac', 'chromeos', 'ios'
1147 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1148
1149
Yuke Liao80afff32018-03-07 01:26:201150def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541151 """Parses args.gn file and returns results as a dictionary.
1152
1153 Returns:
1154 A dictionary representing the build args.
1155 """
Yuke Liao80afff32018-03-07 01:26:201156 global _BUILD_ARGS
1157 if _BUILD_ARGS is not None:
1158 return _BUILD_ARGS
1159
1160 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541161 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1162 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1163 'missing args.gn file.' % BUILD_DIR)
1164 with open(build_args_path) as build_args_file:
1165 build_args_lines = build_args_file.readlines()
1166
Yuke Liao506e8822017-12-04 16:52:541167 for build_arg_line in build_args_lines:
1168 build_arg_without_comments = build_arg_line.split('#')[0]
1169 key_value_pair = build_arg_without_comments.split('=')
1170 if len(key_value_pair) != 2:
1171 continue
1172
1173 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431174
1175 # Values are wrapped within a pair of double-quotes, so remove the leading
1176 # and trailing double-quotes.
1177 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201178 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541179
Yuke Liao80afff32018-03-07 01:26:201180 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541181
1182
Abhishek Arya16f059a2017-12-07 17:47:321183def _VerifyPathsAndReturnAbsolutes(paths):
1184 """Verifies that the paths specified in |paths| exist and returns absolute
1185 versions.
Yuke Liao66da1732017-12-05 22:19:421186
1187 Args:
1188 paths: A list of files or directories.
1189 """
Abhishek Arya16f059a2017-12-07 17:47:321190 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421191 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321192 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1193 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1194
1195 absolute_paths.append(absolute_path)
1196
1197 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421198
1199
Yuke Liaodd1ec0592018-02-02 01:26:371200def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1201 """Returns a target path relative to the directory of base_path.
1202
1203 This method requires base_path to be a file, otherwise, one should call
1204 os.path.relpath directly.
1205 """
1206 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141207 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371208 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141209 base_dir = os.path.dirname(base_path)
1210 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371211
1212
Abhishek Arya64636af2018-05-04 14:42:131213def _GetBinaryPathsFromTargets(targets, build_dir):
1214 """Return binary paths from target names."""
1215 # FIXME: Derive output binary from target build definitions rather than
1216 # assuming that it is always the same name.
1217 binary_paths = []
1218 for target in targets:
1219 binary_path = os.path.join(build_dir, target)
1220 if _GetHostPlatform() == 'win':
1221 binary_path += '.exe'
1222
1223 if os.path.exists(binary_path):
1224 binary_paths.append(binary_path)
1225 else:
1226 logging.warning(
1227 'Target binary %s not found in build directory, skipping.',
1228 os.path.basename(binary_path))
1229
1230 return binary_paths
1231
1232
Yuke Liao506e8822017-12-04 16:52:541233def _ParseCommandArguments():
1234 """Adds and parses relevant arguments for tool comands.
1235
1236 Returns:
1237 A dictionary representing the arguments.
1238 """
1239 arg_parser = argparse.ArgumentParser()
1240 arg_parser.usage = __doc__
1241
Abhishek Arya1ec832c2017-12-05 18:06:591242 arg_parser.add_argument(
1243 '-b',
1244 '--build-dir',
1245 type=str,
1246 required=True,
1247 help='The build directory, the path needs to be relative to the root of '
1248 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541249
Abhishek Arya1ec832c2017-12-05 18:06:591250 arg_parser.add_argument(
1251 '-o',
1252 '--output-dir',
1253 type=str,
1254 required=True,
1255 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541256
Abhishek Arya1ec832c2017-12-05 18:06:591257 arg_parser.add_argument(
1258 '-c',
1259 '--command',
1260 action='append',
Abhishek Arya64636af2018-05-04 14:42:131261 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591262 help='Commands used to run test targets, one test target needs one and '
1263 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131264 'current working directory is the root of the checkout. This option is '
1265 'incompatible with -p/--profdata-file option.')
1266
1267 arg_parser.add_argument(
1268 '-p',
1269 '--profdata-file',
1270 type=str,
1271 required=False,
1272 help='Path to profdata file to use for generating code coverage reports. '
1273 'This can be useful if you generated the profdata file seperately in '
1274 'your own test harness. This option is ignored if run command(s) are '
1275 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541276
Abhishek Arya1ec832c2017-12-05 18:06:591277 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421278 '-f',
1279 '--filters',
1280 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321281 required=False,
Yuke Liao66da1732017-12-05 22:19:421282 help='Directories or files to get code coverage for, and all files under '
1283 'the directories are included recursively.')
1284
1285 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591286 '-i',
1287 '--ignore-filename-regex',
1288 type=str,
1289 help='Skip source code files with file paths that match the given '
1290 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1291 'to exclude files in third_party/ and out/ folders from the report.')
1292
1293 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591294 '-j',
1295 '--jobs',
1296 type=int,
1297 default=None,
1298 help='Run N jobs to build in parallel. If not specified, a default value '
1299 'will be derived based on CPUs availability. Please refer to '
1300 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541301
Abhishek Arya1ec832c2017-12-05 18:06:591302 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101303 '-v',
1304 '--verbose',
1305 action='store_true',
1306 help='Prints additional output for diagnostics.')
1307
1308 arg_parser.add_argument(
1309 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1310
1311 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591312 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541313
1314 args = arg_parser.parse_args()
1315 return args
1316
1317
1318def Main():
1319 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131320 # Change directory to source root to aid in relative paths calculations.
1321 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111322
Abhishek Arya64636af2018-05-04 14:42:131323 # Setup coverage binaries even when script is called with empty params. This
1324 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111325 DownloadCoverageToolsIfNeeded()
1326
Yuke Liao506e8822017-12-04 16:52:541327 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131328 _ConfigureLogging(args)
1329
Yuke Liao506e8822017-12-04 16:52:541330 global BUILD_DIR
1331 BUILD_DIR = args.build_dir
1332 global OUTPUT_DIR
1333 OUTPUT_DIR = args.output_dir
1334
Abhishek Arya64636af2018-05-04 14:42:131335 assert args.command or args.profdata_file, (
1336 'Need to either provide commands to run using -c/--command option OR '
1337 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431338
Abhishek Arya64636af2018-05-04 14:42:131339 assert not args.command or (len(args.targets) == len(args.command)), (
1340 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431341
Abhishek Arya1ec832c2017-12-05 18:06:591342 assert os.path.exists(BUILD_DIR), (
1343 'Build directory: {} doesn\'t exist. '
1344 'Please run "gn gen" to generate.').format(BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131345
Yuke Liaoc60b2d02018-03-02 21:40:431346 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541347 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321348
1349 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421350 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321351 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421352
Yuke Liao506e8822017-12-04 16:52:541353 if not os.path.exists(OUTPUT_DIR):
1354 os.makedirs(OUTPUT_DIR)
1355
Abhishek Arya64636af2018-05-04 14:42:131356 # Get profdate file and list of binary paths.
1357 if args.command:
1358 # A list of commands are provided. Run them to generate profdata file, and
1359 # create a list of binary paths from parsing commands.
1360 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1361 profdata_file_path = _CreateCoverageProfileDataForTargets(
1362 args.targets, args.command, args.jobs)
1363 binary_paths = [_GetBinaryPath(command) for command in args.command]
1364 else:
1365 # An input prof-data file is already provided. Just calculate binary paths.
1366 profdata_file_path = args.profdata_file
1367 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331368
Yuke Liao481d3482018-01-29 19:17:101369 logging.info('Generating code coverage report in html (this can take a while '
1370 'depending on size of target!)')
Max Morozd73e45f2018-04-24 18:32:471371 binary_paths.extend(_GetSharedLibraries(binary_paths))
Yuke Liaodd1ec0592018-02-02 01:26:371372 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591373 binary_paths, profdata_file_path, absolute_filter_paths,
1374 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371375 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591376 absolute_filter_paths,
1377 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371378 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1379
1380 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1381 per_file_coverage_summary)
1382 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1383 per_file_coverage_summary)
1384 _GenerateDirectoryViewHtmlIndexFile()
1385
1386 component_to_directories = _ExtractComponentToDirectoriesMapping()
1387 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1388 component_to_directories, per_directory_coverage_summary)
1389 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1390 component_to_directories,
1391 per_directory_coverage_summary)
1392 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331393
1394 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371395 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331396 _OverwriteHtmlReportsIndexFile()
1397
Yuke Liao506e8822017-12-04 16:52:541398 html_index_file_path = 'file://' + os.path.abspath(
1399 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101400 logging.info('Index file for html report is generated as: %s',
1401 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541402
Abhishek Arya1ec832c2017-12-05 18:06:591403
Yuke Liao506e8822017-12-04 16:52:541404if __name__ == '__main__':
1405 sys.exit(Main())