blob: 71957a127b55b22b5b48b2889106d83bbb892a90 [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
Abhishek Arya1c97ea542018-05-10 03:53:1994LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
95LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
96LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5497
98# Build directory, the value is parsed from command line arguments.
99BUILD_DIR = None
100
101# Output directory for generated artifacts, the value is parsed from command
102# line arguemnts.
103OUTPUT_DIR = None
104
105# Default number of jobs used to build when goma is configured and enabled.
106DEFAULT_GOMA_JOBS = 100
107
108# Name of the file extension for profraw data files.
109PROFRAW_FILE_EXTENSION = 'profraw'
110
111# Name of the final profdata file, and this file needs to be passed to
112# "llvm-cov" command in order to call "llvm-cov show" to inspect the
113# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48114PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
115
116# Name of the file with summary information generated by llvm-cov export.
117SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54118
119# Build arg required for generating code coverage data.
120CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
121
Yuke Liaoea228d02018-01-05 19:10:33122# The default name of the html coverage report for a directory.
123DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
124
Yuke Liaodd1ec0592018-02-02 01:26:37125# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37126COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48127DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37128FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48129INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
130
131LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37132
133# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19134COMPONENT_MAPPING_URL = (
135 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37136
Yuke Liao80afff32018-03-07 01:26:20137# Caches the results returned by _GetBuildArgs, don't use this variable
138# directly, call _GetBuildArgs instead.
139_BUILD_ARGS = None
140
Abhishek Aryac19bc5ef2018-05-04 22:10:02141# Retry failed merges.
142MERGE_RETRIES = 3
143
Yuke Liaoea228d02018-01-05 19:10:33144
145class _CoverageSummary(object):
146 """Encapsulates coverage summary representation."""
147
Yuke Liaodd1ec0592018-02-02 01:26:37148 def __init__(self,
149 regions_total=0,
150 regions_covered=0,
151 functions_total=0,
152 functions_covered=0,
153 lines_total=0,
154 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33155 """Initializes _CoverageSummary object."""
156 self._summary = {
157 'regions': {
158 'total': regions_total,
159 'covered': regions_covered
160 },
161 'functions': {
162 'total': functions_total,
163 'covered': functions_covered
164 },
165 'lines': {
166 'total': lines_total,
167 'covered': lines_covered
168 }
169 }
170
171 def Get(self):
172 """Returns summary as a dictionary."""
173 return self._summary
174
175 def AddSummary(self, other_summary):
176 """Adds another summary to this one element-wise."""
177 for feature in self._summary:
178 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
179 self._summary[feature]['covered'] += other_summary.Get()[feature][
180 'covered']
181
182
Yuke Liaodd1ec0592018-02-02 01:26:37183class _CoverageReportHtmlGenerator(object):
184 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33185
Yuke Liaodd1ec0592018-02-02 01:26:37186 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33187 """
188
Yuke Liaodd1ec0592018-02-02 01:26:37189 def __init__(self, output_path, table_entry_type):
190 """Initializes _CoverageReportHtmlGenerator object.
191
192 Args:
193 output_path: Path to the html report that will be generated.
194 table_entry_type: Type of the table entries to be displayed in the table
195 header. For example: 'Path', 'Component'.
196 """
Yuke Liaoea228d02018-01-05 19:10:33197 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48198 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33199 assert os.path.exists(css_absolute_path), (
200 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40201 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33202 css_absolute_path)
203
204 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37205 self._output_path = output_path
206 self._table_entry_type = table_entry_type
207
Yuke Liaoea228d02018-01-05 19:10:33208 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12209 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33210 template_dir = os.path.join(
211 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
212
213 jinja_env = jinja2.Environment(
214 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
215 self._header_template = jinja_env.get_template('header.html')
216 self._table_template = jinja_env.get_template('table.html')
217 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya865fffd2018-05-08 22:16:01218 self._style_overrides = open(
219 os.path.join(template_dir, 'style_overrides.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33220
221 def AddLinkToAnotherReport(self, html_report_path, name, summary):
222 """Adds a link to another html report in this report.
223
224 The link to be added is assumed to be an entry in this directory.
225 """
Yuke Liaodd1ec0592018-02-02 01:26:37226 # Use relative paths instead of absolute paths to make the generated reports
227 # portable.
228 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
229 html_report_path, self._output_path)
230
Yuke Liaod54030e2018-01-08 17:34:12231 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37232 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12233 os.path.basename(html_report_path) ==
234 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
235 self._table_entries.append(table_entry)
236
237 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35238 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12239 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
240
241 def _CreateTableEntryFromCoverageSummary(self,
242 summary,
243 href=None,
244 name=None,
245 is_dir=None):
246 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37247 assert (href is None and name is None and is_dir is None) or (
248 href is not None and name is not None and is_dir is not None), (
249 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35250 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37251 'attributes must be None.')
252
Yuke Liaod54030e2018-01-08 17:34:12253 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37254 if href is not None:
255 entry['href'] = href
256 if name is not None:
257 entry['name'] = name
258 if is_dir is not None:
259 entry['is_dir'] = is_dir
260
Yuke Liaoea228d02018-01-05 19:10:33261 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12262 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37263 if summary_dict[feature]['total'] == 0:
264 percentage = 0.0
265 else:
Yuke Liao0e4c8682018-04-18 21:06:59266 percentage = float(summary_dict[feature]
267 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35268
Yuke Liaoea228d02018-01-05 19:10:33269 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12270 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33271 'total': summary_dict[feature]['total'],
272 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35273 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33274 'color_class': color_class
275 }
Yuke Liaod54030e2018-01-08 17:34:12276
Yuke Liaod54030e2018-01-08 17:34:12277 return entry
Yuke Liaoea228d02018-01-05 19:10:33278
279 def _GetColorClass(self, percentage):
280 """Returns the css color class based on coverage percentage."""
281 if percentage >= 0 and percentage < 80:
282 return 'red'
283 if percentage >= 80 and percentage < 100:
284 return 'yellow'
285 if percentage == 100:
286 return 'green'
287
Abhishek Aryafb70b532018-05-06 17:47:40288 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33289
Yuke Liaodd1ec0592018-02-02 01:26:37290 def WriteHtmlCoverageReport(self):
291 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33292
293 In the report, sub-directories are displayed before files and within each
294 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33295 """
296
297 def EntryCmp(left, right):
298 """Compare function for table entries."""
299 if left['is_dir'] != right['is_dir']:
300 return -1 if left['is_dir'] == True else 1
301
Yuke Liaodd1ec0592018-02-02 01:26:37302 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33303
304 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
305
306 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48307
308 directory_view_path = _GetDirectoryViewPath()
309 component_view_path = _GetComponentViewPath()
310 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37311
Yuke Liaoea228d02018-01-05 19:10:33312 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37313 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
314 directory_view_href=_GetRelativePathToDirectoryOfFile(
315 directory_view_path, self._output_path),
316 component_view_href=_GetRelativePathToDirectoryOfFile(
317 component_view_path, self._output_path),
318 file_view_href=_GetRelativePathToDirectoryOfFile(
Abhishek Arya865fffd2018-05-08 22:16:01319 file_view_path, self._output_path),
320 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37321
Yuke Liaod54030e2018-01-08 17:34:12322 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37323 entries=self._table_entries,
324 total_entry=self._total_entry,
325 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33326 html_footer = self._footer_template.render()
327
Yuke Liaodd1ec0592018-02-02 01:26:37328 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33329 html_file.write(html_header + html_table + html_footer)
330
Yuke Liao506e8822017-12-04 16:52:54331
Abhishek Arya64636af2018-05-04 14:42:13332def _ConfigureLogging(args):
333 """Configures logging settings for later use."""
334 log_level = logging.DEBUG if args.verbose else logging.INFO
335 log_format = '[%(asctime)s %(levelname)s] %(message)s'
336 log_file = args.log_file if args.log_file else None
337 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
338
339
Max Morozd73e45f2018-04-24 18:32:47340def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54341 """Returns list of shared libraries used by specified binaries."""
342 logging.info('Finding shared libraries for targets (if any).')
343 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47344 cmd = []
345 shared_library_re = None
346
347 if sys.platform.startswith('linux'):
348 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13349 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
350 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47351 elif sys.platform.startswith('darwin'):
352 cmd.extend(['otool', '-L'])
353 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
354 else:
Abhishek Aryafb70b532018-05-06 17:47:40355 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47356
357 assert shared_library_re is not None
358
359 cmd.extend(binary_paths)
360 output = subprocess.check_output(cmd)
361
362 for line in output.splitlines():
363 m = shared_library_re.match(line)
364 if not m:
365 continue
366
367 shared_library_path = m.group(1)
368 if sys.platform.startswith('darwin'):
369 # otool outputs "@rpath" macro instead of the dirname of the given binary.
370 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
371
Abhishek Arya78120bc2018-05-07 20:53:54372 if shared_library_path in shared_libraries:
373 continue
374
Max Morozd73e45f2018-04-24 18:32:47375 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
376 'the given target(s) does not '
377 'exist.' % shared_library_path)
378 with open(shared_library_path) as f:
379 data = f.read()
380
381 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
382 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54383 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47384
Abhishek Arya78120bc2018-05-07 20:53:54385 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
386 shared_libraries)
387 logging.info('Finished finding shared libraries for targets.')
388 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47389
390
Yuke Liaoc60b2d02018-03-02 21:40:43391def _GetHostPlatform():
392 """Returns the host platform.
393
394 This is separate from the target platform/os that coverage is running for.
395 """
Abhishek Arya1ec832c2017-12-05 18:06:59396 if sys.platform == 'win32' or sys.platform == 'cygwin':
397 return 'win'
398 if sys.platform.startswith('linux'):
399 return 'linux'
400 else:
401 assert sys.platform == 'darwin'
402 return 'mac'
403
404
Abhishek Arya1c97ea542018-05-10 03:53:19405def _GetPathWithLLVMSymbolizerDir():
406 """Add llvm-symbolizer directory to path for symbolized stacks."""
407 path = os.getenv('PATH')
408 dirs = path.split(os.pathsep)
409 if LLVM_BIN_DIR in dirs:
410 return path
411
412 return path + os.pathsep + LLVM_BIN_DIR
413
414
Yuke Liaoc60b2d02018-03-02 21:40:43415def _GetTargetOS():
416 """Returns the target os specified in args.gn file.
417
418 Returns an empty string is target_os is not specified.
419 """
Yuke Liao80afff32018-03-07 01:26:20420 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43421 return build_args['target_os'] if 'target_os' in build_args else ''
422
423
Yuke Liaob2926832018-03-02 17:34:29424def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10425 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43426 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10427
428
Yuke Liao506e8822017-12-04 16:52:54429# TODO(crbug.com/759794): remove this function once tools get included to
430# Clang bundle:
431# https://chromium-review.googlesource.com/c/chromium/src/+/688221
432def DownloadCoverageToolsIfNeeded():
433 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59434
Yuke Liaoc60b2d02018-03-02 21:40:43435 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54436 """Returns a pair of revision number by reading the build stamp file.
437
438 Args:
439 stamp_file_path: A path the build stamp file created by
440 tools/clang/scripts/update.py.
441 Returns:
442 A pair of integers represeting the main and sub revision respectively.
443 """
444 if not os.path.exists(stamp_file_path):
445 return 0, 0
446
447 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43448 stamp_file_line = stamp_file.readline()
449 if ',' in stamp_file_line:
450 package_version = stamp_file_line.rstrip().split(',')[0]
451 else:
452 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54453
Yuke Liaoc60b2d02018-03-02 21:40:43454 clang_revision_str, clang_sub_revision_str = package_version.split('-')
455 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59456
Yuke Liaoc60b2d02018-03-02 21:40:43457 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54458 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43459 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54460
461 coverage_revision_stamp_file = os.path.join(
462 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
463 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43464 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54465
Yuke Liaoea228d02018-01-05 19:10:33466 has_coverage_tools = (
467 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32468
Yuke Liaoea228d02018-01-05 19:10:33469 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54470 coverage_sub_revision == clang_sub_revision):
471 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43472 return
Yuke Liao506e8822017-12-04 16:52:54473
474 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
475 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
476
477 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43478 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54479 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43480 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54481 coverage_tools_url = (
482 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43483 else:
484 assert host_platform == 'win'
485 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54486
487 try:
488 clang_update.DownloadAndUnpack(coverage_tools_url,
489 clang_update.LLVM_BUILD_DIR)
Abhishek Aryafb70b532018-05-06 17:47:40490 logging.info('Coverage tools %s unpacked.', package_version)
Yuke Liao506e8822017-12-04 16:52:54491 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43492 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54493 file_handle.write('\n')
494 except urllib2.URLError:
495 raise Exception(
496 'Failed to download coverage tools: %s.' % coverage_tools_url)
497
498
Yuke Liaodd1ec0592018-02-02 01:26:37499def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59500 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54501 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
502
503 For a file with absolute path /a/b/x.cc, a html report is generated as:
504 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
505 OUTPUT_DIR/index.html.
506
507 Args:
508 binary_paths: A list of paths to the instrumented binaries.
509 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42510 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54511 """
Yuke Liao506e8822017-12-04 16:52:54512 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
513 # [[-object BIN]] [SOURCES]
514 # NOTE: For object files, the first one is specified as a positional argument,
515 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10516 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40517 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59518 subprocess_cmd = [
519 LLVM_COV_PATH, 'show', '-format=html',
520 '-output-dir={}'.format(OUTPUT_DIR),
521 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
522 ]
523 subprocess_cmd.extend(
524 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29525 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42526 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59527 if ignore_filename_regex:
528 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
529
Yuke Liao506e8822017-12-04 16:52:54530 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34531
532 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
533 # the platform name instead, as it simplifies the report dir structure when
534 # the same report is generated for different platforms.
535 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48536 platform_report_subdir_path = _GetCoverageReportRootDirPath()
537 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34538
Abhishek Aryafb70b532018-05-06 17:47:40539 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54540
541
Yuke Liaodd1ec0592018-02-02 01:26:37542def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
543 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48544 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37545 logging.debug('Generating file view html index file as: "%s".',
546 file_view_index_file_path)
547 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
548 'Path')
549 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33550
Yuke Liaodd1ec0592018-02-02 01:26:37551 for file_path in per_file_coverage_summary:
552 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
553
554 html_generator.AddLinkToAnotherReport(
555 _GetCoverageHtmlReportPathForFile(file_path),
556 os.path.relpath(file_path, SRC_ROOT_PATH),
557 per_file_coverage_summary[file_path])
558
559 html_generator.CreateTotalsEntry(totals_coverage_summary)
560 html_generator.WriteHtmlCoverageReport()
561 logging.debug('Finished generating file view html index file.')
562
563
564def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
565 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40566 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37567 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
568
Yuke Liaoea228d02018-01-05 19:10:33569 for file_path in per_file_coverage_summary:
570 summary = per_file_coverage_summary[file_path]
571 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40572
Yuke Liaoea228d02018-01-05 19:10:33573 while True:
574 per_directory_coverage_summary[parent_dir].AddSummary(summary)
575
576 if parent_dir == SRC_ROOT_PATH:
577 break
578 parent_dir = os.path.dirname(parent_dir)
579
Abhishek Aryafb70b532018-05-06 17:47:40580 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37581 return per_directory_coverage_summary
582
583
584def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
585 per_file_coverage_summary):
586 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40587 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33588 for dir_path in per_directory_coverage_summary:
589 _GenerateCoverageInHtmlForDirectory(
590 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
591
Abhishek Aryafb70b532018-05-06 17:47:40592 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10593
Yuke Liaoea228d02018-01-05 19:10:33594
595def _GenerateCoverageInHtmlForDirectory(
596 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
597 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37598 html_generator = _CoverageReportHtmlGenerator(
599 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33600
601 for entry_name in os.listdir(dir_path):
602 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33603
Yuke Liaodd1ec0592018-02-02 01:26:37604 if entry_path in per_file_coverage_summary:
605 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
606 entry_coverage_summary = per_file_coverage_summary[entry_path]
607 elif entry_path in per_directory_coverage_summary:
608 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
609 entry_path)
610 entry_coverage_summary = per_directory_coverage_summary[entry_path]
611 else:
Yuke Liaoc7e607142018-02-05 20:26:14612 # Any file without executable lines shouldn't be included into the report.
613 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37614 continue
Yuke Liaoea228d02018-01-05 19:10:33615
Yuke Liaodd1ec0592018-02-02 01:26:37616 html_generator.AddLinkToAnotherReport(entry_html_report_path,
617 os.path.basename(entry_path),
618 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33619
Yuke Liaod54030e2018-01-08 17:34:12620 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37621 html_generator.WriteHtmlCoverageReport()
622
623
624def _GenerateDirectoryViewHtmlIndexFile():
625 """Generates the html index file for directory view.
626
627 Note that the index file is already generated under SRC_ROOT_PATH, so this
628 file simply redirects to it, and the reason of this extra layer is for
629 structural consistency with other views.
630 """
Max Moroz7c5354f2018-05-06 00:03:48631 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37632 logging.debug('Generating directory view html index file as: "%s".',
633 directory_view_index_file_path)
634 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
635 SRC_ROOT_PATH)
636 _WriteRedirectHtmlFile(directory_view_index_file_path,
637 src_root_html_report_path)
638 logging.debug('Finished generating directory view html index file.')
639
640
641def _CalculatePerComponentCoverageSummary(component_to_directories,
642 per_directory_coverage_summary):
643 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40644 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37645 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
646
647 for component in component_to_directories:
648 for directory in component_to_directories[component]:
649 absolute_directory_path = os.path.abspath(directory)
650 if absolute_directory_path in per_directory_coverage_summary:
651 per_component_coverage_summary[component].AddSummary(
652 per_directory_coverage_summary[absolute_directory_path])
653
Abhishek Aryafb70b532018-05-06 17:47:40654 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37655 return per_component_coverage_summary
656
657
658def _ExtractComponentToDirectoriesMapping():
659 """Returns a mapping from components to directories."""
660 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
661 directory_to_component = component_mappings['dir-to-component']
662
663 component_to_directories = defaultdict(list)
664 for directory in directory_to_component:
665 component = directory_to_component[directory]
666 component_to_directories[component].append(directory)
667
668 return component_to_directories
669
670
671def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
672 component_to_directories,
673 per_directory_coverage_summary):
674 """Generates per-component coverage reports in html."""
675 logging.debug('Writing per-component coverage html reports.')
676 for component in per_component_coverage_summary:
677 _GenerateCoverageInHtmlForComponent(
678 component, per_component_coverage_summary, component_to_directories,
679 per_directory_coverage_summary)
680
681 logging.debug('Finished writing per-component coverage html reports.')
682
683
684def _GenerateCoverageInHtmlForComponent(
685 component_name, per_component_coverage_summary, component_to_directories,
686 per_directory_coverage_summary):
687 """Generates coverage html report for a component."""
688 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
689 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14690 component_html_report_dir = os.path.dirname(component_html_report_path)
691 if not os.path.exists(component_html_report_dir):
692 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37693
694 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
695 'Path')
696
697 for dir_path in component_to_directories[component_name]:
698 dir_absolute_path = os.path.abspath(dir_path)
699 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14700 # Any directory without an excercised file shouldn't be included into the
701 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37702 continue
703
704 html_generator.AddLinkToAnotherReport(
705 _GetCoverageHtmlReportPathForDirectory(dir_path),
706 os.path.relpath(dir_path, SRC_ROOT_PATH),
707 per_directory_coverage_summary[dir_absolute_path])
708
709 html_generator.CreateTotalsEntry(
710 per_component_coverage_summary[component_name])
711 html_generator.WriteHtmlCoverageReport()
712
713
714def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
715 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48716 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37717 logging.debug('Generating component view html index file as: "%s".',
718 component_view_index_file_path)
719 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
720 'Component')
721 totals_coverage_summary = _CoverageSummary()
722
723 for component in per_component_coverage_summary:
724 totals_coverage_summary.AddSummary(
725 per_component_coverage_summary[component])
726
727 html_generator.AddLinkToAnotherReport(
728 _GetCoverageHtmlReportPathForComponent(component), component,
729 per_component_coverage_summary[component])
730
731 html_generator.CreateTotalsEntry(totals_coverage_summary)
732 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14733 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33734
735
Max Moroz7c5354f2018-05-06 00:03:48736def _MergeTwoDirectories(src_path, dst_path):
737 """Merge src_path directory into dst_path directory."""
738 for filename in os.listdir(src_path):
739 dst_path = os.path.join(dst_path, filename)
740 if os.path.exists(dst_path):
741 shutil.rmtree(dst_path)
742 os.rename(os.path.join(src_path, filename), dst_path)
743 shutil.rmtree(src_path)
744
745
Yuke Liaoea228d02018-01-05 19:10:33746def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37747 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48748 html_index_file_path = _GetHtmlIndexPath()
749 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37750 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
751
752
753def _WriteRedirectHtmlFile(from_html_path, to_html_path):
754 """Writes a html file that redirects to another html file."""
755 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
756 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33757 content = ("""
758 <!DOCTYPE html>
759 <html>
760 <head>
761 <!-- HTML meta refresh URL redirection -->
762 <meta http-equiv="refresh" content="0; url=%s">
763 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37764 </html>""" % to_html_relative_path)
765 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33766 f.write(content)
767
768
Max Moroz7c5354f2018-05-06 00:03:48769def _CleanUpOutputDir():
770 """Perform a cleanup of the output dir."""
771 # Remove the default index.html file produced by llvm-cov.
772 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
773 if os.path.exists(index_path):
774 os.remove(index_path)
775
776
Yuke Liaodd1ec0592018-02-02 01:26:37777def _GetCoverageHtmlReportPathForFile(file_path):
778 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40779 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37780 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
781
782 # '+' is used instead of os.path.join because both of them are absolute paths
783 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14784 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37785 return _GetCoverageReportRootDirPath() + html_report_path
786
787
788def _GetCoverageHtmlReportPathForDirectory(dir_path):
789 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40790 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37791 html_report_path = os.path.join(
792 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
793
794 # '+' is used instead of os.path.join because both of them are absolute paths
795 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14796 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37797 return _GetCoverageReportRootDirPath() + html_report_path
798
799
800def _GetCoverageHtmlReportPathForComponent(component_name):
801 """Given a component, returns the corresponding html report path."""
802 component_file_name = component_name.lower().replace('>', '-')
803 html_report_name = os.extsep.join([component_file_name, 'html'])
804 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
805 html_report_name)
806
807
808def _GetCoverageReportRootDirPath():
809 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48810 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
811
812
813def _GetComponentViewPath():
814 """Path to the HTML file for the component view."""
815 return os.path.join(_GetCoverageReportRootDirPath(),
816 COMPONENT_VIEW_INDEX_FILE)
817
818
819def _GetDirectoryViewPath():
820 """Path to the HTML file for the directory view."""
821 return os.path.join(_GetCoverageReportRootDirPath(),
822 DIRECTORY_VIEW_INDEX_FILE)
823
824
825def _GetFileViewPath():
826 """Path to the HTML file for the file view."""
827 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
828
829
830def _GetLogsDirectoryPath():
831 """Path to the logs directory."""
832 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
833
834
835def _GetHtmlIndexPath():
836 """Path to the main HTML index file."""
837 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
838
839
840def _GetProfdataFilePath():
841 """Path to the resulting .profdata file."""
842 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
843
844
845def _GetSummaryFilePath():
846 """The JSON file that contains coverage summary written by llvm-cov export."""
847 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33848
849
Yuke Liao506e8822017-12-04 16:52:54850def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
851 """Builds and runs target to generate the coverage profile data.
852
853 Args:
854 targets: A list of targets to build with coverage instrumentation.
855 commands: A list of commands used to run the targets.
856 jobs_count: Number of jobs to run in parallel for building. If None, a
857 default value is derived based on CPUs availability.
858
859 Returns:
860 A relative path to the generated profdata file.
861 """
862 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02863 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59864 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02865 coverage_profdata_file_path = (
866 _CreateCoverageProfileDataFromTargetProfDataFiles(
867 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54868
Abhishek Aryac19bc5ef2018-05-04 22:10:02869 for target_profdata_file_path in target_profdata_file_paths:
870 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52871
Abhishek Aryac19bc5ef2018-05-04 22:10:02872 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54873
874
875def _BuildTargets(targets, jobs_count):
876 """Builds target with Clang coverage instrumentation.
877
878 This function requires current working directory to be the root of checkout.
879
880 Args:
881 targets: A list of targets to build with coverage instrumentation.
882 jobs_count: Number of jobs to run in parallel for compilation. If None, a
883 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54884 """
Abhishek Arya1ec832c2017-12-05 18:06:59885
Yuke Liao506e8822017-12-04 16:52:54886 def _IsGomaConfigured():
887 """Returns True if goma is enabled in the gn build args.
888
889 Returns:
890 A boolean indicates whether goma is configured for building or not.
891 """
Yuke Liao80afff32018-03-07 01:26:20892 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54893 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
894
Abhishek Aryafb70b532018-05-06 17:47:40895 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54896 if jobs_count is None and _IsGomaConfigured():
897 jobs_count = DEFAULT_GOMA_JOBS
898
899 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
900 if jobs_count is not None:
901 subprocess_cmd.append('-j' + str(jobs_count))
902
903 subprocess_cmd.extend(targets)
904 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40905 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54906
907
Abhishek Aryac19bc5ef2018-05-04 22:10:02908def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54909 """Runs commands and returns the relative paths to the profraw data files.
910
911 Args:
912 targets: A list of targets built with coverage instrumentation.
913 commands: A list of commands used to run the targets.
914
915 Returns:
916 A list of relative paths to the generated profraw data files.
917 """
Abhishek Aryafb70b532018-05-06 17:47:40918 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10919
Yuke Liao506e8822017-12-04 16:52:54920 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48921 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54922 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48923 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
924
925 # Ensure that logs directory exists.
926 if not os.path.exists(_GetLogsDirectoryPath()):
927 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54928
Abhishek Aryac19bc5ef2018-05-04 22:10:02929 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10930
Yuke Liaod4a9865202018-01-12 23:17:52931 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54932 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48933 output_file_name = os.extsep.join([target + '_output', 'log'])
934 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10935
Abhishek Aryac19bc5ef2018-05-04 22:10:02936 profdata_file_path = None
937 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40938 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02939 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10940
Abhishek Aryac19bc5ef2018-05-04 22:10:02941 if _IsIOSCommand(command):
942 # On iOS platform, due to lack of write permissions, profraw files are
943 # generated outside of the OUTPUT_DIR, and the exact paths are contained
944 # in the output of the command execution.
945 output = _ExecuteIOSCommand(target, command)
946 else:
947 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
948 output = _ExecuteCommand(target, command)
949
950 with open(output_file_path, 'w') as output_file:
951 output_file.write(output)
952
953 profraw_file_paths = []
954 if _IsIOS():
955 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
956 else:
Max Moroz7c5354f2018-05-06 00:03:48957 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02958 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48959 profraw_file_paths.append(
960 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02961
962 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40963 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02964 'please make sure the binary exists and is properly '
965 'instrumented.' % target)
966
967 try:
968 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
969 target, profraw_file_paths)
970 break
971 except Exception:
972 print('Retrying...')
973 finally:
974 # Remove profraw files now so that they are not used in next iteration.
975 for profraw_file_path in profraw_file_paths:
976 os.remove(profraw_file_path)
977
978 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40979 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02980 'Please file a bug with command you used, commit position and args.gn '
981 'config here: '
982 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40983 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02984 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54985
Abhishek Aryafb70b532018-05-06 17:47:40986 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10987
Abhishek Aryac19bc5ef2018-05-04 22:10:02988 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54989
990
991def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10992 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52993 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01994 #
Max Morozd73e45f2018-04-24 18:32:47995 # "%p" expands out to the process ID. It's not used by this scripts due to:
996 # 1) If a target program spawns too many processess, it may exhaust all disk
997 # space available. For example, unit_tests writes thousands of .profraw
998 # files each of size 1GB+.
999 # 2) If a target binary uses shared libraries, coverage profile data for them
1000 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011001 #
Yuke Liaod4a9865202018-01-12 23:17:521002 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1003 # is specified, the runtime creates a pool of N raw profiles which are used
1004 # for on-line profile merging. The runtime takes care of selecting a raw
1005 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521006 # N must be between 1 and 9. The merge pool specifier can only occur once per
1007 # filename pattern.
1008 #
Max Morozd73e45f2018-04-24 18:32:471009 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011010 #
Max Morozd73e45f2018-04-24 18:32:471011 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1012 # but it's not too big to consume too much computing resource or disk space.
1013 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591014 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011015 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481016 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541017 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541018
Yuke Liaoa0c8c2f2018-02-28 20:14:101019 try:
Max Moroz7c5354f2018-05-06 00:03:481020 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101021 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291022 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481023 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191024 env={
1025 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1026 'PATH': _GetPathWithLLVMSymbolizerDir()
1027 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101028 except subprocess.CalledProcessError as e:
1029 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191030 logging.warning(
1031 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1032 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101033
1034 return output
1035
1036
Yuke Liao27349c92018-03-22 21:10:011037def _IsFuzzerTarget(target):
1038 """Returns true if the target is a fuzzer target."""
1039 build_args = _GetBuildArgs()
1040 use_libfuzzer = ('use_libfuzzer' in build_args and
1041 build_args['use_libfuzzer'] == 'true')
1042 return use_libfuzzer and target.endswith('_fuzzer')
1043
1044
Yuke Liaob2926832018-03-02 17:34:291045def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101046 """Runs a single iOS command and generates a profraw data file.
1047
1048 iOS application doesn't have write access to folders outside of the app, so
1049 it's impossible to instruct the app to flush the profraw data file to the
1050 desired location. The profraw data file will be generated somewhere within the
1051 application's Documents folder, and the full path can be obtained by parsing
1052 the output.
1053 """
Yuke Liaob2926832018-03-02 17:34:291054 assert _IsIOSCommand(command)
1055
1056 # After running tests, iossim generates a profraw data file, it won't be
1057 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1058 # checkout.
1059 iossim_profraw_file_path = os.path.join(
1060 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101061
1062 try:
Yuke Liaob2926832018-03-02 17:34:291063 output = subprocess.check_output(
1064 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191065 env={
1066 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1067 'PATH': _GetPathWithLLVMSymbolizerDir()
1068 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101069 except subprocess.CalledProcessError as e:
1070 # iossim emits non-zero return code even if tests run successfully, so
1071 # ignore the return code.
1072 output = e.output
1073
1074 return output
1075
1076
1077def _GetProfrawDataFileByParsingOutput(output):
1078 """Returns the path to the profraw data file obtained by parsing the output.
1079
1080 The output of running the test target has no format, but it is guaranteed to
1081 have a single line containing the path to the generated profraw data file.
1082 NOTE: This should only be called when target os is iOS.
1083 """
Yuke Liaob2926832018-03-02 17:34:291084 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101085
Yuke Liaob2926832018-03-02 17:34:291086 output_by_lines = ''.join(output).splitlines()
1087 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101088
1089 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291090 result = profraw_file_pattern.match(line)
1091 if result:
1092 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101093
1094 assert False, ('No profraw data file was generated, did you call '
1095 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1096 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541097
1098
Abhishek Aryac19bc5ef2018-05-04 22:10:021099def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1100 """Returns a relative path to coverage profdata file by merging target
1101 profdata files.
Yuke Liao506e8822017-12-04 16:52:541102
1103 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021104 profdata_file_paths: A list of relative paths to the profdata data files
1105 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541106
1107 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021108 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541109
1110 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021111 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541112 """
Abhishek Aryafb70b532018-05-06 17:47:401113 logging.info('Creating the coverage profile data file.')
1114 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481115 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541116 try:
Abhishek Arya1ec832c2017-12-05 18:06:591117 subprocess_cmd = [
1118 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1119 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021120 subprocess_cmd.extend(profdata_file_paths)
1121 subprocess.check_call(subprocess_cmd)
1122 except subprocess.CalledProcessError as error:
1123 print('Failed to merge target profdata files to create coverage profdata. '
1124 'Try again.')
1125 raise error
1126
Abhishek Aryafb70b532018-05-06 17:47:401127 logging.debug('Finished merging target profdata files.')
1128 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021129 profdata_file_path)
1130 return profdata_file_path
1131
1132
1133def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1134 """Returns a relative path to target profdata file by merging target
1135 profraw files.
1136
1137 Args:
1138 profraw_file_paths: A list of relative paths to the profdata data files
1139 that are to be merged.
1140
1141 Returns:
1142 A relative path to the merged coverage profdata file.
1143
1144 Raises:
1145 CalledProcessError: An error occurred merging profdata files.
1146 """
Abhishek Aryafb70b532018-05-06 17:47:401147 logging.info('Creating target profile data file.')
1148 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021149 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1150
1151 try:
1152 subprocess_cmd = [
1153 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1154 ]
Yuke Liao506e8822017-12-04 16:52:541155 subprocess_cmd.extend(profraw_file_paths)
1156 subprocess.check_call(subprocess_cmd)
1157 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021158 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541159 raise error
1160
Abhishek Aryafb70b532018-05-06 17:47:401161 logging.debug('Finished merging target profraw files.')
1162 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101163 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541164 return profdata_file_path
1165
1166
Yuke Liao0e4c8682018-04-18 21:06:591167def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1168 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331169 """Generates per file coverage summary using "llvm-cov export" command."""
1170 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1171 # [[-object BIN]] [SOURCES].
1172 # NOTE: For object files, the first one is specified as a positional argument,
1173 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101174 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401175 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331176 subprocess_cmd = [
1177 LLVM_COV_PATH, 'export', '-summary-only',
1178 '-instr-profile=' + profdata_file_path, binary_paths[0]
1179 ]
1180 subprocess_cmd.extend(
1181 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291182 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331183 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591184 if ignore_filename_regex:
1185 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331186
Max Moroz7c5354f2018-05-06 00:03:481187 export_output = subprocess.check_output(subprocess_cmd)
1188
1189 # Write output on the disk to be used by code coverage bot.
1190 with open(_GetSummaryFilePath(), 'w') as f:
1191 f.write(export_output)
1192
1193 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331194 assert len(json_output['data']) == 1
1195 files_coverage_data = json_output['data'][0]['files']
1196
1197 per_file_coverage_summary = {}
1198 for file_coverage_data in files_coverage_data:
1199 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401200 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1201 'File path "%s" in coverage summary is outside source checkout.' %
1202 file_path)
Yuke Liaoea228d02018-01-05 19:10:331203
Abhishek Aryafb70b532018-05-06 17:47:401204 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331205 if summary['lines']['count'] == 0:
1206 continue
1207
1208 per_file_coverage_summary[file_path] = _CoverageSummary(
1209 regions_total=summary['regions']['count'],
1210 regions_covered=summary['regions']['covered'],
1211 functions_total=summary['functions']['count'],
1212 functions_covered=summary['functions']['covered'],
1213 lines_total=summary['lines']['count'],
1214 lines_covered=summary['lines']['covered'])
1215
Abhishek Aryafb70b532018-05-06 17:47:401216 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331217 return per_file_coverage_summary
1218
1219
Yuke Liaob2926832018-03-02 17:34:291220def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1221 """Appends -arch arguments to the command list if it's ios platform.
1222
1223 iOS binaries are universal binaries, and require specifying the architecture
1224 to use, and one architecture needs to be specified for each binary.
1225 """
1226 if _IsIOS():
1227 cmd_list.extend(['-arch=x86_64'] * num_archs)
1228
1229
Yuke Liao506e8822017-12-04 16:52:541230def _GetBinaryPath(command):
1231 """Returns a relative path to the binary to be run by the command.
1232
Yuke Liao545db322018-02-15 17:12:011233 Currently, following types of commands are supported (e.g. url_unittests):
1234 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1235 2. Use xvfb.
1236 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1237 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371238 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1239 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101240 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371241 <iossim_arguments> -c <app_arguments>
1242 out/Coverage-iphonesimulator/url_unittests.app"
1243
Yuke Liao545db322018-02-15 17:12:011244
Yuke Liao506e8822017-12-04 16:52:541245 Args:
1246 command: A command used to run a target.
1247
1248 Returns:
1249 A relative path to the binary.
1250 """
Yuke Liao545db322018-02-15 17:12:011251 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1252
Yuke Liaob2926832018-03-02 17:34:291253 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011254 if os.path.basename(command_parts[0]) == 'python':
1255 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401256 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011257 return command_parts[2]
1258
1259 if os.path.basename(command_parts[0]) == xvfb_script_name:
1260 return command_parts[1]
1261
Yuke Liaob2926832018-03-02 17:34:291262 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101263 # For a given application bundle, the binary resides in the bundle and has
1264 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371265 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101266 app_name = os.path.splitext(os.path.basename(app_path))[0]
1267 return os.path.join(app_path, app_name)
1268
Yuke Liaob2926832018-03-02 17:34:291269 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541270
1271
Yuke Liaob2926832018-03-02 17:34:291272def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101273 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291274 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101275
1276
Yuke Liao95d13d72017-12-07 18:18:501277def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1278 """Verifies that the target executables specified in the commands are inside
1279 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541280 for command in commands:
1281 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501282 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481283 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501284 'Target executable "%s" in command: "%s" is outside of '
1285 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541286
1287
1288def _ValidateBuildingWithClangCoverage():
1289 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201290 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541291
1292 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1293 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591294 assert False, ('\'{} = true\' is required in args.gn.'
1295 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541296
1297
Yuke Liaoc60b2d02018-03-02 21:40:431298def _ValidateCurrentPlatformIsSupported():
1299 """Asserts that this script suports running on the current platform"""
1300 target_os = _GetTargetOS()
1301 if target_os:
1302 current_platform = target_os
1303 else:
1304 current_platform = _GetHostPlatform()
1305
1306 assert current_platform in [
1307 'linux', 'mac', 'chromeos', 'ios'
1308 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1309
1310
Yuke Liao80afff32018-03-07 01:26:201311def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541312 """Parses args.gn file and returns results as a dictionary.
1313
1314 Returns:
1315 A dictionary representing the build args.
1316 """
Yuke Liao80afff32018-03-07 01:26:201317 global _BUILD_ARGS
1318 if _BUILD_ARGS is not None:
1319 return _BUILD_ARGS
1320
1321 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541322 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1323 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1324 'missing args.gn file.' % BUILD_DIR)
1325 with open(build_args_path) as build_args_file:
1326 build_args_lines = build_args_file.readlines()
1327
Yuke Liao506e8822017-12-04 16:52:541328 for build_arg_line in build_args_lines:
1329 build_arg_without_comments = build_arg_line.split('#')[0]
1330 key_value_pair = build_arg_without_comments.split('=')
1331 if len(key_value_pair) != 2:
1332 continue
1333
1334 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431335
1336 # Values are wrapped within a pair of double-quotes, so remove the leading
1337 # and trailing double-quotes.
1338 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201339 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541340
Yuke Liao80afff32018-03-07 01:26:201341 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541342
1343
Abhishek Arya16f059a2017-12-07 17:47:321344def _VerifyPathsAndReturnAbsolutes(paths):
1345 """Verifies that the paths specified in |paths| exist and returns absolute
1346 versions.
Yuke Liao66da1732017-12-05 22:19:421347
1348 Args:
1349 paths: A list of files or directories.
1350 """
Abhishek Arya16f059a2017-12-07 17:47:321351 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421352 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321353 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1354 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1355
1356 absolute_paths.append(absolute_path)
1357
1358 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421359
1360
Yuke Liaodd1ec0592018-02-02 01:26:371361def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1362 """Returns a target path relative to the directory of base_path.
1363
1364 This method requires base_path to be a file, otherwise, one should call
1365 os.path.relpath directly.
1366 """
1367 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141368 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371369 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141370 base_dir = os.path.dirname(base_path)
1371 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371372
1373
Abhishek Arya64636af2018-05-04 14:42:131374def _GetBinaryPathsFromTargets(targets, build_dir):
1375 """Return binary paths from target names."""
1376 # FIXME: Derive output binary from target build definitions rather than
1377 # assuming that it is always the same name.
1378 binary_paths = []
1379 for target in targets:
1380 binary_path = os.path.join(build_dir, target)
1381 if _GetHostPlatform() == 'win':
1382 binary_path += '.exe'
1383
1384 if os.path.exists(binary_path):
1385 binary_paths.append(binary_path)
1386 else:
1387 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401388 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131389 os.path.basename(binary_path))
1390
1391 return binary_paths
1392
1393
Yuke Liao506e8822017-12-04 16:52:541394def _ParseCommandArguments():
1395 """Adds and parses relevant arguments for tool comands.
1396
1397 Returns:
1398 A dictionary representing the arguments.
1399 """
1400 arg_parser = argparse.ArgumentParser()
1401 arg_parser.usage = __doc__
1402
Abhishek Arya1ec832c2017-12-05 18:06:591403 arg_parser.add_argument(
1404 '-b',
1405 '--build-dir',
1406 type=str,
1407 required=True,
1408 help='The build directory, the path needs to be relative to the root of '
1409 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541410
Abhishek Arya1ec832c2017-12-05 18:06:591411 arg_parser.add_argument(
1412 '-o',
1413 '--output-dir',
1414 type=str,
1415 required=True,
1416 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541417
Abhishek Arya1ec832c2017-12-05 18:06:591418 arg_parser.add_argument(
1419 '-c',
1420 '--command',
1421 action='append',
Abhishek Arya64636af2018-05-04 14:42:131422 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591423 help='Commands used to run test targets, one test target needs one and '
1424 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131425 'current working directory is the root of the checkout. This option is '
1426 'incompatible with -p/--profdata-file option.')
1427
1428 arg_parser.add_argument(
1429 '-p',
1430 '--profdata-file',
1431 type=str,
1432 required=False,
1433 help='Path to profdata file to use for generating code coverage reports. '
1434 'This can be useful if you generated the profdata file seperately in '
1435 'your own test harness. This option is ignored if run command(s) are '
1436 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541437
Abhishek Arya1ec832c2017-12-05 18:06:591438 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421439 '-f',
1440 '--filters',
1441 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321442 required=False,
Yuke Liao66da1732017-12-05 22:19:421443 help='Directories or files to get code coverage for, and all files under '
1444 'the directories are included recursively.')
1445
1446 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591447 '-i',
1448 '--ignore-filename-regex',
1449 type=str,
1450 help='Skip source code files with file paths that match the given '
1451 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1452 'to exclude files in third_party/ and out/ folders from the report.')
1453
1454 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591455 '-j',
1456 '--jobs',
1457 type=int,
1458 default=None,
1459 help='Run N jobs to build in parallel. If not specified, a default value '
1460 'will be derived based on CPUs availability. Please refer to '
1461 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541462
Abhishek Arya1ec832c2017-12-05 18:06:591463 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101464 '-v',
1465 '--verbose',
1466 action='store_true',
1467 help='Prints additional output for diagnostics.')
1468
1469 arg_parser.add_argument(
1470 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1471
1472 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021473 'targets',
1474 nargs='+',
1475 help='The names of the test targets to run. If multiple run commands are '
1476 'specified using the -c/--command option, then the order of targets and '
1477 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541478
1479 args = arg_parser.parse_args()
1480 return args
1481
1482
1483def Main():
1484 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131485 # Change directory to source root to aid in relative paths calculations.
1486 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111487
Abhishek Arya64636af2018-05-04 14:42:131488 # Setup coverage binaries even when script is called with empty params. This
1489 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111490 DownloadCoverageToolsIfNeeded()
1491
Yuke Liao506e8822017-12-04 16:52:541492 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131493 _ConfigureLogging(args)
1494
Yuke Liao506e8822017-12-04 16:52:541495 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481496 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541497 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481498 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541499
Abhishek Arya64636af2018-05-04 14:42:131500 assert args.command or args.profdata_file, (
1501 'Need to either provide commands to run using -c/--command option OR '
1502 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431503
Abhishek Arya64636af2018-05-04 14:42:131504 assert not args.command or (len(args.targets) == len(args.command)), (
1505 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431506
Abhishek Arya1ec832c2017-12-05 18:06:591507 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401508 'Build directory: "%s" doesn\'t exist. '
1509 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131510
Yuke Liaoc60b2d02018-03-02 21:40:431511 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541512 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321513
1514 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421515 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321516 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421517
Max Moroz7c5354f2018-05-06 00:03:481518 if not os.path.exists(_GetCoverageReportRootDirPath()):
1519 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541520
Abhishek Arya64636af2018-05-04 14:42:131521 # Get profdate file and list of binary paths.
1522 if args.command:
1523 # A list of commands are provided. Run them to generate profdata file, and
1524 # create a list of binary paths from parsing commands.
1525 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1526 profdata_file_path = _CreateCoverageProfileDataForTargets(
1527 args.targets, args.command, args.jobs)
1528 binary_paths = [_GetBinaryPath(command) for command in args.command]
1529 else:
1530 # An input prof-data file is already provided. Just calculate binary paths.
1531 profdata_file_path = args.profdata_file
1532 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331533
Abhishek Arya78120bc2018-05-07 20:53:541534 binary_paths.extend(_GetSharedLibraries(binary_paths))
1535
Yuke Liao481d3482018-01-29 19:17:101536 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401537 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371538 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591539 binary_paths, profdata_file_path, absolute_filter_paths,
1540 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371541 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591542 absolute_filter_paths,
1543 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371544 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1545
1546 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1547 per_file_coverage_summary)
1548 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1549 per_file_coverage_summary)
1550 _GenerateDirectoryViewHtmlIndexFile()
1551
1552 component_to_directories = _ExtractComponentToDirectoriesMapping()
1553 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1554 component_to_directories, per_directory_coverage_summary)
1555 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1556 component_to_directories,
1557 per_directory_coverage_summary)
1558 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331559
1560 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371561 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331562 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481563 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331564
Max Moroz7c5354f2018-05-06 00:03:481565 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401566 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101567 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541568
Abhishek Arya1ec832c2017-12-05 18:06:591569
Yuke Liao506e8822017-12-04 16:52:541570if __name__ == '__main__':
1571 sys.exit(Main())