blob: 314df3013eb4132ad9530f1b540c362ddba5e0c9 [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
Abhishek Aryac19bc5ef2018-05-04 22:10:02133# Retry failed merges.
134MERGE_RETRIES = 3
135
Yuke Liaoea228d02018-01-05 19:10:33136
137class _CoverageSummary(object):
138 """Encapsulates coverage summary representation."""
139
Yuke Liaodd1ec0592018-02-02 01:26:37140 def __init__(self,
141 regions_total=0,
142 regions_covered=0,
143 functions_total=0,
144 functions_covered=0,
145 lines_total=0,
146 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33147 """Initializes _CoverageSummary object."""
148 self._summary = {
149 'regions': {
150 'total': regions_total,
151 'covered': regions_covered
152 },
153 'functions': {
154 'total': functions_total,
155 'covered': functions_covered
156 },
157 'lines': {
158 'total': lines_total,
159 'covered': lines_covered
160 }
161 }
162
163 def Get(self):
164 """Returns summary as a dictionary."""
165 return self._summary
166
167 def AddSummary(self, other_summary):
168 """Adds another summary to this one element-wise."""
169 for feature in self._summary:
170 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
171 self._summary[feature]['covered'] += other_summary.Get()[feature][
172 'covered']
173
174
Yuke Liaodd1ec0592018-02-02 01:26:37175class _CoverageReportHtmlGenerator(object):
176 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33177
Yuke Liaodd1ec0592018-02-02 01:26:37178 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33179 """
180
Yuke Liaodd1ec0592018-02-02 01:26:37181 def __init__(self, output_path, table_entry_type):
182 """Initializes _CoverageReportHtmlGenerator object.
183
184 Args:
185 output_path: Path to the html report that will be generated.
186 table_entry_type: Type of the table entries to be displayed in the table
187 header. For example: 'Path', 'Component'.
188 """
Yuke Liaoea228d02018-01-05 19:10:33189 css_file_name = os.extsep.join(['style', 'css'])
190 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
191 assert os.path.exists(css_absolute_path), (
192 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
193 'is called first, and the css file is generated at: "%s"' %
194 css_absolute_path)
195
196 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37197 self._output_path = output_path
198 self._table_entry_type = table_entry_type
199
Yuke Liaoea228d02018-01-05 19:10:33200 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12201 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33202 template_dir = os.path.join(
203 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
204
205 jinja_env = jinja2.Environment(
206 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
207 self._header_template = jinja_env.get_template('header.html')
208 self._table_template = jinja_env.get_template('table.html')
209 self._footer_template = jinja_env.get_template('footer.html')
210
211 def AddLinkToAnotherReport(self, html_report_path, name, summary):
212 """Adds a link to another html report in this report.
213
214 The link to be added is assumed to be an entry in this directory.
215 """
Yuke Liaodd1ec0592018-02-02 01:26:37216 # Use relative paths instead of absolute paths to make the generated reports
217 # portable.
218 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
219 html_report_path, self._output_path)
220
Yuke Liaod54030e2018-01-08 17:34:12221 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37222 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12223 os.path.basename(html_report_path) ==
224 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
225 self._table_entries.append(table_entry)
226
227 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35228 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12229 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
230
231 def _CreateTableEntryFromCoverageSummary(self,
232 summary,
233 href=None,
234 name=None,
235 is_dir=None):
236 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37237 assert (href is None and name is None and is_dir is None) or (
238 href is not None and name is not None and is_dir is not None), (
239 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35240 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37241 'attributes must be None.')
242
Yuke Liaod54030e2018-01-08 17:34:12243 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37244 if href is not None:
245 entry['href'] = href
246 if name is not None:
247 entry['name'] = name
248 if is_dir is not None:
249 entry['is_dir'] = is_dir
250
Yuke Liaoea228d02018-01-05 19:10:33251 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12252 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37253 if summary_dict[feature]['total'] == 0:
254 percentage = 0.0
255 else:
Yuke Liao0e4c8682018-04-18 21:06:59256 percentage = float(summary_dict[feature]
257 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35258
Yuke Liaoea228d02018-01-05 19:10:33259 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12260 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33261 'total': summary_dict[feature]['total'],
262 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35263 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33264 'color_class': color_class
265 }
Yuke Liaod54030e2018-01-08 17:34:12266
Yuke Liaod54030e2018-01-08 17:34:12267 return entry
Yuke Liaoea228d02018-01-05 19:10:33268
269 def _GetColorClass(self, percentage):
270 """Returns the css color class based on coverage percentage."""
271 if percentage >= 0 and percentage < 80:
272 return 'red'
273 if percentage >= 80 and percentage < 100:
274 return 'yellow'
275 if percentage == 100:
276 return 'green'
277
278 assert False, 'Invalid coverage percentage: "%d"' % percentage
279
Yuke Liaodd1ec0592018-02-02 01:26:37280 def WriteHtmlCoverageReport(self):
281 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33282
283 In the report, sub-directories are displayed before files and within each
284 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33285 """
286
287 def EntryCmp(left, right):
288 """Compare function for table entries."""
289 if left['is_dir'] != right['is_dir']:
290 return -1 if left['is_dir'] == True else 1
291
Yuke Liaodd1ec0592018-02-02 01:26:37292 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33293
294 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
295
296 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37297 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
298 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
299 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
300
Yuke Liaoea228d02018-01-05 19:10:33301 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37302 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
303 directory_view_href=_GetRelativePathToDirectoryOfFile(
304 directory_view_path, self._output_path),
305 component_view_href=_GetRelativePathToDirectoryOfFile(
306 component_view_path, self._output_path),
307 file_view_href=_GetRelativePathToDirectoryOfFile(
308 file_view_path, self._output_path))
309
Yuke Liaod54030e2018-01-08 17:34:12310 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37311 entries=self._table_entries,
312 total_entry=self._total_entry,
313 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33314 html_footer = self._footer_template.render()
315
Yuke Liaodd1ec0592018-02-02 01:26:37316 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33317 html_file.write(html_header + html_table + html_footer)
318
Yuke Liao506e8822017-12-04 16:52:54319
Abhishek Arya64636af2018-05-04 14:42:13320def _ConfigureLogging(args):
321 """Configures logging settings for later use."""
322 log_level = logging.DEBUG if args.verbose else logging.INFO
323 log_format = '[%(asctime)s %(levelname)s] %(message)s'
324 log_file = args.log_file if args.log_file else None
325 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
326
327
Max Morozd73e45f2018-04-24 18:32:47328def _GetSharedLibraries(binary_paths):
329 """Returns set of shared libraries used by specified binaries."""
330 libraries = set()
331 cmd = []
332 shared_library_re = None
333
334 if sys.platform.startswith('linux'):
335 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13336 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
337 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47338 elif sys.platform.startswith('darwin'):
339 cmd.extend(['otool', '-L'])
340 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
341 else:
342 assert False, ('Cannot detect shared libraries used by the given targets.')
343
344 assert shared_library_re is not None
345
346 cmd.extend(binary_paths)
347 output = subprocess.check_output(cmd)
348
349 for line in output.splitlines():
350 m = shared_library_re.match(line)
351 if not m:
352 continue
353
354 shared_library_path = m.group(1)
355 if sys.platform.startswith('darwin'):
356 # otool outputs "@rpath" macro instead of the dirname of the given binary.
357 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
358
359 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
360 'the given target(s) does not '
361 'exist.' % shared_library_path)
362 with open(shared_library_path) as f:
363 data = f.read()
364
365 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
366 if '__llvm_cov' in data:
367 libraries.add(shared_library_path)
368
369 return list(libraries)
370
371
Yuke Liaoc60b2d02018-03-02 21:40:43372def _GetHostPlatform():
373 """Returns the host platform.
374
375 This is separate from the target platform/os that coverage is running for.
376 """
Abhishek Arya1ec832c2017-12-05 18:06:59377 if sys.platform == 'win32' or sys.platform == 'cygwin':
378 return 'win'
379 if sys.platform.startswith('linux'):
380 return 'linux'
381 else:
382 assert sys.platform == 'darwin'
383 return 'mac'
384
385
Yuke Liaoc60b2d02018-03-02 21:40:43386def _GetTargetOS():
387 """Returns the target os specified in args.gn file.
388
389 Returns an empty string is target_os is not specified.
390 """
Yuke Liao80afff32018-03-07 01:26:20391 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43392 return build_args['target_os'] if 'target_os' in build_args else ''
393
394
Yuke Liaob2926832018-03-02 17:34:29395def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10396 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43397 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10398
399
Yuke Liao506e8822017-12-04 16:52:54400# TODO(crbug.com/759794): remove this function once tools get included to
401# Clang bundle:
402# https://chromium-review.googlesource.com/c/chromium/src/+/688221
403def DownloadCoverageToolsIfNeeded():
404 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59405
Yuke Liaoc60b2d02018-03-02 21:40:43406 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54407 """Returns a pair of revision number by reading the build stamp file.
408
409 Args:
410 stamp_file_path: A path the build stamp file created by
411 tools/clang/scripts/update.py.
412 Returns:
413 A pair of integers represeting the main and sub revision respectively.
414 """
415 if not os.path.exists(stamp_file_path):
416 return 0, 0
417
418 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43419 stamp_file_line = stamp_file.readline()
420 if ',' in stamp_file_line:
421 package_version = stamp_file_line.rstrip().split(',')[0]
422 else:
423 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54424
Yuke Liaoc60b2d02018-03-02 21:40:43425 clang_revision_str, clang_sub_revision_str = package_version.split('-')
426 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59427
Yuke Liaoc60b2d02018-03-02 21:40:43428 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54429 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43430 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54431
432 coverage_revision_stamp_file = os.path.join(
433 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
434 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43435 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54436
Yuke Liaoea228d02018-01-05 19:10:33437 has_coverage_tools = (
438 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32439
Yuke Liaoea228d02018-01-05 19:10:33440 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54441 coverage_sub_revision == clang_sub_revision):
442 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43443 return
Yuke Liao506e8822017-12-04 16:52:54444
445 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
446 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
447
448 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43449 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54450 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43451 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54452 coverage_tools_url = (
453 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43454 else:
455 assert host_platform == 'win'
456 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54457
458 try:
459 clang_update.DownloadAndUnpack(coverage_tools_url,
460 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10461 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54462 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43463 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54464 file_handle.write('\n')
465 except urllib2.URLError:
466 raise Exception(
467 'Failed to download coverage tools: %s.' % coverage_tools_url)
468
469
Yuke Liaodd1ec0592018-02-02 01:26:37470def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59471 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54472 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
473
474 For a file with absolute path /a/b/x.cc, a html report is generated as:
475 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
476 OUTPUT_DIR/index.html.
477
478 Args:
479 binary_paths: A list of paths to the instrumented binaries.
480 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42481 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54482 """
Yuke Liao506e8822017-12-04 16:52:54483 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
484 # [[-object BIN]] [SOURCES]
485 # NOTE: For object files, the first one is specified as a positional argument,
486 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10487 logging.debug('Generating per file line by line coverage reports using '
488 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59489 subprocess_cmd = [
490 LLVM_COV_PATH, 'show', '-format=html',
491 '-output-dir={}'.format(OUTPUT_DIR),
492 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
493 ]
494 subprocess_cmd.extend(
495 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29496 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42497 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59498 if ignore_filename_regex:
499 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
500
Yuke Liao506e8822017-12-04 16:52:54501 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34502
503 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
504 # the platform name instead, as it simplifies the report dir structure when
505 # the same report is generated for different platforms.
506 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
507 platform_report_subdir_path = os.path.join(OUTPUT_DIR, _GetHostPlatform())
508 if os.path.exists(platform_report_subdir_path):
509 shutil.rmtree(platform_report_subdir_path)
510 os.rename(default_report_subdir_path, platform_report_subdir_path)
511
Yuke Liao481d3482018-01-29 19:17:10512 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54513
514
Yuke Liaodd1ec0592018-02-02 01:26:37515def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
516 """Generates html index file for file view."""
517 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
518 logging.debug('Generating file view html index file as: "%s".',
519 file_view_index_file_path)
520 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
521 'Path')
522 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33523
Yuke Liaodd1ec0592018-02-02 01:26:37524 for file_path in per_file_coverage_summary:
525 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
526
527 html_generator.AddLinkToAnotherReport(
528 _GetCoverageHtmlReportPathForFile(file_path),
529 os.path.relpath(file_path, SRC_ROOT_PATH),
530 per_file_coverage_summary[file_path])
531
532 html_generator.CreateTotalsEntry(totals_coverage_summary)
533 html_generator.WriteHtmlCoverageReport()
534 logging.debug('Finished generating file view html index file.')
535
536
537def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
538 """Calculates per directory coverage summary."""
539 logging.debug('Calculating per-directory coverage summary')
540 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
541
Yuke Liaoea228d02018-01-05 19:10:33542 for file_path in per_file_coverage_summary:
543 summary = per_file_coverage_summary[file_path]
544 parent_dir = os.path.dirname(file_path)
545 while True:
546 per_directory_coverage_summary[parent_dir].AddSummary(summary)
547
548 if parent_dir == SRC_ROOT_PATH:
549 break
550 parent_dir = os.path.dirname(parent_dir)
551
Yuke Liaodd1ec0592018-02-02 01:26:37552 logging.debug('Finished calculating per-directory coverage summary')
553 return per_directory_coverage_summary
554
555
556def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
557 per_file_coverage_summary):
558 """Generates per directory coverage breakdown in html."""
559 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33560 for dir_path in per_directory_coverage_summary:
561 _GenerateCoverageInHtmlForDirectory(
562 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
563
Yuke Liaodd1ec0592018-02-02 01:26:37564 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10565
Yuke Liaoea228d02018-01-05 19:10:33566
567def _GenerateCoverageInHtmlForDirectory(
568 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
569 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37570 html_generator = _CoverageReportHtmlGenerator(
571 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33572
573 for entry_name in os.listdir(dir_path):
574 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33575
Yuke Liaodd1ec0592018-02-02 01:26:37576 if entry_path in per_file_coverage_summary:
577 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
578 entry_coverage_summary = per_file_coverage_summary[entry_path]
579 elif entry_path in per_directory_coverage_summary:
580 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
581 entry_path)
582 entry_coverage_summary = per_directory_coverage_summary[entry_path]
583 else:
Yuke Liaoc7e607142018-02-05 20:26:14584 # Any file without executable lines shouldn't be included into the report.
585 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37586 continue
Yuke Liaoea228d02018-01-05 19:10:33587
Yuke Liaodd1ec0592018-02-02 01:26:37588 html_generator.AddLinkToAnotherReport(entry_html_report_path,
589 os.path.basename(entry_path),
590 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33591
Yuke Liaod54030e2018-01-08 17:34:12592 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37593 html_generator.WriteHtmlCoverageReport()
594
595
596def _GenerateDirectoryViewHtmlIndexFile():
597 """Generates the html index file for directory view.
598
599 Note that the index file is already generated under SRC_ROOT_PATH, so this
600 file simply redirects to it, and the reason of this extra layer is for
601 structural consistency with other views.
602 """
603 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
604 DIRECTORY_VIEW_INDEX_FILE)
605 logging.debug('Generating directory view html index file as: "%s".',
606 directory_view_index_file_path)
607 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
608 SRC_ROOT_PATH)
609 _WriteRedirectHtmlFile(directory_view_index_file_path,
610 src_root_html_report_path)
611 logging.debug('Finished generating directory view html index file.')
612
613
614def _CalculatePerComponentCoverageSummary(component_to_directories,
615 per_directory_coverage_summary):
616 """Calculates per component coverage summary."""
617 logging.debug('Calculating per-component coverage summary')
618 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
619
620 for component in component_to_directories:
621 for directory in component_to_directories[component]:
622 absolute_directory_path = os.path.abspath(directory)
623 if absolute_directory_path in per_directory_coverage_summary:
624 per_component_coverage_summary[component].AddSummary(
625 per_directory_coverage_summary[absolute_directory_path])
626
627 logging.debug('Finished calculating per-component coverage summary')
628 return per_component_coverage_summary
629
630
631def _ExtractComponentToDirectoriesMapping():
632 """Returns a mapping from components to directories."""
633 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
634 directory_to_component = component_mappings['dir-to-component']
635
636 component_to_directories = defaultdict(list)
637 for directory in directory_to_component:
638 component = directory_to_component[directory]
639 component_to_directories[component].append(directory)
640
641 return component_to_directories
642
643
644def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
645 component_to_directories,
646 per_directory_coverage_summary):
647 """Generates per-component coverage reports in html."""
648 logging.debug('Writing per-component coverage html reports.')
649 for component in per_component_coverage_summary:
650 _GenerateCoverageInHtmlForComponent(
651 component, per_component_coverage_summary, component_to_directories,
652 per_directory_coverage_summary)
653
654 logging.debug('Finished writing per-component coverage html reports.')
655
656
657def _GenerateCoverageInHtmlForComponent(
658 component_name, per_component_coverage_summary, component_to_directories,
659 per_directory_coverage_summary):
660 """Generates coverage html report for a component."""
661 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
662 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14663 component_html_report_dir = os.path.dirname(component_html_report_path)
664 if not os.path.exists(component_html_report_dir):
665 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37666
667 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
668 'Path')
669
670 for dir_path in component_to_directories[component_name]:
671 dir_absolute_path = os.path.abspath(dir_path)
672 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14673 # Any directory without an excercised file shouldn't be included into the
674 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37675 continue
676
677 html_generator.AddLinkToAnotherReport(
678 _GetCoverageHtmlReportPathForDirectory(dir_path),
679 os.path.relpath(dir_path, SRC_ROOT_PATH),
680 per_directory_coverage_summary[dir_absolute_path])
681
682 html_generator.CreateTotalsEntry(
683 per_component_coverage_summary[component_name])
684 html_generator.WriteHtmlCoverageReport()
685
686
687def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
688 """Generates the html index file for component view."""
689 component_view_index_file_path = os.path.join(OUTPUT_DIR,
690 COMPONENT_VIEW_INDEX_FILE)
691 logging.debug('Generating component view html index file as: "%s".',
692 component_view_index_file_path)
693 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
694 'Component')
695 totals_coverage_summary = _CoverageSummary()
696
697 for component in per_component_coverage_summary:
698 totals_coverage_summary.AddSummary(
699 per_component_coverage_summary[component])
700
701 html_generator.AddLinkToAnotherReport(
702 _GetCoverageHtmlReportPathForComponent(component), component,
703 per_component_coverage_summary[component])
704
705 html_generator.CreateTotalsEntry(totals_coverage_summary)
706 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14707 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33708
709
710def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37711 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33712 html_index_file_path = os.path.join(OUTPUT_DIR,
713 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37714 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
715 DIRECTORY_VIEW_INDEX_FILE)
716 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
717
718
719def _WriteRedirectHtmlFile(from_html_path, to_html_path):
720 """Writes a html file that redirects to another html file."""
721 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
722 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33723 content = ("""
724 <!DOCTYPE html>
725 <html>
726 <head>
727 <!-- HTML meta refresh URL redirection -->
728 <meta http-equiv="refresh" content="0; url=%s">
729 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37730 </html>""" % to_html_relative_path)
731 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33732 f.write(content)
733
734
Yuke Liaodd1ec0592018-02-02 01:26:37735def _GetCoverageHtmlReportPathForFile(file_path):
736 """Given a file path, returns the corresponding html report path."""
737 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
738 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
739
740 # '+' is used instead of os.path.join because both of them are absolute paths
741 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14742 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37743 return _GetCoverageReportRootDirPath() + html_report_path
744
745
746def _GetCoverageHtmlReportPathForDirectory(dir_path):
747 """Given a directory path, returns the corresponding html report path."""
748 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
749 html_report_path = os.path.join(
750 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
751
752 # '+' is used instead of os.path.join because both of them are absolute paths
753 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14754 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37755 return _GetCoverageReportRootDirPath() + html_report_path
756
757
758def _GetCoverageHtmlReportPathForComponent(component_name):
759 """Given a component, returns the corresponding html report path."""
760 component_file_name = component_name.lower().replace('>', '-')
761 html_report_name = os.extsep.join([component_file_name, 'html'])
762 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
763 html_report_name)
764
765
766def _GetCoverageReportRootDirPath():
767 """The root directory that contains all generated coverage html reports."""
Max Moroz025d8952018-05-03 16:33:34768 return os.path.join(os.path.abspath(OUTPUT_DIR), _GetHostPlatform())
Yuke Liaoea228d02018-01-05 19:10:33769
770
Yuke Liao506e8822017-12-04 16:52:54771def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
772 """Builds and runs target to generate the coverage profile data.
773
774 Args:
775 targets: A list of targets to build with coverage instrumentation.
776 commands: A list of commands used to run the targets.
777 jobs_count: Number of jobs to run in parallel for building. If None, a
778 default value is derived based on CPUs availability.
779
780 Returns:
781 A relative path to the generated profdata file.
782 """
783 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02784 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59785 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02786 coverage_profdata_file_path = (
787 _CreateCoverageProfileDataFromTargetProfDataFiles(
788 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54789
Abhishek Aryac19bc5ef2018-05-04 22:10:02790 for target_profdata_file_path in target_profdata_file_paths:
791 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52792
Abhishek Aryac19bc5ef2018-05-04 22:10:02793 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54794
795
796def _BuildTargets(targets, jobs_count):
797 """Builds target with Clang coverage instrumentation.
798
799 This function requires current working directory to be the root of checkout.
800
801 Args:
802 targets: A list of targets to build with coverage instrumentation.
803 jobs_count: Number of jobs to run in parallel for compilation. If None, a
804 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54805 """
Abhishek Arya1ec832c2017-12-05 18:06:59806
Yuke Liao506e8822017-12-04 16:52:54807 def _IsGomaConfigured():
808 """Returns True if goma is enabled in the gn build args.
809
810 Returns:
811 A boolean indicates whether goma is configured for building or not.
812 """
Yuke Liao80afff32018-03-07 01:26:20813 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54814 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
815
Yuke Liao481d3482018-01-29 19:17:10816 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54817 if jobs_count is None and _IsGomaConfigured():
818 jobs_count = DEFAULT_GOMA_JOBS
819
820 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
821 if jobs_count is not None:
822 subprocess_cmd.append('-j' + str(jobs_count))
823
824 subprocess_cmd.extend(targets)
825 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10826 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54827
828
Abhishek Aryac19bc5ef2018-05-04 22:10:02829def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54830 """Runs commands and returns the relative paths to the profraw data files.
831
832 Args:
833 targets: A list of targets built with coverage instrumentation.
834 commands: A list of commands used to run the targets.
835
836 Returns:
837 A list of relative paths to the generated profraw data files.
838 """
Yuke Liao481d3482018-01-29 19:17:10839 logging.debug('Executing the test commands')
840
Yuke Liao506e8822017-12-04 16:52:54841 # Remove existing profraw data files.
842 for file_or_dir in os.listdir(OUTPUT_DIR):
843 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
844 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
845
Abhishek Aryac19bc5ef2018-05-04 22:10:02846 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10847
Yuke Liaod4a9865202018-01-12 23:17:52848 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54849 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10850 output_file_name = os.extsep.join([target + '_output', 'txt'])
851 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10852
Abhishek Aryac19bc5ef2018-05-04 22:10:02853 profdata_file_path = None
854 for _ in xrange(MERGE_RETRIES):
855 logging.info('Running command: "%s", the output is redirected to "%s"',
856 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10857
Abhishek Aryac19bc5ef2018-05-04 22:10:02858 if _IsIOSCommand(command):
859 # On iOS platform, due to lack of write permissions, profraw files are
860 # generated outside of the OUTPUT_DIR, and the exact paths are contained
861 # in the output of the command execution.
862 output = _ExecuteIOSCommand(target, command)
863 else:
864 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
865 output = _ExecuteCommand(target, command)
866
867 with open(output_file_path, 'w') as output_file:
868 output_file.write(output)
869
870 profraw_file_paths = []
871 if _IsIOS():
872 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
873 else:
874 for file_or_dir in os.listdir(OUTPUT_DIR):
875 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
876 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
877
878 assert profraw_file_paths, (
879 'Running target %s failed to generate any profraw data file, '
880 'please make sure the binary exists and is properly '
881 'instrumented.' % target)
882
883 try:
884 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
885 target, profraw_file_paths)
886 break
887 except Exception:
888 print('Retrying...')
889 finally:
890 # Remove profraw files now so that they are not used in next iteration.
891 for profraw_file_path in profraw_file_paths:
892 os.remove(profraw_file_path)
893
894 assert profdata_file_path, (
895 'Failed to merge target %s profraw files after %d retries. '
896 'Please file a bug with command you used, commit position and args.gn '
897 'config here: '
898 'https://bugs.chromium.org/p/chromium/issues/entry?'
899 'components=Tools%%3ECodeCoverage'% (target, MERGE_RETRIES))
900 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54901
Yuke Liao481d3482018-01-29 19:17:10902 logging.debug('Finished executing the test commands')
903
Abhishek Aryac19bc5ef2018-05-04 22:10:02904 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54905
906
907def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10908 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52909 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01910 #
Max Morozd73e45f2018-04-24 18:32:47911 # "%p" expands out to the process ID. It's not used by this scripts due to:
912 # 1) If a target program spawns too many processess, it may exhaust all disk
913 # space available. For example, unit_tests writes thousands of .profraw
914 # files each of size 1GB+.
915 # 2) If a target binary uses shared libraries, coverage profile data for them
916 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01917 #
Yuke Liaod4a9865202018-01-12 23:17:52918 # "%Nm" expands out to the instrumented binary's signature. When this pattern
919 # is specified, the runtime creates a pool of N raw profiles which are used
920 # for on-line profile merging. The runtime takes care of selecting a raw
921 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52922 # N must be between 1 and 9. The merge pool specifier can only occur once per
923 # filename pattern.
924 #
Max Morozd73e45f2018-04-24 18:32:47925 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01926 #
Max Morozd73e45f2018-04-24 18:32:47927 # For other cases, "%4m" is chosen as it creates some level of parallelism,
928 # but it's not too big to consume too much computing resource or disk space.
929 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59930 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01931 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54932 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
933 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54934
Yuke Liaoa0c8c2f2018-02-28 20:14:10935 try:
936 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29937 shlex.split(command),
938 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10939 except subprocess.CalledProcessError as e:
940 output = e.output
941 logging.warning('Command: "%s" exited with non-zero return code', command)
942
943 return output
944
945
Yuke Liao27349c92018-03-22 21:10:01946def _IsFuzzerTarget(target):
947 """Returns true if the target is a fuzzer target."""
948 build_args = _GetBuildArgs()
949 use_libfuzzer = ('use_libfuzzer' in build_args and
950 build_args['use_libfuzzer'] == 'true')
951 return use_libfuzzer and target.endswith('_fuzzer')
952
953
Yuke Liaob2926832018-03-02 17:34:29954def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10955 """Runs a single iOS command and generates a profraw data file.
956
957 iOS application doesn't have write access to folders outside of the app, so
958 it's impossible to instruct the app to flush the profraw data file to the
959 desired location. The profraw data file will be generated somewhere within the
960 application's Documents folder, and the full path can be obtained by parsing
961 the output.
962 """
Yuke Liaob2926832018-03-02 17:34:29963 assert _IsIOSCommand(command)
964
965 # After running tests, iossim generates a profraw data file, it won't be
966 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
967 # checkout.
968 iossim_profraw_file_path = os.path.join(
969 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10970
971 try:
Yuke Liaob2926832018-03-02 17:34:29972 output = subprocess.check_output(
973 shlex.split(command),
974 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10975 except subprocess.CalledProcessError as e:
976 # iossim emits non-zero return code even if tests run successfully, so
977 # ignore the return code.
978 output = e.output
979
980 return output
981
982
983def _GetProfrawDataFileByParsingOutput(output):
984 """Returns the path to the profraw data file obtained by parsing the output.
985
986 The output of running the test target has no format, but it is guaranteed to
987 have a single line containing the path to the generated profraw data file.
988 NOTE: This should only be called when target os is iOS.
989 """
Yuke Liaob2926832018-03-02 17:34:29990 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10991
Yuke Liaob2926832018-03-02 17:34:29992 output_by_lines = ''.join(output).splitlines()
993 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10994
995 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29996 result = profraw_file_pattern.match(line)
997 if result:
998 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10999
1000 assert False, ('No profraw data file was generated, did you call '
1001 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1002 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541003
1004
Abhishek Aryac19bc5ef2018-05-04 22:10:021005def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1006 """Returns a relative path to coverage profdata file by merging target
1007 profdata files.
Yuke Liao506e8822017-12-04 16:52:541008
1009 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021010 profdata_file_paths: A list of relative paths to the profdata data files
1011 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541012
1013 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021014 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541015
1016 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021017 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541018 """
Yuke Liao481d3482018-01-29 19:17:101019 logging.info('Creating the coverage profile data file')
Abhishek Aryac19bc5ef2018-05-04 22:10:021020 logging.debug(
1021 'Merging target profdata files to create coverage profdata file')
Yuke Liao506e8822017-12-04 16:52:541022 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
1023 try:
Abhishek Arya1ec832c2017-12-05 18:06:591024 subprocess_cmd = [
1025 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1026 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021027 subprocess_cmd.extend(profdata_file_paths)
1028 subprocess.check_call(subprocess_cmd)
1029 except subprocess.CalledProcessError as error:
1030 print('Failed to merge target profdata files to create coverage profdata. '
1031 'Try again.')
1032 raise error
1033
1034 logging.debug('Finished merging target profdata files')
1035 logging.info('Code coverage profile data is created as: %s',
1036 profdata_file_path)
1037 return profdata_file_path
1038
1039
1040def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1041 """Returns a relative path to target profdata file by merging target
1042 profraw files.
1043
1044 Args:
1045 profraw_file_paths: A list of relative paths to the profdata data files
1046 that are to be merged.
1047
1048 Returns:
1049 A relative path to the merged coverage profdata file.
1050
1051 Raises:
1052 CalledProcessError: An error occurred merging profdata files.
1053 """
1054 logging.info('Creating target profile data file')
1055 logging.debug('Merging target profraw files to create target profdata file')
1056 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1057
1058 try:
1059 subprocess_cmd = [
1060 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1061 ]
Yuke Liao506e8822017-12-04 16:52:541062 subprocess_cmd.extend(profraw_file_paths)
1063 subprocess.check_call(subprocess_cmd)
1064 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021065 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541066 raise error
1067
Abhishek Aryac19bc5ef2018-05-04 22:10:021068 logging.debug('Finished merging target profraw files')
1069 logging.info('Target %s profile data is created as: %s', target,
Yuke Liao481d3482018-01-29 19:17:101070 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541071 return profdata_file_path
1072
1073
Yuke Liao0e4c8682018-04-18 21:06:591074def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1075 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331076 """Generates per file coverage summary using "llvm-cov export" command."""
1077 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1078 # [[-object BIN]] [SOURCES].
1079 # NOTE: For object files, the first one is specified as a positional argument,
1080 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101081 logging.debug('Generating per-file code coverage summary using "llvm-cov '
1082 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:331083 subprocess_cmd = [
1084 LLVM_COV_PATH, 'export', '-summary-only',
1085 '-instr-profile=' + profdata_file_path, binary_paths[0]
1086 ]
1087 subprocess_cmd.extend(
1088 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291089 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331090 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591091 if ignore_filename_regex:
1092 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331093
1094 json_output = json.loads(subprocess.check_output(subprocess_cmd))
1095 assert len(json_output['data']) == 1
1096 files_coverage_data = json_output['data'][0]['files']
1097
1098 per_file_coverage_summary = {}
1099 for file_coverage_data in files_coverage_data:
1100 file_path = file_coverage_data['filename']
1101 summary = file_coverage_data['summary']
1102
Yuke Liaoea228d02018-01-05 19:10:331103 if summary['lines']['count'] == 0:
1104 continue
1105
1106 per_file_coverage_summary[file_path] = _CoverageSummary(
1107 regions_total=summary['regions']['count'],
1108 regions_covered=summary['regions']['covered'],
1109 functions_total=summary['functions']['count'],
1110 functions_covered=summary['functions']['covered'],
1111 lines_total=summary['lines']['count'],
1112 lines_covered=summary['lines']['covered'])
1113
Yuke Liao481d3482018-01-29 19:17:101114 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:331115 return per_file_coverage_summary
1116
1117
Yuke Liaob2926832018-03-02 17:34:291118def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1119 """Appends -arch arguments to the command list if it's ios platform.
1120
1121 iOS binaries are universal binaries, and require specifying the architecture
1122 to use, and one architecture needs to be specified for each binary.
1123 """
1124 if _IsIOS():
1125 cmd_list.extend(['-arch=x86_64'] * num_archs)
1126
1127
Yuke Liao506e8822017-12-04 16:52:541128def _GetBinaryPath(command):
1129 """Returns a relative path to the binary to be run by the command.
1130
Yuke Liao545db322018-02-15 17:12:011131 Currently, following types of commands are supported (e.g. url_unittests):
1132 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1133 2. Use xvfb.
1134 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1135 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371136 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1137 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101138 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371139 <iossim_arguments> -c <app_arguments>
1140 out/Coverage-iphonesimulator/url_unittests.app"
1141
Yuke Liao545db322018-02-15 17:12:011142
Yuke Liao506e8822017-12-04 16:52:541143 Args:
1144 command: A command used to run a target.
1145
1146 Returns:
1147 A relative path to the binary.
1148 """
Yuke Liao545db322018-02-15 17:12:011149 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1150
Yuke Liaob2926832018-03-02 17:34:291151 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011152 if os.path.basename(command_parts[0]) == 'python':
1153 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1154 'This tool doesn\'t understand the command: "%s"' % command)
1155 return command_parts[2]
1156
1157 if os.path.basename(command_parts[0]) == xvfb_script_name:
1158 return command_parts[1]
1159
Yuke Liaob2926832018-03-02 17:34:291160 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101161 # For a given application bundle, the binary resides in the bundle and has
1162 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371163 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101164 app_name = os.path.splitext(os.path.basename(app_path))[0]
1165 return os.path.join(app_path, app_name)
1166
Yuke Liaob2926832018-03-02 17:34:291167 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541168
1169
Yuke Liaob2926832018-03-02 17:34:291170def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101171 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291172 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101173
1174
Yuke Liao95d13d72017-12-07 18:18:501175def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1176 """Verifies that the target executables specified in the commands are inside
1177 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541178 for command in commands:
1179 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501180 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1181 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1182 'Target executable "%s" in command: "%s" is outside of '
1183 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541184
1185
1186def _ValidateBuildingWithClangCoverage():
1187 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201188 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541189
1190 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1191 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591192 assert False, ('\'{} = true\' is required in args.gn.'
1193 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541194
1195
Yuke Liaoc60b2d02018-03-02 21:40:431196def _ValidateCurrentPlatformIsSupported():
1197 """Asserts that this script suports running on the current platform"""
1198 target_os = _GetTargetOS()
1199 if target_os:
1200 current_platform = target_os
1201 else:
1202 current_platform = _GetHostPlatform()
1203
1204 assert current_platform in [
1205 'linux', 'mac', 'chromeos', 'ios'
1206 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1207
1208
Yuke Liao80afff32018-03-07 01:26:201209def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541210 """Parses args.gn file and returns results as a dictionary.
1211
1212 Returns:
1213 A dictionary representing the build args.
1214 """
Yuke Liao80afff32018-03-07 01:26:201215 global _BUILD_ARGS
1216 if _BUILD_ARGS is not None:
1217 return _BUILD_ARGS
1218
1219 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541220 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1221 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1222 'missing args.gn file.' % BUILD_DIR)
1223 with open(build_args_path) as build_args_file:
1224 build_args_lines = build_args_file.readlines()
1225
Yuke Liao506e8822017-12-04 16:52:541226 for build_arg_line in build_args_lines:
1227 build_arg_without_comments = build_arg_line.split('#')[0]
1228 key_value_pair = build_arg_without_comments.split('=')
1229 if len(key_value_pair) != 2:
1230 continue
1231
1232 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431233
1234 # Values are wrapped within a pair of double-quotes, so remove the leading
1235 # and trailing double-quotes.
1236 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201237 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541238
Yuke Liao80afff32018-03-07 01:26:201239 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541240
1241
Abhishek Arya16f059a2017-12-07 17:47:321242def _VerifyPathsAndReturnAbsolutes(paths):
1243 """Verifies that the paths specified in |paths| exist and returns absolute
1244 versions.
Yuke Liao66da1732017-12-05 22:19:421245
1246 Args:
1247 paths: A list of files or directories.
1248 """
Abhishek Arya16f059a2017-12-07 17:47:321249 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421250 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321251 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1252 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1253
1254 absolute_paths.append(absolute_path)
1255
1256 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421257
1258
Yuke Liaodd1ec0592018-02-02 01:26:371259def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1260 """Returns a target path relative to the directory of base_path.
1261
1262 This method requires base_path to be a file, otherwise, one should call
1263 os.path.relpath directly.
1264 """
1265 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141266 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371267 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141268 base_dir = os.path.dirname(base_path)
1269 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371270
1271
Abhishek Arya64636af2018-05-04 14:42:131272def _GetBinaryPathsFromTargets(targets, build_dir):
1273 """Return binary paths from target names."""
1274 # FIXME: Derive output binary from target build definitions rather than
1275 # assuming that it is always the same name.
1276 binary_paths = []
1277 for target in targets:
1278 binary_path = os.path.join(build_dir, target)
1279 if _GetHostPlatform() == 'win':
1280 binary_path += '.exe'
1281
1282 if os.path.exists(binary_path):
1283 binary_paths.append(binary_path)
1284 else:
1285 logging.warning(
1286 'Target binary %s not found in build directory, skipping.',
1287 os.path.basename(binary_path))
1288
1289 return binary_paths
1290
1291
Yuke Liao506e8822017-12-04 16:52:541292def _ParseCommandArguments():
1293 """Adds and parses relevant arguments for tool comands.
1294
1295 Returns:
1296 A dictionary representing the arguments.
1297 """
1298 arg_parser = argparse.ArgumentParser()
1299 arg_parser.usage = __doc__
1300
Abhishek Arya1ec832c2017-12-05 18:06:591301 arg_parser.add_argument(
1302 '-b',
1303 '--build-dir',
1304 type=str,
1305 required=True,
1306 help='The build directory, the path needs to be relative to the root of '
1307 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541308
Abhishek Arya1ec832c2017-12-05 18:06:591309 arg_parser.add_argument(
1310 '-o',
1311 '--output-dir',
1312 type=str,
1313 required=True,
1314 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541315
Abhishek Arya1ec832c2017-12-05 18:06:591316 arg_parser.add_argument(
1317 '-c',
1318 '--command',
1319 action='append',
Abhishek Arya64636af2018-05-04 14:42:131320 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591321 help='Commands used to run test targets, one test target needs one and '
1322 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131323 'current working directory is the root of the checkout. This option is '
1324 'incompatible with -p/--profdata-file option.')
1325
1326 arg_parser.add_argument(
1327 '-p',
1328 '--profdata-file',
1329 type=str,
1330 required=False,
1331 help='Path to profdata file to use for generating code coverage reports. '
1332 'This can be useful if you generated the profdata file seperately in '
1333 'your own test harness. This option is ignored if run command(s) are '
1334 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541335
Abhishek Arya1ec832c2017-12-05 18:06:591336 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421337 '-f',
1338 '--filters',
1339 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321340 required=False,
Yuke Liao66da1732017-12-05 22:19:421341 help='Directories or files to get code coverage for, and all files under '
1342 'the directories are included recursively.')
1343
1344 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591345 '-i',
1346 '--ignore-filename-regex',
1347 type=str,
1348 help='Skip source code files with file paths that match the given '
1349 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1350 'to exclude files in third_party/ and out/ folders from the report.')
1351
1352 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591353 '-j',
1354 '--jobs',
1355 type=int,
1356 default=None,
1357 help='Run N jobs to build in parallel. If not specified, a default value '
1358 'will be derived based on CPUs availability. Please refer to '
1359 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541360
Abhishek Arya1ec832c2017-12-05 18:06:591361 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101362 '-v',
1363 '--verbose',
1364 action='store_true',
1365 help='Prints additional output for diagnostics.')
1366
1367 arg_parser.add_argument(
1368 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1369
1370 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021371 'targets',
1372 nargs='+',
1373 help='The names of the test targets to run. If multiple run commands are '
1374 'specified using the -c/--command option, then the order of targets and '
1375 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541376
1377 args = arg_parser.parse_args()
1378 return args
1379
1380
1381def Main():
1382 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131383 # Change directory to source root to aid in relative paths calculations.
1384 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111385
Abhishek Arya64636af2018-05-04 14:42:131386 # Setup coverage binaries even when script is called with empty params. This
1387 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111388 DownloadCoverageToolsIfNeeded()
1389
Yuke Liao506e8822017-12-04 16:52:541390 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131391 _ConfigureLogging(args)
1392
Yuke Liao506e8822017-12-04 16:52:541393 global BUILD_DIR
1394 BUILD_DIR = args.build_dir
1395 global OUTPUT_DIR
1396 OUTPUT_DIR = args.output_dir
1397
Abhishek Arya64636af2018-05-04 14:42:131398 assert args.command or args.profdata_file, (
1399 'Need to either provide commands to run using -c/--command option OR '
1400 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431401
Abhishek Arya64636af2018-05-04 14:42:131402 assert not args.command or (len(args.targets) == len(args.command)), (
1403 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431404
Abhishek Arya1ec832c2017-12-05 18:06:591405 assert os.path.exists(BUILD_DIR), (
1406 'Build directory: {} doesn\'t exist. '
1407 'Please run "gn gen" to generate.').format(BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131408
Yuke Liaoc60b2d02018-03-02 21:40:431409 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541410 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321411
1412 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421413 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321414 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421415
Yuke Liao506e8822017-12-04 16:52:541416 if not os.path.exists(OUTPUT_DIR):
1417 os.makedirs(OUTPUT_DIR)
1418
Abhishek Arya64636af2018-05-04 14:42:131419 # Get profdate file and list of binary paths.
1420 if args.command:
1421 # A list of commands are provided. Run them to generate profdata file, and
1422 # create a list of binary paths from parsing commands.
1423 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1424 profdata_file_path = _CreateCoverageProfileDataForTargets(
1425 args.targets, args.command, args.jobs)
1426 binary_paths = [_GetBinaryPath(command) for command in args.command]
1427 else:
1428 # An input prof-data file is already provided. Just calculate binary paths.
1429 profdata_file_path = args.profdata_file
1430 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331431
Yuke Liao481d3482018-01-29 19:17:101432 logging.info('Generating code coverage report in html (this can take a while '
1433 'depending on size of target!)')
Max Morozd73e45f2018-04-24 18:32:471434 binary_paths.extend(_GetSharedLibraries(binary_paths))
Yuke Liaodd1ec0592018-02-02 01:26:371435 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591436 binary_paths, profdata_file_path, absolute_filter_paths,
1437 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371438 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591439 absolute_filter_paths,
1440 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371441 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1442
1443 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1444 per_file_coverage_summary)
1445 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1446 per_file_coverage_summary)
1447 _GenerateDirectoryViewHtmlIndexFile()
1448
1449 component_to_directories = _ExtractComponentToDirectoriesMapping()
1450 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1451 component_to_directories, per_directory_coverage_summary)
1452 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1453 component_to_directories,
1454 per_directory_coverage_summary)
1455 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331456
1457 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371458 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331459 _OverwriteHtmlReportsIndexFile()
1460
Yuke Liao506e8822017-12-04 16:52:541461 html_index_file_path = 'file://' + os.path.abspath(
1462 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101463 logging.info('Index file for html report is generated as: %s',
1464 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541465
Abhishek Arya1ec832c2017-12-05 18:06:591466
Yuke Liao506e8822017-12-04 16:52:541467if __name__ == '__main__':
1468 sys.exit(Main())