blob: 58d016c047450123e30c669da5d767c812f63707 [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.
Max Moroz7c5354f2018-05-06 00:03:48113PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
114
115# Name of the file with summary information generated by llvm-cov export.
116SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54117
118# Build arg required for generating code coverage data.
119CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
120
Yuke Liaoea228d02018-01-05 19:10:33121# The default name of the html coverage report for a directory.
122DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
123
Yuke Liaodd1ec0592018-02-02 01:26:37124# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37125COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48126DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37127FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48128INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
129
130LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37131
132# Used to extract a mapping between directories and components.
133COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
134
Yuke Liao80afff32018-03-07 01:26:20135# Caches the results returned by _GetBuildArgs, don't use this variable
136# directly, call _GetBuildArgs instead.
137_BUILD_ARGS = None
138
Abhishek Aryac19bc5ef2018-05-04 22:10:02139# Retry failed merges.
140MERGE_RETRIES = 3
141
Yuke Liaoea228d02018-01-05 19:10:33142
143class _CoverageSummary(object):
144 """Encapsulates coverage summary representation."""
145
Yuke Liaodd1ec0592018-02-02 01:26:37146 def __init__(self,
147 regions_total=0,
148 regions_covered=0,
149 functions_total=0,
150 functions_covered=0,
151 lines_total=0,
152 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33153 """Initializes _CoverageSummary object."""
154 self._summary = {
155 'regions': {
156 'total': regions_total,
157 'covered': regions_covered
158 },
159 'functions': {
160 'total': functions_total,
161 'covered': functions_covered
162 },
163 'lines': {
164 'total': lines_total,
165 'covered': lines_covered
166 }
167 }
168
169 def Get(self):
170 """Returns summary as a dictionary."""
171 return self._summary
172
173 def AddSummary(self, other_summary):
174 """Adds another summary to this one element-wise."""
175 for feature in self._summary:
176 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
177 self._summary[feature]['covered'] += other_summary.Get()[feature][
178 'covered']
179
180
Yuke Liaodd1ec0592018-02-02 01:26:37181class _CoverageReportHtmlGenerator(object):
182 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33183
Yuke Liaodd1ec0592018-02-02 01:26:37184 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33185 """
186
Yuke Liaodd1ec0592018-02-02 01:26:37187 def __init__(self, output_path, table_entry_type):
188 """Initializes _CoverageReportHtmlGenerator object.
189
190 Args:
191 output_path: Path to the html report that will be generated.
192 table_entry_type: Type of the table entries to be displayed in the table
193 header. For example: 'Path', 'Component'.
194 """
Yuke Liaoea228d02018-01-05 19:10:33195 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48196 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33197 assert os.path.exists(css_absolute_path), (
198 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40199 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33200 css_absolute_path)
201
202 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37203 self._output_path = output_path
204 self._table_entry_type = table_entry_type
205
Yuke Liaoea228d02018-01-05 19:10:33206 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12207 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33208 template_dir = os.path.join(
209 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
210
211 jinja_env = jinja2.Environment(
212 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
213 self._header_template = jinja_env.get_template('header.html')
214 self._table_template = jinja_env.get_template('table.html')
215 self._footer_template = jinja_env.get_template('footer.html')
216
217 def AddLinkToAnotherReport(self, html_report_path, name, summary):
218 """Adds a link to another html report in this report.
219
220 The link to be added is assumed to be an entry in this directory.
221 """
Yuke Liaodd1ec0592018-02-02 01:26:37222 # Use relative paths instead of absolute paths to make the generated reports
223 # portable.
224 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
225 html_report_path, self._output_path)
226
Yuke Liaod54030e2018-01-08 17:34:12227 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37228 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12229 os.path.basename(html_report_path) ==
230 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
231 self._table_entries.append(table_entry)
232
233 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35234 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12235 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
236
237 def _CreateTableEntryFromCoverageSummary(self,
238 summary,
239 href=None,
240 name=None,
241 is_dir=None):
242 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37243 assert (href is None and name is None and is_dir is None) or (
244 href is not None and name is not None and is_dir is not None), (
245 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35246 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37247 'attributes must be None.')
248
Yuke Liaod54030e2018-01-08 17:34:12249 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37250 if href is not None:
251 entry['href'] = href
252 if name is not None:
253 entry['name'] = name
254 if is_dir is not None:
255 entry['is_dir'] = is_dir
256
Yuke Liaoea228d02018-01-05 19:10:33257 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12258 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37259 if summary_dict[feature]['total'] == 0:
260 percentage = 0.0
261 else:
Yuke Liao0e4c8682018-04-18 21:06:59262 percentage = float(summary_dict[feature]
263 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35264
Yuke Liaoea228d02018-01-05 19:10:33265 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12266 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33267 'total': summary_dict[feature]['total'],
268 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35269 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33270 'color_class': color_class
271 }
Yuke Liaod54030e2018-01-08 17:34:12272
Yuke Liaod54030e2018-01-08 17:34:12273 return entry
Yuke Liaoea228d02018-01-05 19:10:33274
275 def _GetColorClass(self, percentage):
276 """Returns the css color class based on coverage percentage."""
277 if percentage >= 0 and percentage < 80:
278 return 'red'
279 if percentage >= 80 and percentage < 100:
280 return 'yellow'
281 if percentage == 100:
282 return 'green'
283
Abhishek Aryafb70b532018-05-06 17:47:40284 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33285
Yuke Liaodd1ec0592018-02-02 01:26:37286 def WriteHtmlCoverageReport(self):
287 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33288
289 In the report, sub-directories are displayed before files and within each
290 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33291 """
292
293 def EntryCmp(left, right):
294 """Compare function for table entries."""
295 if left['is_dir'] != right['is_dir']:
296 return -1 if left['is_dir'] == True else 1
297
Yuke Liaodd1ec0592018-02-02 01:26:37298 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33299
300 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
301
302 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48303
304 directory_view_path = _GetDirectoryViewPath()
305 component_view_path = _GetComponentViewPath()
306 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37307
Yuke Liaoea228d02018-01-05 19:10:33308 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37309 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
310 directory_view_href=_GetRelativePathToDirectoryOfFile(
311 directory_view_path, self._output_path),
312 component_view_href=_GetRelativePathToDirectoryOfFile(
313 component_view_path, self._output_path),
314 file_view_href=_GetRelativePathToDirectoryOfFile(
315 file_view_path, self._output_path))
316
Yuke Liaod54030e2018-01-08 17:34:12317 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37318 entries=self._table_entries,
319 total_entry=self._total_entry,
320 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33321 html_footer = self._footer_template.render()
322
Yuke Liaodd1ec0592018-02-02 01:26:37323 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33324 html_file.write(html_header + html_table + html_footer)
325
Yuke Liao506e8822017-12-04 16:52:54326
Abhishek Arya64636af2018-05-04 14:42:13327def _ConfigureLogging(args):
328 """Configures logging settings for later use."""
329 log_level = logging.DEBUG if args.verbose else logging.INFO
330 log_format = '[%(asctime)s %(levelname)s] %(message)s'
331 log_file = args.log_file if args.log_file else None
332 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
333
334
Max Morozd73e45f2018-04-24 18:32:47335def _GetSharedLibraries(binary_paths):
336 """Returns set of shared libraries used by specified binaries."""
337 libraries = set()
338 cmd = []
339 shared_library_re = None
340
341 if sys.platform.startswith('linux'):
342 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13343 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
344 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47345 elif sys.platform.startswith('darwin'):
346 cmd.extend(['otool', '-L'])
347 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
348 else:
Abhishek Aryafb70b532018-05-06 17:47:40349 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47350
351 assert shared_library_re is not None
352
353 cmd.extend(binary_paths)
354 output = subprocess.check_output(cmd)
355
356 for line in output.splitlines():
357 m = shared_library_re.match(line)
358 if not m:
359 continue
360
361 shared_library_path = m.group(1)
362 if sys.platform.startswith('darwin'):
363 # otool outputs "@rpath" macro instead of the dirname of the given binary.
364 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
365
366 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
367 'the given target(s) does not '
368 'exist.' % shared_library_path)
369 with open(shared_library_path) as f:
370 data = f.read()
371
372 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
373 if '__llvm_cov' in data:
374 libraries.add(shared_library_path)
375
376 return list(libraries)
377
378
Yuke Liaoc60b2d02018-03-02 21:40:43379def _GetHostPlatform():
380 """Returns the host platform.
381
382 This is separate from the target platform/os that coverage is running for.
383 """
Abhishek Arya1ec832c2017-12-05 18:06:59384 if sys.platform == 'win32' or sys.platform == 'cygwin':
385 return 'win'
386 if sys.platform.startswith('linux'):
387 return 'linux'
388 else:
389 assert sys.platform == 'darwin'
390 return 'mac'
391
392
Yuke Liaoc60b2d02018-03-02 21:40:43393def _GetTargetOS():
394 """Returns the target os specified in args.gn file.
395
396 Returns an empty string is target_os is not specified.
397 """
Yuke Liao80afff32018-03-07 01:26:20398 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43399 return build_args['target_os'] if 'target_os' in build_args else ''
400
401
Yuke Liaob2926832018-03-02 17:34:29402def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10403 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43404 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10405
406
Yuke Liao506e8822017-12-04 16:52:54407# TODO(crbug.com/759794): remove this function once tools get included to
408# Clang bundle:
409# https://chromium-review.googlesource.com/c/chromium/src/+/688221
410def DownloadCoverageToolsIfNeeded():
411 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59412
Yuke Liaoc60b2d02018-03-02 21:40:43413 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54414 """Returns a pair of revision number by reading the build stamp file.
415
416 Args:
417 stamp_file_path: A path the build stamp file created by
418 tools/clang/scripts/update.py.
419 Returns:
420 A pair of integers represeting the main and sub revision respectively.
421 """
422 if not os.path.exists(stamp_file_path):
423 return 0, 0
424
425 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43426 stamp_file_line = stamp_file.readline()
427 if ',' in stamp_file_line:
428 package_version = stamp_file_line.rstrip().split(',')[0]
429 else:
430 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54431
Yuke Liaoc60b2d02018-03-02 21:40:43432 clang_revision_str, clang_sub_revision_str = package_version.split('-')
433 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59434
Yuke Liaoc60b2d02018-03-02 21:40:43435 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54436 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43437 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54438
439 coverage_revision_stamp_file = os.path.join(
440 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
441 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43442 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54443
Yuke Liaoea228d02018-01-05 19:10:33444 has_coverage_tools = (
445 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32446
Yuke Liaoea228d02018-01-05 19:10:33447 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54448 coverage_sub_revision == clang_sub_revision):
449 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43450 return
Yuke Liao506e8822017-12-04 16:52:54451
452 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
453 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
454
455 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43456 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54457 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43458 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54459 coverage_tools_url = (
460 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43461 else:
462 assert host_platform == 'win'
463 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54464
465 try:
466 clang_update.DownloadAndUnpack(coverage_tools_url,
467 clang_update.LLVM_BUILD_DIR)
Abhishek Aryafb70b532018-05-06 17:47:40468 logging.info('Coverage tools %s unpacked.', package_version)
Yuke Liao506e8822017-12-04 16:52:54469 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43470 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54471 file_handle.write('\n')
472 except urllib2.URLError:
473 raise Exception(
474 'Failed to download coverage tools: %s.' % coverage_tools_url)
475
476
Yuke Liaodd1ec0592018-02-02 01:26:37477def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59478 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54479 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
480
481 For a file with absolute path /a/b/x.cc, a html report is generated as:
482 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
483 OUTPUT_DIR/index.html.
484
485 Args:
486 binary_paths: A list of paths to the instrumented binaries.
487 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42488 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54489 """
Yuke Liao506e8822017-12-04 16:52:54490 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
491 # [[-object BIN]] [SOURCES]
492 # NOTE: For object files, the first one is specified as a positional argument,
493 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10494 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40495 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59496 subprocess_cmd = [
497 LLVM_COV_PATH, 'show', '-format=html',
498 '-output-dir={}'.format(OUTPUT_DIR),
499 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
500 ]
501 subprocess_cmd.extend(
502 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29503 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42504 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59505 if ignore_filename_regex:
506 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
507
Yuke Liao506e8822017-12-04 16:52:54508 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34509
510 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
511 # the platform name instead, as it simplifies the report dir structure when
512 # the same report is generated for different platforms.
513 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48514 platform_report_subdir_path = _GetCoverageReportRootDirPath()
515 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34516
Abhishek Aryafb70b532018-05-06 17:47:40517 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54518
519
Yuke Liaodd1ec0592018-02-02 01:26:37520def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
521 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48522 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37523 logging.debug('Generating file view html index file as: "%s".',
524 file_view_index_file_path)
525 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
526 'Path')
527 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33528
Yuke Liaodd1ec0592018-02-02 01:26:37529 for file_path in per_file_coverage_summary:
530 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
531
532 html_generator.AddLinkToAnotherReport(
533 _GetCoverageHtmlReportPathForFile(file_path),
534 os.path.relpath(file_path, SRC_ROOT_PATH),
535 per_file_coverage_summary[file_path])
536
537 html_generator.CreateTotalsEntry(totals_coverage_summary)
538 html_generator.WriteHtmlCoverageReport()
539 logging.debug('Finished generating file view html index file.')
540
541
542def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
543 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40544 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37545 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
546
Yuke Liaoea228d02018-01-05 19:10:33547 for file_path in per_file_coverage_summary:
548 summary = per_file_coverage_summary[file_path]
549 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40550
Yuke Liaoea228d02018-01-05 19:10:33551 while True:
552 per_directory_coverage_summary[parent_dir].AddSummary(summary)
553
554 if parent_dir == SRC_ROOT_PATH:
555 break
556 parent_dir = os.path.dirname(parent_dir)
557
Abhishek Aryafb70b532018-05-06 17:47:40558 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37559 return per_directory_coverage_summary
560
561
562def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
563 per_file_coverage_summary):
564 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40565 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33566 for dir_path in per_directory_coverage_summary:
567 _GenerateCoverageInHtmlForDirectory(
568 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
569
Abhishek Aryafb70b532018-05-06 17:47:40570 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10571
Yuke Liaoea228d02018-01-05 19:10:33572
573def _GenerateCoverageInHtmlForDirectory(
574 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
575 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37576 html_generator = _CoverageReportHtmlGenerator(
577 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33578
579 for entry_name in os.listdir(dir_path):
580 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33581
Yuke Liaodd1ec0592018-02-02 01:26:37582 if entry_path in per_file_coverage_summary:
583 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
584 entry_coverage_summary = per_file_coverage_summary[entry_path]
585 elif entry_path in per_directory_coverage_summary:
586 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
587 entry_path)
588 entry_coverage_summary = per_directory_coverage_summary[entry_path]
589 else:
Yuke Liaoc7e607142018-02-05 20:26:14590 # Any file without executable lines shouldn't be included into the report.
591 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37592 continue
Yuke Liaoea228d02018-01-05 19:10:33593
Yuke Liaodd1ec0592018-02-02 01:26:37594 html_generator.AddLinkToAnotherReport(entry_html_report_path,
595 os.path.basename(entry_path),
596 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33597
Yuke Liaod54030e2018-01-08 17:34:12598 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37599 html_generator.WriteHtmlCoverageReport()
600
601
602def _GenerateDirectoryViewHtmlIndexFile():
603 """Generates the html index file for directory view.
604
605 Note that the index file is already generated under SRC_ROOT_PATH, so this
606 file simply redirects to it, and the reason of this extra layer is for
607 structural consistency with other views.
608 """
Max Moroz7c5354f2018-05-06 00:03:48609 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37610 logging.debug('Generating directory view html index file as: "%s".',
611 directory_view_index_file_path)
612 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
613 SRC_ROOT_PATH)
614 _WriteRedirectHtmlFile(directory_view_index_file_path,
615 src_root_html_report_path)
616 logging.debug('Finished generating directory view html index file.')
617
618
619def _CalculatePerComponentCoverageSummary(component_to_directories,
620 per_directory_coverage_summary):
621 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40622 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37623 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
624
625 for component in component_to_directories:
626 for directory in component_to_directories[component]:
627 absolute_directory_path = os.path.abspath(directory)
628 if absolute_directory_path in per_directory_coverage_summary:
629 per_component_coverage_summary[component].AddSummary(
630 per_directory_coverage_summary[absolute_directory_path])
631
Abhishek Aryafb70b532018-05-06 17:47:40632 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37633 return per_component_coverage_summary
634
635
636def _ExtractComponentToDirectoriesMapping():
637 """Returns a mapping from components to directories."""
638 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
639 directory_to_component = component_mappings['dir-to-component']
640
641 component_to_directories = defaultdict(list)
642 for directory in directory_to_component:
643 component = directory_to_component[directory]
644 component_to_directories[component].append(directory)
645
646 return component_to_directories
647
648
649def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
650 component_to_directories,
651 per_directory_coverage_summary):
652 """Generates per-component coverage reports in html."""
653 logging.debug('Writing per-component coverage html reports.')
654 for component in per_component_coverage_summary:
655 _GenerateCoverageInHtmlForComponent(
656 component, per_component_coverage_summary, component_to_directories,
657 per_directory_coverage_summary)
658
659 logging.debug('Finished writing per-component coverage html reports.')
660
661
662def _GenerateCoverageInHtmlForComponent(
663 component_name, per_component_coverage_summary, component_to_directories,
664 per_directory_coverage_summary):
665 """Generates coverage html report for a component."""
666 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
667 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14668 component_html_report_dir = os.path.dirname(component_html_report_path)
669 if not os.path.exists(component_html_report_dir):
670 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37671
672 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
673 'Path')
674
675 for dir_path in component_to_directories[component_name]:
676 dir_absolute_path = os.path.abspath(dir_path)
677 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14678 # Any directory without an excercised file shouldn't be included into the
679 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37680 continue
681
682 html_generator.AddLinkToAnotherReport(
683 _GetCoverageHtmlReportPathForDirectory(dir_path),
684 os.path.relpath(dir_path, SRC_ROOT_PATH),
685 per_directory_coverage_summary[dir_absolute_path])
686
687 html_generator.CreateTotalsEntry(
688 per_component_coverage_summary[component_name])
689 html_generator.WriteHtmlCoverageReport()
690
691
692def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
693 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48694 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37695 logging.debug('Generating component view html index file as: "%s".',
696 component_view_index_file_path)
697 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
698 'Component')
699 totals_coverage_summary = _CoverageSummary()
700
701 for component in per_component_coverage_summary:
702 totals_coverage_summary.AddSummary(
703 per_component_coverage_summary[component])
704
705 html_generator.AddLinkToAnotherReport(
706 _GetCoverageHtmlReportPathForComponent(component), component,
707 per_component_coverage_summary[component])
708
709 html_generator.CreateTotalsEntry(totals_coverage_summary)
710 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14711 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33712
713
Max Moroz7c5354f2018-05-06 00:03:48714def _MergeTwoDirectories(src_path, dst_path):
715 """Merge src_path directory into dst_path directory."""
716 for filename in os.listdir(src_path):
717 dst_path = os.path.join(dst_path, filename)
718 if os.path.exists(dst_path):
719 shutil.rmtree(dst_path)
720 os.rename(os.path.join(src_path, filename), dst_path)
721 shutil.rmtree(src_path)
722
723
Yuke Liaoea228d02018-01-05 19:10:33724def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37725 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48726 html_index_file_path = _GetHtmlIndexPath()
727 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37728 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
729
730
731def _WriteRedirectHtmlFile(from_html_path, to_html_path):
732 """Writes a html file that redirects to another html file."""
733 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
734 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33735 content = ("""
736 <!DOCTYPE html>
737 <html>
738 <head>
739 <!-- HTML meta refresh URL redirection -->
740 <meta http-equiv="refresh" content="0; url=%s">
741 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37742 </html>""" % to_html_relative_path)
743 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33744 f.write(content)
745
746
Max Moroz7c5354f2018-05-06 00:03:48747def _CleanUpOutputDir():
748 """Perform a cleanup of the output dir."""
749 # Remove the default index.html file produced by llvm-cov.
750 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
751 if os.path.exists(index_path):
752 os.remove(index_path)
753
754
Yuke Liaodd1ec0592018-02-02 01:26:37755def _GetCoverageHtmlReportPathForFile(file_path):
756 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40757 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37758 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
759
760 # '+' is used instead of os.path.join because both of them are absolute paths
761 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14762 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37763 return _GetCoverageReportRootDirPath() + html_report_path
764
765
766def _GetCoverageHtmlReportPathForDirectory(dir_path):
767 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40768 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37769 html_report_path = os.path.join(
770 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
771
772 # '+' is used instead of os.path.join because both of them are absolute paths
773 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14774 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37775 return _GetCoverageReportRootDirPath() + html_report_path
776
777
778def _GetCoverageHtmlReportPathForComponent(component_name):
779 """Given a component, returns the corresponding html report path."""
780 component_file_name = component_name.lower().replace('>', '-')
781 html_report_name = os.extsep.join([component_file_name, 'html'])
782 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
783 html_report_name)
784
785
786def _GetCoverageReportRootDirPath():
787 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48788 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
789
790
791def _GetComponentViewPath():
792 """Path to the HTML file for the component view."""
793 return os.path.join(_GetCoverageReportRootDirPath(),
794 COMPONENT_VIEW_INDEX_FILE)
795
796
797def _GetDirectoryViewPath():
798 """Path to the HTML file for the directory view."""
799 return os.path.join(_GetCoverageReportRootDirPath(),
800 DIRECTORY_VIEW_INDEX_FILE)
801
802
803def _GetFileViewPath():
804 """Path to the HTML file for the file view."""
805 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
806
807
808def _GetLogsDirectoryPath():
809 """Path to the logs directory."""
810 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
811
812
813def _GetHtmlIndexPath():
814 """Path to the main HTML index file."""
815 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
816
817
818def _GetProfdataFilePath():
819 """Path to the resulting .profdata file."""
820 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
821
822
823def _GetSummaryFilePath():
824 """The JSON file that contains coverage summary written by llvm-cov export."""
825 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33826
827
Yuke Liao506e8822017-12-04 16:52:54828def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
829 """Builds and runs target to generate the coverage profile data.
830
831 Args:
832 targets: A list of targets to build with coverage instrumentation.
833 commands: A list of commands used to run the targets.
834 jobs_count: Number of jobs to run in parallel for building. If None, a
835 default value is derived based on CPUs availability.
836
837 Returns:
838 A relative path to the generated profdata file.
839 """
840 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02841 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59842 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02843 coverage_profdata_file_path = (
844 _CreateCoverageProfileDataFromTargetProfDataFiles(
845 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54846
Abhishek Aryac19bc5ef2018-05-04 22:10:02847 for target_profdata_file_path in target_profdata_file_paths:
848 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52849
Abhishek Aryac19bc5ef2018-05-04 22:10:02850 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54851
852
853def _BuildTargets(targets, jobs_count):
854 """Builds target with Clang coverage instrumentation.
855
856 This function requires current working directory to be the root of checkout.
857
858 Args:
859 targets: A list of targets to build with coverage instrumentation.
860 jobs_count: Number of jobs to run in parallel for compilation. If None, a
861 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54862 """
Abhishek Arya1ec832c2017-12-05 18:06:59863
Yuke Liao506e8822017-12-04 16:52:54864 def _IsGomaConfigured():
865 """Returns True if goma is enabled in the gn build args.
866
867 Returns:
868 A boolean indicates whether goma is configured for building or not.
869 """
Yuke Liao80afff32018-03-07 01:26:20870 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54871 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
872
Abhishek Aryafb70b532018-05-06 17:47:40873 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54874 if jobs_count is None and _IsGomaConfigured():
875 jobs_count = DEFAULT_GOMA_JOBS
876
877 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
878 if jobs_count is not None:
879 subprocess_cmd.append('-j' + str(jobs_count))
880
881 subprocess_cmd.extend(targets)
882 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40883 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54884
885
Abhishek Aryac19bc5ef2018-05-04 22:10:02886def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54887 """Runs commands and returns the relative paths to the profraw data files.
888
889 Args:
890 targets: A list of targets built with coverage instrumentation.
891 commands: A list of commands used to run the targets.
892
893 Returns:
894 A list of relative paths to the generated profraw data files.
895 """
Abhishek Aryafb70b532018-05-06 17:47:40896 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10897
Yuke Liao506e8822017-12-04 16:52:54898 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48899 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54900 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48901 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
902
903 # Ensure that logs directory exists.
904 if not os.path.exists(_GetLogsDirectoryPath()):
905 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54906
Abhishek Aryac19bc5ef2018-05-04 22:10:02907 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10908
Yuke Liaod4a9865202018-01-12 23:17:52909 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54910 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48911 output_file_name = os.extsep.join([target + '_output', 'log'])
912 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10913
Abhishek Aryac19bc5ef2018-05-04 22:10:02914 profdata_file_path = None
915 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40916 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02917 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10918
Abhishek Aryac19bc5ef2018-05-04 22:10:02919 if _IsIOSCommand(command):
920 # On iOS platform, due to lack of write permissions, profraw files are
921 # generated outside of the OUTPUT_DIR, and the exact paths are contained
922 # in the output of the command execution.
923 output = _ExecuteIOSCommand(target, command)
924 else:
925 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
926 output = _ExecuteCommand(target, command)
927
928 with open(output_file_path, 'w') as output_file:
929 output_file.write(output)
930
931 profraw_file_paths = []
932 if _IsIOS():
933 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
934 else:
Max Moroz7c5354f2018-05-06 00:03:48935 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02936 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48937 profraw_file_paths.append(
938 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02939
940 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40941 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02942 'please make sure the binary exists and is properly '
943 'instrumented.' % target)
944
945 try:
946 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
947 target, profraw_file_paths)
948 break
949 except Exception:
950 print('Retrying...')
951 finally:
952 # Remove profraw files now so that they are not used in next iteration.
953 for profraw_file_path in profraw_file_paths:
954 os.remove(profraw_file_path)
955
956 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40957 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02958 'Please file a bug with command you used, commit position and args.gn '
959 'config here: '
960 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40961 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02962 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54963
Abhishek Aryafb70b532018-05-06 17:47:40964 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10965
Abhishek Aryac19bc5ef2018-05-04 22:10:02966 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54967
968
969def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10970 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52971 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01972 #
Max Morozd73e45f2018-04-24 18:32:47973 # "%p" expands out to the process ID. It's not used by this scripts due to:
974 # 1) If a target program spawns too many processess, it may exhaust all disk
975 # space available. For example, unit_tests writes thousands of .profraw
976 # files each of size 1GB+.
977 # 2) If a target binary uses shared libraries, coverage profile data for them
978 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01979 #
Yuke Liaod4a9865202018-01-12 23:17:52980 # "%Nm" expands out to the instrumented binary's signature. When this pattern
981 # is specified, the runtime creates a pool of N raw profiles which are used
982 # for on-line profile merging. The runtime takes care of selecting a raw
983 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52984 # N must be between 1 and 9. The merge pool specifier can only occur once per
985 # filename pattern.
986 #
Max Morozd73e45f2018-04-24 18:32:47987 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01988 #
Max Morozd73e45f2018-04-24 18:32:47989 # For other cases, "%4m" is chosen as it creates some level of parallelism,
990 # but it's not too big to consume too much computing resource or disk space.
991 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59992 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01993 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:48994 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:54995 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54996
Yuke Liaoa0c8c2f2018-02-28 20:14:10997 try:
Max Moroz7c5354f2018-05-06 00:03:48998 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:10999 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291000 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481001 stderr=subprocess.STDOUT,
Yuke Liaob2926832018-03-02 17:34:291002 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101003 except subprocess.CalledProcessError as e:
1004 output = e.output
Abhishek Aryafb70b532018-05-06 17:47:401005 logging.warning('Command: "%s" exited with non-zero return code.', command)
Yuke Liaoa0c8c2f2018-02-28 20:14:101006
1007 return output
1008
1009
Yuke Liao27349c92018-03-22 21:10:011010def _IsFuzzerTarget(target):
1011 """Returns true if the target is a fuzzer target."""
1012 build_args = _GetBuildArgs()
1013 use_libfuzzer = ('use_libfuzzer' in build_args and
1014 build_args['use_libfuzzer'] == 'true')
1015 return use_libfuzzer and target.endswith('_fuzzer')
1016
1017
Yuke Liaob2926832018-03-02 17:34:291018def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101019 """Runs a single iOS command and generates a profraw data file.
1020
1021 iOS application doesn't have write access to folders outside of the app, so
1022 it's impossible to instruct the app to flush the profraw data file to the
1023 desired location. The profraw data file will be generated somewhere within the
1024 application's Documents folder, and the full path can be obtained by parsing
1025 the output.
1026 """
Yuke Liaob2926832018-03-02 17:34:291027 assert _IsIOSCommand(command)
1028
1029 # After running tests, iossim generates a profraw data file, it won't be
1030 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1031 # checkout.
1032 iossim_profraw_file_path = os.path.join(
1033 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101034
1035 try:
Yuke Liaob2926832018-03-02 17:34:291036 output = subprocess.check_output(
1037 shlex.split(command),
1038 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101039 except subprocess.CalledProcessError as e:
1040 # iossim emits non-zero return code even if tests run successfully, so
1041 # ignore the return code.
1042 output = e.output
1043
1044 return output
1045
1046
1047def _GetProfrawDataFileByParsingOutput(output):
1048 """Returns the path to the profraw data file obtained by parsing the output.
1049
1050 The output of running the test target has no format, but it is guaranteed to
1051 have a single line containing the path to the generated profraw data file.
1052 NOTE: This should only be called when target os is iOS.
1053 """
Yuke Liaob2926832018-03-02 17:34:291054 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101055
Yuke Liaob2926832018-03-02 17:34:291056 output_by_lines = ''.join(output).splitlines()
1057 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101058
1059 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291060 result = profraw_file_pattern.match(line)
1061 if result:
1062 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101063
1064 assert False, ('No profraw data file was generated, did you call '
1065 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1066 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541067
1068
Abhishek Aryac19bc5ef2018-05-04 22:10:021069def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1070 """Returns a relative path to coverage profdata file by merging target
1071 profdata files.
Yuke Liao506e8822017-12-04 16:52:541072
1073 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021074 profdata_file_paths: A list of relative paths to the profdata data files
1075 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541076
1077 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021078 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541079
1080 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021081 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541082 """
Abhishek Aryafb70b532018-05-06 17:47:401083 logging.info('Creating the coverage profile data file.')
1084 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481085 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541086 try:
Abhishek Arya1ec832c2017-12-05 18:06:591087 subprocess_cmd = [
1088 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1089 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021090 subprocess_cmd.extend(profdata_file_paths)
1091 subprocess.check_call(subprocess_cmd)
1092 except subprocess.CalledProcessError as error:
1093 print('Failed to merge target profdata files to create coverage profdata. '
1094 'Try again.')
1095 raise error
1096
Abhishek Aryafb70b532018-05-06 17:47:401097 logging.debug('Finished merging target profdata files.')
1098 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021099 profdata_file_path)
1100 return profdata_file_path
1101
1102
1103def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1104 """Returns a relative path to target profdata file by merging target
1105 profraw files.
1106
1107 Args:
1108 profraw_file_paths: A list of relative paths to the profdata data files
1109 that are to be merged.
1110
1111 Returns:
1112 A relative path to the merged coverage profdata file.
1113
1114 Raises:
1115 CalledProcessError: An error occurred merging profdata files.
1116 """
Abhishek Aryafb70b532018-05-06 17:47:401117 logging.info('Creating target profile data file.')
1118 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021119 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1120
1121 try:
1122 subprocess_cmd = [
1123 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1124 ]
Yuke Liao506e8822017-12-04 16:52:541125 subprocess_cmd.extend(profraw_file_paths)
1126 subprocess.check_call(subprocess_cmd)
1127 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021128 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541129 raise error
1130
Abhishek Aryafb70b532018-05-06 17:47:401131 logging.debug('Finished merging target profraw files.')
1132 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101133 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541134 return profdata_file_path
1135
1136
Yuke Liao0e4c8682018-04-18 21:06:591137def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1138 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331139 """Generates per file coverage summary using "llvm-cov export" command."""
1140 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1141 # [[-object BIN]] [SOURCES].
1142 # NOTE: For object files, the first one is specified as a positional argument,
1143 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101144 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401145 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331146 subprocess_cmd = [
1147 LLVM_COV_PATH, 'export', '-summary-only',
1148 '-instr-profile=' + profdata_file_path, binary_paths[0]
1149 ]
1150 subprocess_cmd.extend(
1151 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291152 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331153 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591154 if ignore_filename_regex:
1155 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331156
Max Moroz7c5354f2018-05-06 00:03:481157 export_output = subprocess.check_output(subprocess_cmd)
1158
1159 # Write output on the disk to be used by code coverage bot.
1160 with open(_GetSummaryFilePath(), 'w') as f:
1161 f.write(export_output)
1162
1163 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331164 assert len(json_output['data']) == 1
1165 files_coverage_data = json_output['data'][0]['files']
1166
1167 per_file_coverage_summary = {}
1168 for file_coverage_data in files_coverage_data:
1169 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401170 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1171 'File path "%s" in coverage summary is outside source checkout.' %
1172 file_path)
Yuke Liaoea228d02018-01-05 19:10:331173
Abhishek Aryafb70b532018-05-06 17:47:401174 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331175 if summary['lines']['count'] == 0:
1176 continue
1177
1178 per_file_coverage_summary[file_path] = _CoverageSummary(
1179 regions_total=summary['regions']['count'],
1180 regions_covered=summary['regions']['covered'],
1181 functions_total=summary['functions']['count'],
1182 functions_covered=summary['functions']['covered'],
1183 lines_total=summary['lines']['count'],
1184 lines_covered=summary['lines']['covered'])
1185
Abhishek Aryafb70b532018-05-06 17:47:401186 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331187 return per_file_coverage_summary
1188
1189
Yuke Liaob2926832018-03-02 17:34:291190def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1191 """Appends -arch arguments to the command list if it's ios platform.
1192
1193 iOS binaries are universal binaries, and require specifying the architecture
1194 to use, and one architecture needs to be specified for each binary.
1195 """
1196 if _IsIOS():
1197 cmd_list.extend(['-arch=x86_64'] * num_archs)
1198
1199
Yuke Liao506e8822017-12-04 16:52:541200def _GetBinaryPath(command):
1201 """Returns a relative path to the binary to be run by the command.
1202
Yuke Liao545db322018-02-15 17:12:011203 Currently, following types of commands are supported (e.g. url_unittests):
1204 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1205 2. Use xvfb.
1206 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1207 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371208 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1209 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101210 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371211 <iossim_arguments> -c <app_arguments>
1212 out/Coverage-iphonesimulator/url_unittests.app"
1213
Yuke Liao545db322018-02-15 17:12:011214
Yuke Liao506e8822017-12-04 16:52:541215 Args:
1216 command: A command used to run a target.
1217
1218 Returns:
1219 A relative path to the binary.
1220 """
Yuke Liao545db322018-02-15 17:12:011221 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1222
Yuke Liaob2926832018-03-02 17:34:291223 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011224 if os.path.basename(command_parts[0]) == 'python':
1225 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401226 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011227 return command_parts[2]
1228
1229 if os.path.basename(command_parts[0]) == xvfb_script_name:
1230 return command_parts[1]
1231
Yuke Liaob2926832018-03-02 17:34:291232 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101233 # For a given application bundle, the binary resides in the bundle and has
1234 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371235 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101236 app_name = os.path.splitext(os.path.basename(app_path))[0]
1237 return os.path.join(app_path, app_name)
1238
Yuke Liaob2926832018-03-02 17:34:291239 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541240
1241
Yuke Liaob2926832018-03-02 17:34:291242def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101243 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291244 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101245
1246
Yuke Liao95d13d72017-12-07 18:18:501247def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1248 """Verifies that the target executables specified in the commands are inside
1249 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541250 for command in commands:
1251 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501252 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481253 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501254 'Target executable "%s" in command: "%s" is outside of '
1255 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541256
1257
1258def _ValidateBuildingWithClangCoverage():
1259 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201260 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541261
1262 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1263 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591264 assert False, ('\'{} = true\' is required in args.gn.'
1265 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541266
1267
Yuke Liaoc60b2d02018-03-02 21:40:431268def _ValidateCurrentPlatformIsSupported():
1269 """Asserts that this script suports running on the current platform"""
1270 target_os = _GetTargetOS()
1271 if target_os:
1272 current_platform = target_os
1273 else:
1274 current_platform = _GetHostPlatform()
1275
1276 assert current_platform in [
1277 'linux', 'mac', 'chromeos', 'ios'
1278 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1279
1280
Yuke Liao80afff32018-03-07 01:26:201281def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541282 """Parses args.gn file and returns results as a dictionary.
1283
1284 Returns:
1285 A dictionary representing the build args.
1286 """
Yuke Liao80afff32018-03-07 01:26:201287 global _BUILD_ARGS
1288 if _BUILD_ARGS is not None:
1289 return _BUILD_ARGS
1290
1291 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541292 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1293 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1294 'missing args.gn file.' % BUILD_DIR)
1295 with open(build_args_path) as build_args_file:
1296 build_args_lines = build_args_file.readlines()
1297
Yuke Liao506e8822017-12-04 16:52:541298 for build_arg_line in build_args_lines:
1299 build_arg_without_comments = build_arg_line.split('#')[0]
1300 key_value_pair = build_arg_without_comments.split('=')
1301 if len(key_value_pair) != 2:
1302 continue
1303
1304 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431305
1306 # Values are wrapped within a pair of double-quotes, so remove the leading
1307 # and trailing double-quotes.
1308 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201309 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541310
Yuke Liao80afff32018-03-07 01:26:201311 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541312
1313
Abhishek Arya16f059a2017-12-07 17:47:321314def _VerifyPathsAndReturnAbsolutes(paths):
1315 """Verifies that the paths specified in |paths| exist and returns absolute
1316 versions.
Yuke Liao66da1732017-12-05 22:19:421317
1318 Args:
1319 paths: A list of files or directories.
1320 """
Abhishek Arya16f059a2017-12-07 17:47:321321 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421322 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321323 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1324 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1325
1326 absolute_paths.append(absolute_path)
1327
1328 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421329
1330
Yuke Liaodd1ec0592018-02-02 01:26:371331def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1332 """Returns a target path relative to the directory of base_path.
1333
1334 This method requires base_path to be a file, otherwise, one should call
1335 os.path.relpath directly.
1336 """
1337 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141338 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371339 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141340 base_dir = os.path.dirname(base_path)
1341 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371342
1343
Abhishek Arya64636af2018-05-04 14:42:131344def _GetBinaryPathsFromTargets(targets, build_dir):
1345 """Return binary paths from target names."""
1346 # FIXME: Derive output binary from target build definitions rather than
1347 # assuming that it is always the same name.
1348 binary_paths = []
1349 for target in targets:
1350 binary_path = os.path.join(build_dir, target)
1351 if _GetHostPlatform() == 'win':
1352 binary_path += '.exe'
1353
1354 if os.path.exists(binary_path):
1355 binary_paths.append(binary_path)
1356 else:
1357 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401358 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131359 os.path.basename(binary_path))
1360
1361 return binary_paths
1362
1363
Yuke Liao506e8822017-12-04 16:52:541364def _ParseCommandArguments():
1365 """Adds and parses relevant arguments for tool comands.
1366
1367 Returns:
1368 A dictionary representing the arguments.
1369 """
1370 arg_parser = argparse.ArgumentParser()
1371 arg_parser.usage = __doc__
1372
Abhishek Arya1ec832c2017-12-05 18:06:591373 arg_parser.add_argument(
1374 '-b',
1375 '--build-dir',
1376 type=str,
1377 required=True,
1378 help='The build directory, the path needs to be relative to the root of '
1379 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541380
Abhishek Arya1ec832c2017-12-05 18:06:591381 arg_parser.add_argument(
1382 '-o',
1383 '--output-dir',
1384 type=str,
1385 required=True,
1386 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541387
Abhishek Arya1ec832c2017-12-05 18:06:591388 arg_parser.add_argument(
1389 '-c',
1390 '--command',
1391 action='append',
Abhishek Arya64636af2018-05-04 14:42:131392 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591393 help='Commands used to run test targets, one test target needs one and '
1394 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131395 'current working directory is the root of the checkout. This option is '
1396 'incompatible with -p/--profdata-file option.')
1397
1398 arg_parser.add_argument(
1399 '-p',
1400 '--profdata-file',
1401 type=str,
1402 required=False,
1403 help='Path to profdata file to use for generating code coverage reports. '
1404 'This can be useful if you generated the profdata file seperately in '
1405 'your own test harness. This option is ignored if run command(s) are '
1406 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541407
Abhishek Arya1ec832c2017-12-05 18:06:591408 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421409 '-f',
1410 '--filters',
1411 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321412 required=False,
Yuke Liao66da1732017-12-05 22:19:421413 help='Directories or files to get code coverage for, and all files under '
1414 'the directories are included recursively.')
1415
1416 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591417 '-i',
1418 '--ignore-filename-regex',
1419 type=str,
1420 help='Skip source code files with file paths that match the given '
1421 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1422 'to exclude files in third_party/ and out/ folders from the report.')
1423
1424 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591425 '-j',
1426 '--jobs',
1427 type=int,
1428 default=None,
1429 help='Run N jobs to build in parallel. If not specified, a default value '
1430 'will be derived based on CPUs availability. Please refer to '
1431 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541432
Abhishek Arya1ec832c2017-12-05 18:06:591433 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101434 '-v',
1435 '--verbose',
1436 action='store_true',
1437 help='Prints additional output for diagnostics.')
1438
1439 arg_parser.add_argument(
1440 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1441
1442 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021443 'targets',
1444 nargs='+',
1445 help='The names of the test targets to run. If multiple run commands are '
1446 'specified using the -c/--command option, then the order of targets and '
1447 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541448
1449 args = arg_parser.parse_args()
1450 return args
1451
1452
1453def Main():
1454 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131455 # Change directory to source root to aid in relative paths calculations.
1456 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111457
Abhishek Arya64636af2018-05-04 14:42:131458 # Setup coverage binaries even when script is called with empty params. This
1459 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111460 DownloadCoverageToolsIfNeeded()
1461
Yuke Liao506e8822017-12-04 16:52:541462 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131463 _ConfigureLogging(args)
1464
Yuke Liao506e8822017-12-04 16:52:541465 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481466 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541467 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481468 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541469
Abhishek Arya64636af2018-05-04 14:42:131470 assert args.command or args.profdata_file, (
1471 'Need to either provide commands to run using -c/--command option OR '
1472 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431473
Abhishek Arya64636af2018-05-04 14:42:131474 assert not args.command or (len(args.targets) == len(args.command)), (
1475 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431476
Abhishek Arya1ec832c2017-12-05 18:06:591477 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401478 'Build directory: "%s" doesn\'t exist. '
1479 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131480
Yuke Liaoc60b2d02018-03-02 21:40:431481 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541482 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321483
1484 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421485 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321486 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421487
Max Moroz7c5354f2018-05-06 00:03:481488 if not os.path.exists(_GetCoverageReportRootDirPath()):
1489 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541490
Abhishek Arya64636af2018-05-04 14:42:131491 # Get profdate file and list of binary paths.
1492 if args.command:
1493 # A list of commands are provided. Run them to generate profdata file, and
1494 # create a list of binary paths from parsing commands.
1495 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1496 profdata_file_path = _CreateCoverageProfileDataForTargets(
1497 args.targets, args.command, args.jobs)
1498 binary_paths = [_GetBinaryPath(command) for command in args.command]
1499 else:
1500 # An input prof-data file is already provided. Just calculate binary paths.
1501 profdata_file_path = args.profdata_file
1502 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331503
Yuke Liao481d3482018-01-29 19:17:101504 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401505 'depending on size of target!).')
Max Morozd73e45f2018-04-24 18:32:471506 binary_paths.extend(_GetSharedLibraries(binary_paths))
Yuke Liaodd1ec0592018-02-02 01:26:371507 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591508 binary_paths, profdata_file_path, absolute_filter_paths,
1509 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371510 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591511 absolute_filter_paths,
1512 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371513 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1514
1515 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1516 per_file_coverage_summary)
1517 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1518 per_file_coverage_summary)
1519 _GenerateDirectoryViewHtmlIndexFile()
1520
1521 component_to_directories = _ExtractComponentToDirectoriesMapping()
1522 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1523 component_to_directories, per_directory_coverage_summary)
1524 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1525 component_to_directories,
1526 per_directory_coverage_summary)
1527 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331528
1529 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371530 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331531 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481532 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331533
Max Moroz7c5354f2018-05-06 00:03:481534 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401535 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101536 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541537
Abhishek Arya1ec832c2017-12-05 18:06:591538
Yuke Liao506e8822017-12-04 16:52:541539if __name__ == '__main__':
1540 sys.exit(Main())