blob: 4f65bcd63ac82ece3f927cad7728f4ee90935e54 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Abhishek Arya1ec832c2017-12-05 18:06:5914 Example usage:
15
Abhishek Arya16f059a2017-12-07 17:47:3216 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
17 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5918 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3219 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
20 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
21 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5922
Abhishek Arya16f059a2017-12-07 17:47:3223 The command above builds crypto_unittests and url_unittests targets and then
24 runs them with specified command line arguments. For url_unittests, it only
25 runs the test URLParser.PathURL. The coverage report is filtered to include
26 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5927
Yuke Liao545db322018-02-15 17:12:0128 If you want to run tests that try to draw to the screen but don't have a
29 display connected, you can run tests in headless mode with xvfb.
30
31 Sample flow for running a test target with xvfb (e.g. unit_tests):
32
33 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
34 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
35
Abhishek Arya1ec832c2017-12-05 18:06:5936 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
37 flag as well.
38
39 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
40
Abhishek Arya16f059a2017-12-07 17:47:3241 python tools/code_coverage/coverage.py pdfium_fuzzer \\
42 -b out/coverage -o out/report \\
43 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
44 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5945
46 where:
47 <corpus_dir> - directory containing samples files for this format.
48 <runs> - number of times to fuzz target function. Should be 0 when you just
49 want to see the coverage on corpus and don't want to fuzz at all.
50
51 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3852
53 For an overview of how code coverage works in Chromium, please refer to
54 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5455"""
56
57from __future__ import print_function
58
59import sys
60
61import argparse
Yuke Liaoea228d02018-01-05 19:10:3362import json
Yuke Liao481d3482018-01-29 19:17:1063import logging
Yuke Liao506e8822017-12-04 16:52:5464import os
Yuke Liaob2926832018-03-02 17:34:2965import re
66import shlex
Max Moroz025d8952018-05-03 16:33:3467import shutil
Yuke Liao506e8822017-12-04 16:52:5468import subprocess
Yuke Liao506e8822017-12-04 16:52:5469import urllib2
70
Abhishek Arya1ec832c2017-12-05 18:06:5971sys.path.append(
72 os.path.join(
73 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
74 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5475import update as clang_update
76
Yuke Liaoea228d02018-01-05 19:10:3377sys.path.append(
78 os.path.join(
79 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
80 'third_party'))
81import jinja2
82from collections import defaultdict
83
Yuke Liao506e8822017-12-04 16:52:5484# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5985SRC_ROOT_PATH = os.path.abspath(
86 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5487
88# Absolute path to the code coverage tools binary.
89LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:1990LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
91LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
92LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5493
94# Build directory, the value is parsed from command line arguments.
95BUILD_DIR = None
96
97# Output directory for generated artifacts, the value is parsed from command
98# line arguemnts.
99OUTPUT_DIR = None
100
101# Default number of jobs used to build when goma is configured and enabled.
102DEFAULT_GOMA_JOBS = 100
103
104# Name of the file extension for profraw data files.
105PROFRAW_FILE_EXTENSION = 'profraw'
106
107# Name of the final profdata file, and this file needs to be passed to
108# "llvm-cov" command in order to call "llvm-cov show" to inspect the
109# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48110PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
111
112# Name of the file with summary information generated by llvm-cov export.
113SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54114
115# Build arg required for generating code coverage data.
116CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
117
Yuke Liaoea228d02018-01-05 19:10:33118# The default name of the html coverage report for a directory.
119DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
120
Yuke Liaodd1ec0592018-02-02 01:26:37121# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37122COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48123DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37124FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48125INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
126
127LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37128
129# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19130COMPONENT_MAPPING_URL = (
131 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37132
Yuke Liao80afff32018-03-07 01:26:20133# Caches the results returned by _GetBuildArgs, don't use this variable
134# directly, call _GetBuildArgs instead.
135_BUILD_ARGS = None
136
Abhishek Aryac19bc5ef2018-05-04 22:10:02137# Retry failed merges.
138MERGE_RETRIES = 3
139
Yuke Liaoea228d02018-01-05 19:10:33140
141class _CoverageSummary(object):
142 """Encapsulates coverage summary representation."""
143
Yuke Liaodd1ec0592018-02-02 01:26:37144 def __init__(self,
145 regions_total=0,
146 regions_covered=0,
147 functions_total=0,
148 functions_covered=0,
149 lines_total=0,
150 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33151 """Initializes _CoverageSummary object."""
152 self._summary = {
153 'regions': {
154 'total': regions_total,
155 'covered': regions_covered
156 },
157 'functions': {
158 'total': functions_total,
159 'covered': functions_covered
160 },
161 'lines': {
162 'total': lines_total,
163 'covered': lines_covered
164 }
165 }
166
167 def Get(self):
168 """Returns summary as a dictionary."""
169 return self._summary
170
171 def AddSummary(self, other_summary):
172 """Adds another summary to this one element-wise."""
173 for feature in self._summary:
174 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
175 self._summary[feature]['covered'] += other_summary.Get()[feature][
176 'covered']
177
178
Yuke Liaodd1ec0592018-02-02 01:26:37179class _CoverageReportHtmlGenerator(object):
180 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33181
Yuke Liaodd1ec0592018-02-02 01:26:37182 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33183 """
184
Yuke Liaodd1ec0592018-02-02 01:26:37185 def __init__(self, output_path, table_entry_type):
186 """Initializes _CoverageReportHtmlGenerator object.
187
188 Args:
189 output_path: Path to the html report that will be generated.
190 table_entry_type: Type of the table entries to be displayed in the table
191 header. For example: 'Path', 'Component'.
192 """
Yuke Liaoea228d02018-01-05 19:10:33193 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48194 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33195 assert os.path.exists(css_absolute_path), (
196 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40197 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33198 css_absolute_path)
199
200 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37201 self._output_path = output_path
202 self._table_entry_type = table_entry_type
203
Yuke Liaoea228d02018-01-05 19:10:33204 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12205 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23206
207 source_dir = os.path.dirname(os.path.realpath(__file__))
208 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33209
210 jinja_env = jinja2.Environment(
211 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
212 self._header_template = jinja_env.get_template('header.html')
213 self._table_template = jinja_env.get_template('table.html')
214 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23215
Abhishek Arya865fffd2018-05-08 22:16:01216 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23217 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33218
219 def AddLinkToAnotherReport(self, html_report_path, name, summary):
220 """Adds a link to another html report in this report.
221
222 The link to be added is assumed to be an entry in this directory.
223 """
Yuke Liaodd1ec0592018-02-02 01:26:37224 # Use relative paths instead of absolute paths to make the generated reports
225 # portable.
226 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
227 html_report_path, self._output_path)
228
Yuke Liaod54030e2018-01-08 17:34:12229 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37230 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12231 os.path.basename(html_report_path) ==
232 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
233 self._table_entries.append(table_entry)
234
235 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35236 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12237 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
238
239 def _CreateTableEntryFromCoverageSummary(self,
240 summary,
241 href=None,
242 name=None,
243 is_dir=None):
244 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37245 assert (href is None and name is None and is_dir is None) or (
246 href is not None and name is not None and is_dir is not None), (
247 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35248 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37249 'attributes must be None.')
250
Yuke Liaod54030e2018-01-08 17:34:12251 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37252 if href is not None:
253 entry['href'] = href
254 if name is not None:
255 entry['name'] = name
256 if is_dir is not None:
257 entry['is_dir'] = is_dir
258
Yuke Liaoea228d02018-01-05 19:10:33259 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12260 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37261 if summary_dict[feature]['total'] == 0:
262 percentage = 0.0
263 else:
Yuke Liao0e4c8682018-04-18 21:06:59264 percentage = float(summary_dict[feature]
265 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35266
Yuke Liaoea228d02018-01-05 19:10:33267 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12268 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33269 'total': summary_dict[feature]['total'],
270 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35271 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33272 'color_class': color_class
273 }
Yuke Liaod54030e2018-01-08 17:34:12274
Yuke Liaod54030e2018-01-08 17:34:12275 return entry
Yuke Liaoea228d02018-01-05 19:10:33276
277 def _GetColorClass(self, percentage):
278 """Returns the css color class based on coverage percentage."""
279 if percentage >= 0 and percentage < 80:
280 return 'red'
281 if percentage >= 80 and percentage < 100:
282 return 'yellow'
283 if percentage == 100:
284 return 'green'
285
Abhishek Aryafb70b532018-05-06 17:47:40286 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33287
Yuke Liaodd1ec0592018-02-02 01:26:37288 def WriteHtmlCoverageReport(self):
289 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33290
291 In the report, sub-directories are displayed before files and within each
292 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33293 """
294
295 def EntryCmp(left, right):
296 """Compare function for table entries."""
297 if left['is_dir'] != right['is_dir']:
298 return -1 if left['is_dir'] == True else 1
299
Yuke Liaodd1ec0592018-02-02 01:26:37300 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33301
302 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
303
304 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48305
306 directory_view_path = _GetDirectoryViewPath()
307 component_view_path = _GetComponentViewPath()
308 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37309
Yuke Liaoea228d02018-01-05 19:10:33310 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37311 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
312 directory_view_href=_GetRelativePathToDirectoryOfFile(
313 directory_view_path, self._output_path),
314 component_view_href=_GetRelativePathToDirectoryOfFile(
315 component_view_path, self._output_path),
316 file_view_href=_GetRelativePathToDirectoryOfFile(
Abhishek Arya865fffd2018-05-08 22:16:01317 file_view_path, self._output_path),
318 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37319
Yuke Liaod54030e2018-01-08 17:34:12320 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37321 entries=self._table_entries,
322 total_entry=self._total_entry,
323 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33324 html_footer = self._footer_template.render()
325
Yuke Liaodd1ec0592018-02-02 01:26:37326 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33327 html_file.write(html_header + html_table + html_footer)
328
Yuke Liao506e8822017-12-04 16:52:54329
Abhishek Arya64636af2018-05-04 14:42:13330def _ConfigureLogging(args):
331 """Configures logging settings for later use."""
332 log_level = logging.DEBUG if args.verbose else logging.INFO
333 log_format = '[%(asctime)s %(levelname)s] %(message)s'
334 log_file = args.log_file if args.log_file else None
335 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
336
337
Max Morozd73e45f2018-04-24 18:32:47338def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54339 """Returns list of shared libraries used by specified binaries."""
340 logging.info('Finding shared libraries for targets (if any).')
341 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47342 cmd = []
343 shared_library_re = None
344
345 if sys.platform.startswith('linux'):
346 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13347 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
348 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47349 elif sys.platform.startswith('darwin'):
350 cmd.extend(['otool', '-L'])
351 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
352 else:
Abhishek Aryafb70b532018-05-06 17:47:40353 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47354
355 assert shared_library_re is not None
356
357 cmd.extend(binary_paths)
358 output = subprocess.check_output(cmd)
359
360 for line in output.splitlines():
361 m = shared_library_re.match(line)
362 if not m:
363 continue
364
365 shared_library_path = m.group(1)
366 if sys.platform.startswith('darwin'):
367 # otool outputs "@rpath" macro instead of the dirname of the given binary.
368 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
369
Abhishek Arya78120bc2018-05-07 20:53:54370 if shared_library_path in shared_libraries:
371 continue
372
Max Morozd73e45f2018-04-24 18:32:47373 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
374 'the given target(s) does not '
375 'exist.' % shared_library_path)
376 with open(shared_library_path) as f:
377 data = f.read()
378
379 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
380 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54381 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47382
Abhishek Arya78120bc2018-05-07 20:53:54383 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
384 shared_libraries)
385 logging.info('Finished finding shared libraries for targets.')
386 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47387
388
Yuke Liaoc60b2d02018-03-02 21:40:43389def _GetHostPlatform():
390 """Returns the host platform.
391
392 This is separate from the target platform/os that coverage is running for.
393 """
Abhishek Arya1ec832c2017-12-05 18:06:59394 if sys.platform == 'win32' or sys.platform == 'cygwin':
395 return 'win'
396 if sys.platform.startswith('linux'):
397 return 'linux'
398 else:
399 assert sys.platform == 'darwin'
400 return 'mac'
401
402
Abhishek Arya1c97ea542018-05-10 03:53:19403def _GetPathWithLLVMSymbolizerDir():
404 """Add llvm-symbolizer directory to path for symbolized stacks."""
405 path = os.getenv('PATH')
406 dirs = path.split(os.pathsep)
407 if LLVM_BIN_DIR in dirs:
408 return path
409
410 return path + os.pathsep + LLVM_BIN_DIR
411
412
Yuke Liaoc60b2d02018-03-02 21:40:43413def _GetTargetOS():
414 """Returns the target os specified in args.gn file.
415
416 Returns an empty string is target_os is not specified.
417 """
Yuke Liao80afff32018-03-07 01:26:20418 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43419 return build_args['target_os'] if 'target_os' in build_args else ''
420
421
Yuke Liaob2926832018-03-02 17:34:29422def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10423 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43424 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10425
426
Yuke Liao506e8822017-12-04 16:52:54427# TODO(crbug.com/759794): remove this function once tools get included to
428# Clang bundle:
429# https://chromium-review.googlesource.com/c/chromium/src/+/688221
430def DownloadCoverageToolsIfNeeded():
431 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59432
Yuke Liaoc60b2d02018-03-02 21:40:43433 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54434 """Returns a pair of revision number by reading the build stamp file.
435
436 Args:
437 stamp_file_path: A path the build stamp file created by
438 tools/clang/scripts/update.py.
439 Returns:
440 A pair of integers represeting the main and sub revision respectively.
441 """
442 if not os.path.exists(stamp_file_path):
443 return 0, 0
444
445 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43446 stamp_file_line = stamp_file.readline()
447 if ',' in stamp_file_line:
448 package_version = stamp_file_line.rstrip().split(',')[0]
449 else:
450 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54451
Yuke Liaoc60b2d02018-03-02 21:40:43452 clang_revision_str, clang_sub_revision_str = package_version.split('-')
453 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59454
Yuke Liaoc60b2d02018-03-02 21:40:43455 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54456 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43457 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54458
459 coverage_revision_stamp_file = os.path.join(
460 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
461 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43462 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54463
Yuke Liaoea228d02018-01-05 19:10:33464 has_coverage_tools = (
465 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32466
Yuke Liaoea228d02018-01-05 19:10:33467 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54468 coverage_sub_revision == clang_sub_revision):
469 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43470 return
Yuke Liao506e8822017-12-04 16:52:54471
472 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
473 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
474
475 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43476 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54477 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43478 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54479 coverage_tools_url = (
480 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43481 else:
482 assert host_platform == 'win'
483 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54484
485 try:
486 clang_update.DownloadAndUnpack(coverage_tools_url,
487 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54488 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43489 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54490 file_handle.write('\n')
491 except urllib2.URLError:
492 raise Exception(
493 'Failed to download coverage tools: %s.' % coverage_tools_url)
494
495
Yuke Liaodd1ec0592018-02-02 01:26:37496def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59497 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54498 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
499
500 For a file with absolute path /a/b/x.cc, a html report is generated as:
501 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
502 OUTPUT_DIR/index.html.
503
504 Args:
505 binary_paths: A list of paths to the instrumented binaries.
506 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42507 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54508 """
Yuke Liao506e8822017-12-04 16:52:54509 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
510 # [[-object BIN]] [SOURCES]
511 # NOTE: For object files, the first one is specified as a positional argument,
512 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10513 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40514 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59515 subprocess_cmd = [
516 LLVM_COV_PATH, 'show', '-format=html',
517 '-output-dir={}'.format(OUTPUT_DIR),
518 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
519 ]
520 subprocess_cmd.extend(
521 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29522 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42523 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59524 if ignore_filename_regex:
525 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
526
Yuke Liao506e8822017-12-04 16:52:54527 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34528
529 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
530 # the platform name instead, as it simplifies the report dir structure when
531 # the same report is generated for different platforms.
532 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48533 platform_report_subdir_path = _GetCoverageReportRootDirPath()
534 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34535
Abhishek Aryafb70b532018-05-06 17:47:40536 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54537
538
Yuke Liaodd1ec0592018-02-02 01:26:37539def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
540 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48541 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37542 logging.debug('Generating file view html index file as: "%s".',
543 file_view_index_file_path)
544 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
545 'Path')
546 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33547
Yuke Liaodd1ec0592018-02-02 01:26:37548 for file_path in per_file_coverage_summary:
549 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
550
551 html_generator.AddLinkToAnotherReport(
552 _GetCoverageHtmlReportPathForFile(file_path),
553 os.path.relpath(file_path, SRC_ROOT_PATH),
554 per_file_coverage_summary[file_path])
555
556 html_generator.CreateTotalsEntry(totals_coverage_summary)
557 html_generator.WriteHtmlCoverageReport()
558 logging.debug('Finished generating file view html index file.')
559
560
561def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
562 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40563 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37564 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
565
Yuke Liaoea228d02018-01-05 19:10:33566 for file_path in per_file_coverage_summary:
567 summary = per_file_coverage_summary[file_path]
568 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40569
Yuke Liaoea228d02018-01-05 19:10:33570 while True:
571 per_directory_coverage_summary[parent_dir].AddSummary(summary)
572
573 if parent_dir == SRC_ROOT_PATH:
574 break
575 parent_dir = os.path.dirname(parent_dir)
576
Abhishek Aryafb70b532018-05-06 17:47:40577 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37578 return per_directory_coverage_summary
579
580
581def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
582 per_file_coverage_summary):
583 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40584 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33585 for dir_path in per_directory_coverage_summary:
586 _GenerateCoverageInHtmlForDirectory(
587 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
588
Abhishek Aryafb70b532018-05-06 17:47:40589 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10590
Yuke Liaoea228d02018-01-05 19:10:33591
592def _GenerateCoverageInHtmlForDirectory(
593 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
594 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37595 html_generator = _CoverageReportHtmlGenerator(
596 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33597
598 for entry_name in os.listdir(dir_path):
599 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33600
Yuke Liaodd1ec0592018-02-02 01:26:37601 if entry_path in per_file_coverage_summary:
602 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
603 entry_coverage_summary = per_file_coverage_summary[entry_path]
604 elif entry_path in per_directory_coverage_summary:
605 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
606 entry_path)
607 entry_coverage_summary = per_directory_coverage_summary[entry_path]
608 else:
Yuke Liaoc7e607142018-02-05 20:26:14609 # Any file without executable lines shouldn't be included into the report.
610 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37611 continue
Yuke Liaoea228d02018-01-05 19:10:33612
Yuke Liaodd1ec0592018-02-02 01:26:37613 html_generator.AddLinkToAnotherReport(entry_html_report_path,
614 os.path.basename(entry_path),
615 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33616
Yuke Liaod54030e2018-01-08 17:34:12617 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37618 html_generator.WriteHtmlCoverageReport()
619
620
621def _GenerateDirectoryViewHtmlIndexFile():
622 """Generates the html index file for directory view.
623
624 Note that the index file is already generated under SRC_ROOT_PATH, so this
625 file simply redirects to it, and the reason of this extra layer is for
626 structural consistency with other views.
627 """
Max Moroz7c5354f2018-05-06 00:03:48628 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37629 logging.debug('Generating directory view html index file as: "%s".',
630 directory_view_index_file_path)
631 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
632 SRC_ROOT_PATH)
633 _WriteRedirectHtmlFile(directory_view_index_file_path,
634 src_root_html_report_path)
635 logging.debug('Finished generating directory view html index file.')
636
637
638def _CalculatePerComponentCoverageSummary(component_to_directories,
639 per_directory_coverage_summary):
640 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40641 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37642 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
643
644 for component in component_to_directories:
645 for directory in component_to_directories[component]:
646 absolute_directory_path = os.path.abspath(directory)
647 if absolute_directory_path in per_directory_coverage_summary:
648 per_component_coverage_summary[component].AddSummary(
649 per_directory_coverage_summary[absolute_directory_path])
650
Abhishek Aryafb70b532018-05-06 17:47:40651 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37652 return per_component_coverage_summary
653
654
655def _ExtractComponentToDirectoriesMapping():
656 """Returns a mapping from components to directories."""
657 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
658 directory_to_component = component_mappings['dir-to-component']
659
660 component_to_directories = defaultdict(list)
661 for directory in directory_to_component:
662 component = directory_to_component[directory]
663 component_to_directories[component].append(directory)
664
665 return component_to_directories
666
667
668def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
669 component_to_directories,
670 per_directory_coverage_summary):
671 """Generates per-component coverage reports in html."""
672 logging.debug('Writing per-component coverage html reports.')
673 for component in per_component_coverage_summary:
674 _GenerateCoverageInHtmlForComponent(
675 component, per_component_coverage_summary, component_to_directories,
676 per_directory_coverage_summary)
677
678 logging.debug('Finished writing per-component coverage html reports.')
679
680
681def _GenerateCoverageInHtmlForComponent(
682 component_name, per_component_coverage_summary, component_to_directories,
683 per_directory_coverage_summary):
684 """Generates coverage html report for a component."""
685 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
686 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14687 component_html_report_dir = os.path.dirname(component_html_report_path)
688 if not os.path.exists(component_html_report_dir):
689 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37690
691 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
692 'Path')
693
694 for dir_path in component_to_directories[component_name]:
695 dir_absolute_path = os.path.abspath(dir_path)
696 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14697 # Any directory without an excercised file shouldn't be included into the
698 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37699 continue
700
701 html_generator.AddLinkToAnotherReport(
702 _GetCoverageHtmlReportPathForDirectory(dir_path),
703 os.path.relpath(dir_path, SRC_ROOT_PATH),
704 per_directory_coverage_summary[dir_absolute_path])
705
706 html_generator.CreateTotalsEntry(
707 per_component_coverage_summary[component_name])
708 html_generator.WriteHtmlCoverageReport()
709
710
711def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
712 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48713 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37714 logging.debug('Generating component view html index file as: "%s".',
715 component_view_index_file_path)
716 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
717 'Component')
718 totals_coverage_summary = _CoverageSummary()
719
720 for component in per_component_coverage_summary:
721 totals_coverage_summary.AddSummary(
722 per_component_coverage_summary[component])
723
724 html_generator.AddLinkToAnotherReport(
725 _GetCoverageHtmlReportPathForComponent(component), component,
726 per_component_coverage_summary[component])
727
728 html_generator.CreateTotalsEntry(totals_coverage_summary)
729 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14730 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33731
732
Max Moroz7c5354f2018-05-06 00:03:48733def _MergeTwoDirectories(src_path, dst_path):
734 """Merge src_path directory into dst_path directory."""
735 for filename in os.listdir(src_path):
736 dst_path = os.path.join(dst_path, filename)
737 if os.path.exists(dst_path):
738 shutil.rmtree(dst_path)
739 os.rename(os.path.join(src_path, filename), dst_path)
740 shutil.rmtree(src_path)
741
742
Yuke Liaoea228d02018-01-05 19:10:33743def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37744 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48745 html_index_file_path = _GetHtmlIndexPath()
746 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37747 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
748
749
750def _WriteRedirectHtmlFile(from_html_path, to_html_path):
751 """Writes a html file that redirects to another html file."""
752 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
753 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33754 content = ("""
755 <!DOCTYPE html>
756 <html>
757 <head>
758 <!-- HTML meta refresh URL redirection -->
759 <meta http-equiv="refresh" content="0; url=%s">
760 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37761 </html>""" % to_html_relative_path)
762 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33763 f.write(content)
764
765
Max Moroz7c5354f2018-05-06 00:03:48766def _CleanUpOutputDir():
767 """Perform a cleanup of the output dir."""
768 # Remove the default index.html file produced by llvm-cov.
769 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
770 if os.path.exists(index_path):
771 os.remove(index_path)
772
773
Yuke Liaodd1ec0592018-02-02 01:26:37774def _GetCoverageHtmlReportPathForFile(file_path):
775 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40776 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37777 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
778
779 # '+' is used instead of os.path.join because both of them are absolute paths
780 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14781 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37782 return _GetCoverageReportRootDirPath() + html_report_path
783
784
785def _GetCoverageHtmlReportPathForDirectory(dir_path):
786 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40787 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37788 html_report_path = os.path.join(
789 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
790
791 # '+' is used instead of os.path.join because both of them are absolute paths
792 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14793 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37794 return _GetCoverageReportRootDirPath() + html_report_path
795
796
797def _GetCoverageHtmlReportPathForComponent(component_name):
798 """Given a component, returns the corresponding html report path."""
799 component_file_name = component_name.lower().replace('>', '-')
800 html_report_name = os.extsep.join([component_file_name, 'html'])
801 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
802 html_report_name)
803
804
805def _GetCoverageReportRootDirPath():
806 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48807 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
808
809
810def _GetComponentViewPath():
811 """Path to the HTML file for the component view."""
812 return os.path.join(_GetCoverageReportRootDirPath(),
813 COMPONENT_VIEW_INDEX_FILE)
814
815
816def _GetDirectoryViewPath():
817 """Path to the HTML file for the directory view."""
818 return os.path.join(_GetCoverageReportRootDirPath(),
819 DIRECTORY_VIEW_INDEX_FILE)
820
821
822def _GetFileViewPath():
823 """Path to the HTML file for the file view."""
824 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
825
826
827def _GetLogsDirectoryPath():
828 """Path to the logs directory."""
829 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
830
831
832def _GetHtmlIndexPath():
833 """Path to the main HTML index file."""
834 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
835
836
837def _GetProfdataFilePath():
838 """Path to the resulting .profdata file."""
839 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
840
841
842def _GetSummaryFilePath():
843 """The JSON file that contains coverage summary written by llvm-cov export."""
844 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33845
846
Yuke Liao506e8822017-12-04 16:52:54847def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
848 """Builds and runs target to generate the coverage profile data.
849
850 Args:
851 targets: A list of targets to build with coverage instrumentation.
852 commands: A list of commands used to run the targets.
853 jobs_count: Number of jobs to run in parallel for building. If None, a
854 default value is derived based on CPUs availability.
855
856 Returns:
857 A relative path to the generated profdata file.
858 """
859 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02860 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59861 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02862 coverage_profdata_file_path = (
863 _CreateCoverageProfileDataFromTargetProfDataFiles(
864 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54865
Abhishek Aryac19bc5ef2018-05-04 22:10:02866 for target_profdata_file_path in target_profdata_file_paths:
867 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52868
Abhishek Aryac19bc5ef2018-05-04 22:10:02869 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54870
871
872def _BuildTargets(targets, jobs_count):
873 """Builds target with Clang coverage instrumentation.
874
875 This function requires current working directory to be the root of checkout.
876
877 Args:
878 targets: A list of targets to build with coverage instrumentation.
879 jobs_count: Number of jobs to run in parallel for compilation. If None, a
880 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54881 """
Abhishek Arya1ec832c2017-12-05 18:06:59882
Yuke Liao506e8822017-12-04 16:52:54883 def _IsGomaConfigured():
884 """Returns True if goma is enabled in the gn build args.
885
886 Returns:
887 A boolean indicates whether goma is configured for building or not.
888 """
Yuke Liao80afff32018-03-07 01:26:20889 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54890 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
891
Abhishek Aryafb70b532018-05-06 17:47:40892 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54893 if jobs_count is None and _IsGomaConfigured():
894 jobs_count = DEFAULT_GOMA_JOBS
895
896 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
897 if jobs_count is not None:
898 subprocess_cmd.append('-j' + str(jobs_count))
899
900 subprocess_cmd.extend(targets)
901 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40902 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54903
904
Abhishek Aryac19bc5ef2018-05-04 22:10:02905def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54906 """Runs commands and returns the relative paths to the profraw data files.
907
908 Args:
909 targets: A list of targets built with coverage instrumentation.
910 commands: A list of commands used to run the targets.
911
912 Returns:
913 A list of relative paths to the generated profraw data files.
914 """
Abhishek Aryafb70b532018-05-06 17:47:40915 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10916
Yuke Liao506e8822017-12-04 16:52:54917 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48918 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54919 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48920 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
921
922 # Ensure that logs directory exists.
923 if not os.path.exists(_GetLogsDirectoryPath()):
924 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54925
Abhishek Aryac19bc5ef2018-05-04 22:10:02926 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10927
Yuke Liaod4a9865202018-01-12 23:17:52928 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54929 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48930 output_file_name = os.extsep.join([target + '_output', 'log'])
931 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10932
Abhishek Aryac19bc5ef2018-05-04 22:10:02933 profdata_file_path = None
934 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40935 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02936 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10937
Abhishek Aryac19bc5ef2018-05-04 22:10:02938 if _IsIOSCommand(command):
939 # On iOS platform, due to lack of write permissions, profraw files are
940 # generated outside of the OUTPUT_DIR, and the exact paths are contained
941 # in the output of the command execution.
942 output = _ExecuteIOSCommand(target, command)
943 else:
944 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
945 output = _ExecuteCommand(target, command)
946
947 with open(output_file_path, 'w') as output_file:
948 output_file.write(output)
949
950 profraw_file_paths = []
951 if _IsIOS():
952 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
953 else:
Max Moroz7c5354f2018-05-06 00:03:48954 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02955 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48956 profraw_file_paths.append(
957 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02958
959 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40960 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02961 'please make sure the binary exists and is properly '
962 'instrumented.' % target)
963
964 try:
965 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
966 target, profraw_file_paths)
967 break
968 except Exception:
969 print('Retrying...')
970 finally:
971 # Remove profraw files now so that they are not used in next iteration.
972 for profraw_file_path in profraw_file_paths:
973 os.remove(profraw_file_path)
974
975 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40976 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02977 'Please file a bug with command you used, commit position and args.gn '
978 'config here: '
979 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40980 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02981 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54982
Abhishek Aryafb70b532018-05-06 17:47:40983 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10984
Abhishek Aryac19bc5ef2018-05-04 22:10:02985 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54986
987
988def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10989 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52990 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01991 #
Max Morozd73e45f2018-04-24 18:32:47992 # "%p" expands out to the process ID. It's not used by this scripts due to:
993 # 1) If a target program spawns too many processess, it may exhaust all disk
994 # space available. For example, unit_tests writes thousands of .profraw
995 # files each of size 1GB+.
996 # 2) If a target binary uses shared libraries, coverage profile data for them
997 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01998 #
Yuke Liaod4a9865202018-01-12 23:17:52999 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1000 # is specified, the runtime creates a pool of N raw profiles which are used
1001 # for on-line profile merging. The runtime takes care of selecting a raw
1002 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521003 # N must be between 1 and 9. The merge pool specifier can only occur once per
1004 # filename pattern.
1005 #
Max Morozd73e45f2018-04-24 18:32:471006 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011007 #
Max Morozd73e45f2018-04-24 18:32:471008 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1009 # but it's not too big to consume too much computing resource or disk space.
1010 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591011 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011012 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481013 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541014 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541015
Yuke Liaoa0c8c2f2018-02-28 20:14:101016 try:
Max Moroz7c5354f2018-05-06 00:03:481017 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101018 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291019 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481020 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191021 env={
1022 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1023 'PATH': _GetPathWithLLVMSymbolizerDir()
1024 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101025 except subprocess.CalledProcessError as e:
1026 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191027 logging.warning(
1028 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1029 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101030
1031 return output
1032
1033
Yuke Liao27349c92018-03-22 21:10:011034def _IsFuzzerTarget(target):
1035 """Returns true if the target is a fuzzer target."""
1036 build_args = _GetBuildArgs()
1037 use_libfuzzer = ('use_libfuzzer' in build_args and
1038 build_args['use_libfuzzer'] == 'true')
1039 return use_libfuzzer and target.endswith('_fuzzer')
1040
1041
Yuke Liaob2926832018-03-02 17:34:291042def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101043 """Runs a single iOS command and generates a profraw data file.
1044
1045 iOS application doesn't have write access to folders outside of the app, so
1046 it's impossible to instruct the app to flush the profraw data file to the
1047 desired location. The profraw data file will be generated somewhere within the
1048 application's Documents folder, and the full path can be obtained by parsing
1049 the output.
1050 """
Yuke Liaob2926832018-03-02 17:34:291051 assert _IsIOSCommand(command)
1052
1053 # After running tests, iossim generates a profraw data file, it won't be
1054 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1055 # checkout.
1056 iossim_profraw_file_path = os.path.join(
1057 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101058
1059 try:
Yuke Liaob2926832018-03-02 17:34:291060 output = subprocess.check_output(
1061 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191062 env={
1063 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1064 'PATH': _GetPathWithLLVMSymbolizerDir()
1065 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101066 except subprocess.CalledProcessError as e:
1067 # iossim emits non-zero return code even if tests run successfully, so
1068 # ignore the return code.
1069 output = e.output
1070
1071 return output
1072
1073
1074def _GetProfrawDataFileByParsingOutput(output):
1075 """Returns the path to the profraw data file obtained by parsing the output.
1076
1077 The output of running the test target has no format, but it is guaranteed to
1078 have a single line containing the path to the generated profraw data file.
1079 NOTE: This should only be called when target os is iOS.
1080 """
Yuke Liaob2926832018-03-02 17:34:291081 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101082
Yuke Liaob2926832018-03-02 17:34:291083 output_by_lines = ''.join(output).splitlines()
1084 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101085
1086 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291087 result = profraw_file_pattern.match(line)
1088 if result:
1089 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101090
1091 assert False, ('No profraw data file was generated, did you call '
1092 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1093 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541094
1095
Abhishek Aryac19bc5ef2018-05-04 22:10:021096def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1097 """Returns a relative path to coverage profdata file by merging target
1098 profdata files.
Yuke Liao506e8822017-12-04 16:52:541099
1100 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021101 profdata_file_paths: A list of relative paths to the profdata data files
1102 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541103
1104 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021105 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541106
1107 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021108 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541109 """
Abhishek Aryafb70b532018-05-06 17:47:401110 logging.info('Creating the coverage profile data file.')
1111 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481112 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541113 try:
Abhishek Arya1ec832c2017-12-05 18:06:591114 subprocess_cmd = [
1115 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1116 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021117 subprocess_cmd.extend(profdata_file_paths)
1118 subprocess.check_call(subprocess_cmd)
1119 except subprocess.CalledProcessError as error:
1120 print('Failed to merge target profdata files to create coverage profdata. '
1121 'Try again.')
1122 raise error
1123
Abhishek Aryafb70b532018-05-06 17:47:401124 logging.debug('Finished merging target profdata files.')
1125 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021126 profdata_file_path)
1127 return profdata_file_path
1128
1129
1130def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1131 """Returns a relative path to target profdata file by merging target
1132 profraw files.
1133
1134 Args:
1135 profraw_file_paths: A list of relative paths to the profdata data files
1136 that are to be merged.
1137
1138 Returns:
1139 A relative path to the merged coverage profdata file.
1140
1141 Raises:
1142 CalledProcessError: An error occurred merging profdata files.
1143 """
Abhishek Aryafb70b532018-05-06 17:47:401144 logging.info('Creating target profile data file.')
1145 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021146 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1147
1148 try:
1149 subprocess_cmd = [
1150 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1151 ]
Yuke Liao506e8822017-12-04 16:52:541152 subprocess_cmd.extend(profraw_file_paths)
1153 subprocess.check_call(subprocess_cmd)
1154 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021155 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541156 raise error
1157
Abhishek Aryafb70b532018-05-06 17:47:401158 logging.debug('Finished merging target profraw files.')
1159 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101160 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541161 return profdata_file_path
1162
1163
Yuke Liao0e4c8682018-04-18 21:06:591164def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1165 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331166 """Generates per file coverage summary using "llvm-cov export" command."""
1167 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1168 # [[-object BIN]] [SOURCES].
1169 # NOTE: For object files, the first one is specified as a positional argument,
1170 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101171 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401172 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331173 subprocess_cmd = [
1174 LLVM_COV_PATH, 'export', '-summary-only',
1175 '-instr-profile=' + profdata_file_path, binary_paths[0]
1176 ]
1177 subprocess_cmd.extend(
1178 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291179 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331180 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591181 if ignore_filename_regex:
1182 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331183
Max Moroz7c5354f2018-05-06 00:03:481184 export_output = subprocess.check_output(subprocess_cmd)
1185
1186 # Write output on the disk to be used by code coverage bot.
1187 with open(_GetSummaryFilePath(), 'w') as f:
1188 f.write(export_output)
1189
1190 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331191 assert len(json_output['data']) == 1
1192 files_coverage_data = json_output['data'][0]['files']
1193
1194 per_file_coverage_summary = {}
1195 for file_coverage_data in files_coverage_data:
1196 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401197 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1198 'File path "%s" in coverage summary is outside source checkout.' %
1199 file_path)
Yuke Liaoea228d02018-01-05 19:10:331200
Abhishek Aryafb70b532018-05-06 17:47:401201 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331202 if summary['lines']['count'] == 0:
1203 continue
1204
1205 per_file_coverage_summary[file_path] = _CoverageSummary(
1206 regions_total=summary['regions']['count'],
1207 regions_covered=summary['regions']['covered'],
1208 functions_total=summary['functions']['count'],
1209 functions_covered=summary['functions']['covered'],
1210 lines_total=summary['lines']['count'],
1211 lines_covered=summary['lines']['covered'])
1212
Abhishek Aryafb70b532018-05-06 17:47:401213 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331214 return per_file_coverage_summary
1215
1216
Yuke Liaob2926832018-03-02 17:34:291217def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1218 """Appends -arch arguments to the command list if it's ios platform.
1219
1220 iOS binaries are universal binaries, and require specifying the architecture
1221 to use, and one architecture needs to be specified for each binary.
1222 """
1223 if _IsIOS():
1224 cmd_list.extend(['-arch=x86_64'] * num_archs)
1225
1226
Yuke Liao506e8822017-12-04 16:52:541227def _GetBinaryPath(command):
1228 """Returns a relative path to the binary to be run by the command.
1229
Yuke Liao545db322018-02-15 17:12:011230 Currently, following types of commands are supported (e.g. url_unittests):
1231 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1232 2. Use xvfb.
1233 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1234 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371235 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1236 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101237 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371238 <iossim_arguments> -c <app_arguments>
1239 out/Coverage-iphonesimulator/url_unittests.app"
1240
Yuke Liao545db322018-02-15 17:12:011241
Yuke Liao506e8822017-12-04 16:52:541242 Args:
1243 command: A command used to run a target.
1244
1245 Returns:
1246 A relative path to the binary.
1247 """
Yuke Liao545db322018-02-15 17:12:011248 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1249
Yuke Liaob2926832018-03-02 17:34:291250 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011251 if os.path.basename(command_parts[0]) == 'python':
1252 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401253 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011254 return command_parts[2]
1255
1256 if os.path.basename(command_parts[0]) == xvfb_script_name:
1257 return command_parts[1]
1258
Yuke Liaob2926832018-03-02 17:34:291259 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101260 # For a given application bundle, the binary resides in the bundle and has
1261 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371262 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101263 app_name = os.path.splitext(os.path.basename(app_path))[0]
1264 return os.path.join(app_path, app_name)
1265
Yuke Liaob2926832018-03-02 17:34:291266 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541267
1268
Yuke Liaob2926832018-03-02 17:34:291269def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101270 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291271 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101272
1273
Yuke Liao95d13d72017-12-07 18:18:501274def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1275 """Verifies that the target executables specified in the commands are inside
1276 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541277 for command in commands:
1278 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501279 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481280 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501281 'Target executable "%s" in command: "%s" is outside of '
1282 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541283
1284
1285def _ValidateBuildingWithClangCoverage():
1286 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201287 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541288
1289 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1290 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591291 assert False, ('\'{} = true\' is required in args.gn.'
1292 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541293
1294
Yuke Liaoc60b2d02018-03-02 21:40:431295def _ValidateCurrentPlatformIsSupported():
1296 """Asserts that this script suports running on the current platform"""
1297 target_os = _GetTargetOS()
1298 if target_os:
1299 current_platform = target_os
1300 else:
1301 current_platform = _GetHostPlatform()
1302
1303 assert current_platform in [
1304 'linux', 'mac', 'chromeos', 'ios'
1305 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1306
1307
Yuke Liao80afff32018-03-07 01:26:201308def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541309 """Parses args.gn file and returns results as a dictionary.
1310
1311 Returns:
1312 A dictionary representing the build args.
1313 """
Yuke Liao80afff32018-03-07 01:26:201314 global _BUILD_ARGS
1315 if _BUILD_ARGS is not None:
1316 return _BUILD_ARGS
1317
1318 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541319 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1320 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1321 'missing args.gn file.' % BUILD_DIR)
1322 with open(build_args_path) as build_args_file:
1323 build_args_lines = build_args_file.readlines()
1324
Yuke Liao506e8822017-12-04 16:52:541325 for build_arg_line in build_args_lines:
1326 build_arg_without_comments = build_arg_line.split('#')[0]
1327 key_value_pair = build_arg_without_comments.split('=')
1328 if len(key_value_pair) != 2:
1329 continue
1330
1331 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431332
1333 # Values are wrapped within a pair of double-quotes, so remove the leading
1334 # and trailing double-quotes.
1335 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201336 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541337
Yuke Liao80afff32018-03-07 01:26:201338 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541339
1340
Abhishek Arya16f059a2017-12-07 17:47:321341def _VerifyPathsAndReturnAbsolutes(paths):
1342 """Verifies that the paths specified in |paths| exist and returns absolute
1343 versions.
Yuke Liao66da1732017-12-05 22:19:421344
1345 Args:
1346 paths: A list of files or directories.
1347 """
Abhishek Arya16f059a2017-12-07 17:47:321348 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421349 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321350 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1351 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1352
1353 absolute_paths.append(absolute_path)
1354
1355 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421356
1357
Yuke Liaodd1ec0592018-02-02 01:26:371358def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1359 """Returns a target path relative to the directory of base_path.
1360
1361 This method requires base_path to be a file, otherwise, one should call
1362 os.path.relpath directly.
1363 """
1364 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141365 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371366 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141367 base_dir = os.path.dirname(base_path)
1368 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371369
1370
Abhishek Arya64636af2018-05-04 14:42:131371def _GetBinaryPathsFromTargets(targets, build_dir):
1372 """Return binary paths from target names."""
1373 # FIXME: Derive output binary from target build definitions rather than
1374 # assuming that it is always the same name.
1375 binary_paths = []
1376 for target in targets:
1377 binary_path = os.path.join(build_dir, target)
1378 if _GetHostPlatform() == 'win':
1379 binary_path += '.exe'
1380
1381 if os.path.exists(binary_path):
1382 binary_paths.append(binary_path)
1383 else:
1384 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401385 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131386 os.path.basename(binary_path))
1387
1388 return binary_paths
1389
1390
Yuke Liao506e8822017-12-04 16:52:541391def _ParseCommandArguments():
1392 """Adds and parses relevant arguments for tool comands.
1393
1394 Returns:
1395 A dictionary representing the arguments.
1396 """
1397 arg_parser = argparse.ArgumentParser()
1398 arg_parser.usage = __doc__
1399
Abhishek Arya1ec832c2017-12-05 18:06:591400 arg_parser.add_argument(
1401 '-b',
1402 '--build-dir',
1403 type=str,
1404 required=True,
1405 help='The build directory, the path needs to be relative to the root of '
1406 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541407
Abhishek Arya1ec832c2017-12-05 18:06:591408 arg_parser.add_argument(
1409 '-o',
1410 '--output-dir',
1411 type=str,
1412 required=True,
1413 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541414
Abhishek Arya1ec832c2017-12-05 18:06:591415 arg_parser.add_argument(
1416 '-c',
1417 '--command',
1418 action='append',
Abhishek Arya64636af2018-05-04 14:42:131419 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591420 help='Commands used to run test targets, one test target needs one and '
1421 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131422 'current working directory is the root of the checkout. This option is '
1423 'incompatible with -p/--profdata-file option.')
1424
1425 arg_parser.add_argument(
1426 '-p',
1427 '--profdata-file',
1428 type=str,
1429 required=False,
1430 help='Path to profdata file to use for generating code coverage reports. '
1431 'This can be useful if you generated the profdata file seperately in '
1432 'your own test harness. This option is ignored if run command(s) are '
1433 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541434
Abhishek Arya1ec832c2017-12-05 18:06:591435 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421436 '-f',
1437 '--filters',
1438 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321439 required=False,
Yuke Liao66da1732017-12-05 22:19:421440 help='Directories or files to get code coverage for, and all files under '
1441 'the directories are included recursively.')
1442
1443 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591444 '-i',
1445 '--ignore-filename-regex',
1446 type=str,
1447 help='Skip source code files with file paths that match the given '
1448 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1449 'to exclude files in third_party/ and out/ folders from the report.')
1450
1451 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591452 '-j',
1453 '--jobs',
1454 type=int,
1455 default=None,
1456 help='Run N jobs to build in parallel. If not specified, a default value '
1457 'will be derived based on CPUs availability. Please refer to '
1458 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541459
Abhishek Arya1ec832c2017-12-05 18:06:591460 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101461 '-v',
1462 '--verbose',
1463 action='store_true',
1464 help='Prints additional output for diagnostics.')
1465
1466 arg_parser.add_argument(
1467 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1468
1469 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021470 'targets',
1471 nargs='+',
1472 help='The names of the test targets to run. If multiple run commands are '
1473 'specified using the -c/--command option, then the order of targets and '
1474 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541475
1476 args = arg_parser.parse_args()
1477 return args
1478
1479
1480def Main():
1481 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131482 # Change directory to source root to aid in relative paths calculations.
1483 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111484
Abhishek Arya64636af2018-05-04 14:42:131485 # Setup coverage binaries even when script is called with empty params. This
1486 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111487 DownloadCoverageToolsIfNeeded()
1488
Yuke Liao506e8822017-12-04 16:52:541489 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131490 _ConfigureLogging(args)
1491
Yuke Liao506e8822017-12-04 16:52:541492 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481493 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541494 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481495 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541496
Abhishek Arya64636af2018-05-04 14:42:131497 assert args.command or args.profdata_file, (
1498 'Need to either provide commands to run using -c/--command option OR '
1499 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431500
Abhishek Arya64636af2018-05-04 14:42:131501 assert not args.command or (len(args.targets) == len(args.command)), (
1502 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431503
Abhishek Arya1ec832c2017-12-05 18:06:591504 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401505 'Build directory: "%s" doesn\'t exist. '
1506 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131507
Yuke Liaoc60b2d02018-03-02 21:40:431508 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541509 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321510
1511 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421512 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321513 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421514
Max Moroz7c5354f2018-05-06 00:03:481515 if not os.path.exists(_GetCoverageReportRootDirPath()):
1516 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541517
Abhishek Arya64636af2018-05-04 14:42:131518 # Get profdate file and list of binary paths.
1519 if args.command:
1520 # A list of commands are provided. Run them to generate profdata file, and
1521 # create a list of binary paths from parsing commands.
1522 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1523 profdata_file_path = _CreateCoverageProfileDataForTargets(
1524 args.targets, args.command, args.jobs)
1525 binary_paths = [_GetBinaryPath(command) for command in args.command]
1526 else:
1527 # An input prof-data file is already provided. Just calculate binary paths.
1528 profdata_file_path = args.profdata_file
1529 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331530
Abhishek Arya78120bc2018-05-07 20:53:541531 binary_paths.extend(_GetSharedLibraries(binary_paths))
1532
Yuke Liao481d3482018-01-29 19:17:101533 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401534 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371535 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591536 binary_paths, profdata_file_path, absolute_filter_paths,
1537 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371538 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591539 absolute_filter_paths,
1540 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371541 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1542
1543 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1544 per_file_coverage_summary)
1545 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1546 per_file_coverage_summary)
1547 _GenerateDirectoryViewHtmlIndexFile()
1548
1549 component_to_directories = _ExtractComponentToDirectoriesMapping()
1550 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1551 component_to_directories, per_directory_coverage_summary)
1552 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1553 component_to_directories,
1554 per_directory_coverage_summary)
1555 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331556
1557 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371558 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331559 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481560 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331561
Max Moroz7c5354f2018-05-06 00:03:481562 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401563 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101564 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541565
Abhishek Arya1ec832c2017-12-05 18:06:591566
Yuke Liao506e8822017-12-04 16:52:541567if __name__ == '__main__':
1568 sys.exit(Main())