blob: 397ba5122c139a74c49f95bcbd84c2a290ee920f [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
Abhishek Aryad35de7e2018-05-10 22:23:04140# Message to guide user to file a bug when everything else fails.
141FILE_BUG_MESSAGE = (
142 'If it persists, please file a bug with the command you used, git revision '
143 'and args.gn config here: '
144 'https://bugs.chromium.org/p/chromium/issues/entry?'
145 'components=Tools%3ECodeCoverage')
146
Yuke Liaoea228d02018-01-05 19:10:33147
148class _CoverageSummary(object):
149 """Encapsulates coverage summary representation."""
150
Yuke Liaodd1ec0592018-02-02 01:26:37151 def __init__(self,
152 regions_total=0,
153 regions_covered=0,
154 functions_total=0,
155 functions_covered=0,
156 lines_total=0,
157 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33158 """Initializes _CoverageSummary object."""
159 self._summary = {
160 'regions': {
161 'total': regions_total,
162 'covered': regions_covered
163 },
164 'functions': {
165 'total': functions_total,
166 'covered': functions_covered
167 },
168 'lines': {
169 'total': lines_total,
170 'covered': lines_covered
171 }
172 }
173
174 def Get(self):
175 """Returns summary as a dictionary."""
176 return self._summary
177
178 def AddSummary(self, other_summary):
179 """Adds another summary to this one element-wise."""
180 for feature in self._summary:
181 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
182 self._summary[feature]['covered'] += other_summary.Get()[feature][
183 'covered']
184
185
Yuke Liaodd1ec0592018-02-02 01:26:37186class _CoverageReportHtmlGenerator(object):
187 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33188
Yuke Liaodd1ec0592018-02-02 01:26:37189 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33190 """
191
Yuke Liaodd1ec0592018-02-02 01:26:37192 def __init__(self, output_path, table_entry_type):
193 """Initializes _CoverageReportHtmlGenerator object.
194
195 Args:
196 output_path: Path to the html report that will be generated.
197 table_entry_type: Type of the table entries to be displayed in the table
198 header. For example: 'Path', 'Component'.
199 """
Yuke Liaoea228d02018-01-05 19:10:33200 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48201 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33202 assert os.path.exists(css_absolute_path), (
203 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40204 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33205 css_absolute_path)
206
207 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37208 self._output_path = output_path
209 self._table_entry_type = table_entry_type
210
Yuke Liaoea228d02018-01-05 19:10:33211 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12212 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23213
214 source_dir = os.path.dirname(os.path.realpath(__file__))
215 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33216
217 jinja_env = jinja2.Environment(
218 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
219 self._header_template = jinja_env.get_template('header.html')
220 self._table_template = jinja_env.get_template('table.html')
221 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23222
Abhishek Arya865fffd2018-05-08 22:16:01223 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23224 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33225
226 def AddLinkToAnotherReport(self, html_report_path, name, summary):
227 """Adds a link to another html report in this report.
228
229 The link to be added is assumed to be an entry in this directory.
230 """
Yuke Liaodd1ec0592018-02-02 01:26:37231 # Use relative paths instead of absolute paths to make the generated reports
232 # portable.
233 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
234 html_report_path, self._output_path)
235
Yuke Liaod54030e2018-01-08 17:34:12236 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37237 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12238 os.path.basename(html_report_path) ==
239 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
240 self._table_entries.append(table_entry)
241
242 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35243 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12244 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
245
246 def _CreateTableEntryFromCoverageSummary(self,
247 summary,
248 href=None,
249 name=None,
250 is_dir=None):
251 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37252 assert (href is None and name is None and is_dir is None) or (
253 href is not None and name is not None and is_dir is not None), (
254 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35255 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37256 'attributes must be None.')
257
Yuke Liaod54030e2018-01-08 17:34:12258 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37259 if href is not None:
260 entry['href'] = href
261 if name is not None:
262 entry['name'] = name
263 if is_dir is not None:
264 entry['is_dir'] = is_dir
265
Yuke Liaoea228d02018-01-05 19:10:33266 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12267 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37268 if summary_dict[feature]['total'] == 0:
269 percentage = 0.0
270 else:
Yuke Liao0e4c8682018-04-18 21:06:59271 percentage = float(summary_dict[feature]
272 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35273
Yuke Liaoea228d02018-01-05 19:10:33274 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12275 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33276 'total': summary_dict[feature]['total'],
277 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35278 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33279 'color_class': color_class
280 }
Yuke Liaod54030e2018-01-08 17:34:12281
Yuke Liaod54030e2018-01-08 17:34:12282 return entry
Yuke Liaoea228d02018-01-05 19:10:33283
284 def _GetColorClass(self, percentage):
285 """Returns the css color class based on coverage percentage."""
286 if percentage >= 0 and percentage < 80:
287 return 'red'
288 if percentage >= 80 and percentage < 100:
289 return 'yellow'
290 if percentage == 100:
291 return 'green'
292
Abhishek Aryafb70b532018-05-06 17:47:40293 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33294
Yuke Liaodd1ec0592018-02-02 01:26:37295 def WriteHtmlCoverageReport(self):
296 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33297
298 In the report, sub-directories are displayed before files and within each
299 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33300 """
301
302 def EntryCmp(left, right):
303 """Compare function for table entries."""
304 if left['is_dir'] != right['is_dir']:
305 return -1 if left['is_dir'] == True else 1
306
Yuke Liaodd1ec0592018-02-02 01:26:37307 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33308
309 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
310
311 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48312
313 directory_view_path = _GetDirectoryViewPath()
314 component_view_path = _GetComponentViewPath()
315 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37316
Yuke Liaoea228d02018-01-05 19:10:33317 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37318 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
319 directory_view_href=_GetRelativePathToDirectoryOfFile(
320 directory_view_path, self._output_path),
321 component_view_href=_GetRelativePathToDirectoryOfFile(
322 component_view_path, self._output_path),
323 file_view_href=_GetRelativePathToDirectoryOfFile(
Abhishek Arya865fffd2018-05-08 22:16:01324 file_view_path, self._output_path),
325 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37326
Yuke Liaod54030e2018-01-08 17:34:12327 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37328 entries=self._table_entries,
329 total_entry=self._total_entry,
330 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33331 html_footer = self._footer_template.render()
332
Yuke Liaodd1ec0592018-02-02 01:26:37333 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33334 html_file.write(html_header + html_table + html_footer)
335
Yuke Liao506e8822017-12-04 16:52:54336
Abhishek Arya64636af2018-05-04 14:42:13337def _ConfigureLogging(args):
338 """Configures logging settings for later use."""
339 log_level = logging.DEBUG if args.verbose else logging.INFO
340 log_format = '[%(asctime)s %(levelname)s] %(message)s'
341 log_file = args.log_file if args.log_file else None
342 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
343
344
Max Morozd73e45f2018-04-24 18:32:47345def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54346 """Returns list of shared libraries used by specified binaries."""
347 logging.info('Finding shared libraries for targets (if any).')
348 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47349 cmd = []
350 shared_library_re = None
351
352 if sys.platform.startswith('linux'):
353 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13354 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
355 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47356 elif sys.platform.startswith('darwin'):
357 cmd.extend(['otool', '-L'])
358 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
359 else:
Abhishek Aryafb70b532018-05-06 17:47:40360 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47361
362 assert shared_library_re is not None
363
364 cmd.extend(binary_paths)
365 output = subprocess.check_output(cmd)
366
367 for line in output.splitlines():
368 m = shared_library_re.match(line)
369 if not m:
370 continue
371
372 shared_library_path = m.group(1)
373 if sys.platform.startswith('darwin'):
374 # otool outputs "@rpath" macro instead of the dirname of the given binary.
375 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
376
Abhishek Arya78120bc2018-05-07 20:53:54377 if shared_library_path in shared_libraries:
378 continue
379
Max Morozd73e45f2018-04-24 18:32:47380 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
381 'the given target(s) does not '
382 'exist.' % shared_library_path)
383 with open(shared_library_path) as f:
384 data = f.read()
385
386 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
387 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54388 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47389
Abhishek Arya78120bc2018-05-07 20:53:54390 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
391 shared_libraries)
392 logging.info('Finished finding shared libraries for targets.')
393 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47394
395
Yuke Liaoc60b2d02018-03-02 21:40:43396def _GetHostPlatform():
397 """Returns the host platform.
398
399 This is separate from the target platform/os that coverage is running for.
400 """
Abhishek Arya1ec832c2017-12-05 18:06:59401 if sys.platform == 'win32' or sys.platform == 'cygwin':
402 return 'win'
403 if sys.platform.startswith('linux'):
404 return 'linux'
405 else:
406 assert sys.platform == 'darwin'
407 return 'mac'
408
409
Abhishek Arya1c97ea542018-05-10 03:53:19410def _GetPathWithLLVMSymbolizerDir():
411 """Add llvm-symbolizer directory to path for symbolized stacks."""
412 path = os.getenv('PATH')
413 dirs = path.split(os.pathsep)
414 if LLVM_BIN_DIR in dirs:
415 return path
416
417 return path + os.pathsep + LLVM_BIN_DIR
418
419
Yuke Liaoc60b2d02018-03-02 21:40:43420def _GetTargetOS():
421 """Returns the target os specified in args.gn file.
422
423 Returns an empty string is target_os is not specified.
424 """
Yuke Liao80afff32018-03-07 01:26:20425 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43426 return build_args['target_os'] if 'target_os' in build_args else ''
427
428
Yuke Liaob2926832018-03-02 17:34:29429def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10430 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43431 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10432
433
Yuke Liao506e8822017-12-04 16:52:54434# TODO(crbug.com/759794): remove this function once tools get included to
435# Clang bundle:
436# https://chromium-review.googlesource.com/c/chromium/src/+/688221
437def DownloadCoverageToolsIfNeeded():
438 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59439
Yuke Liaoc60b2d02018-03-02 21:40:43440 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54441 """Returns a pair of revision number by reading the build stamp file.
442
443 Args:
444 stamp_file_path: A path the build stamp file created by
445 tools/clang/scripts/update.py.
446 Returns:
447 A pair of integers represeting the main and sub revision respectively.
448 """
449 if not os.path.exists(stamp_file_path):
450 return 0, 0
451
452 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43453 stamp_file_line = stamp_file.readline()
454 if ',' in stamp_file_line:
455 package_version = stamp_file_line.rstrip().split(',')[0]
456 else:
457 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54458
Yuke Liaoc60b2d02018-03-02 21:40:43459 clang_revision_str, clang_sub_revision_str = package_version.split('-')
460 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59461
Yuke Liaoc60b2d02018-03-02 21:40:43462 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54463 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43464 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54465
466 coverage_revision_stamp_file = os.path.join(
467 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
468 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43469 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54470
Yuke Liaoea228d02018-01-05 19:10:33471 has_coverage_tools = (
472 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32473
Yuke Liaoea228d02018-01-05 19:10:33474 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54475 coverage_sub_revision == clang_sub_revision):
476 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43477 return
Yuke Liao506e8822017-12-04 16:52:54478
479 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
480 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
481
482 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43483 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54484 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43485 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54486 coverage_tools_url = (
487 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43488 else:
489 assert host_platform == 'win'
490 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54491
492 try:
493 clang_update.DownloadAndUnpack(coverage_tools_url,
494 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54495 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43496 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54497 file_handle.write('\n')
498 except urllib2.URLError:
499 raise Exception(
500 'Failed to download coverage tools: %s.' % coverage_tools_url)
501
502
Yuke Liaodd1ec0592018-02-02 01:26:37503def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59504 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54505 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
506
507 For a file with absolute path /a/b/x.cc, a html report is generated as:
508 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
509 OUTPUT_DIR/index.html.
510
511 Args:
512 binary_paths: A list of paths to the instrumented binaries.
513 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42514 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54515 """
Yuke Liao506e8822017-12-04 16:52:54516 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
517 # [[-object BIN]] [SOURCES]
518 # NOTE: For object files, the first one is specified as a positional argument,
519 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10520 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40521 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59522 subprocess_cmd = [
523 LLVM_COV_PATH, 'show', '-format=html',
524 '-output-dir={}'.format(OUTPUT_DIR),
525 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
526 ]
527 subprocess_cmd.extend(
528 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29529 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42530 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59531 if ignore_filename_regex:
532 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
533
Yuke Liao506e8822017-12-04 16:52:54534 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34535
536 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
537 # the platform name instead, as it simplifies the report dir structure when
538 # the same report is generated for different platforms.
539 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48540 platform_report_subdir_path = _GetCoverageReportRootDirPath()
541 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34542
Abhishek Aryafb70b532018-05-06 17:47:40543 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54544
545
Yuke Liaodd1ec0592018-02-02 01:26:37546def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
547 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48548 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37549 logging.debug('Generating file view html index file as: "%s".',
550 file_view_index_file_path)
551 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
552 'Path')
553 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33554
Yuke Liaodd1ec0592018-02-02 01:26:37555 for file_path in per_file_coverage_summary:
556 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
557
558 html_generator.AddLinkToAnotherReport(
559 _GetCoverageHtmlReportPathForFile(file_path),
560 os.path.relpath(file_path, SRC_ROOT_PATH),
561 per_file_coverage_summary[file_path])
562
563 html_generator.CreateTotalsEntry(totals_coverage_summary)
564 html_generator.WriteHtmlCoverageReport()
565 logging.debug('Finished generating file view html index file.')
566
567
568def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
569 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40570 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37571 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
572
Yuke Liaoea228d02018-01-05 19:10:33573 for file_path in per_file_coverage_summary:
574 summary = per_file_coverage_summary[file_path]
575 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40576
Yuke Liaoea228d02018-01-05 19:10:33577 while True:
578 per_directory_coverage_summary[parent_dir].AddSummary(summary)
579
580 if parent_dir == SRC_ROOT_PATH:
581 break
582 parent_dir = os.path.dirname(parent_dir)
583
Abhishek Aryafb70b532018-05-06 17:47:40584 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37585 return per_directory_coverage_summary
586
587
588def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
589 per_file_coverage_summary):
590 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40591 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33592 for dir_path in per_directory_coverage_summary:
593 _GenerateCoverageInHtmlForDirectory(
594 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
595
Abhishek Aryafb70b532018-05-06 17:47:40596 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10597
Yuke Liaoea228d02018-01-05 19:10:33598
599def _GenerateCoverageInHtmlForDirectory(
600 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
601 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37602 html_generator = _CoverageReportHtmlGenerator(
603 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33604
605 for entry_name in os.listdir(dir_path):
606 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33607
Yuke Liaodd1ec0592018-02-02 01:26:37608 if entry_path in per_file_coverage_summary:
609 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
610 entry_coverage_summary = per_file_coverage_summary[entry_path]
611 elif entry_path in per_directory_coverage_summary:
612 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
613 entry_path)
614 entry_coverage_summary = per_directory_coverage_summary[entry_path]
615 else:
Yuke Liaoc7e607142018-02-05 20:26:14616 # Any file without executable lines shouldn't be included into the report.
617 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37618 continue
Yuke Liaoea228d02018-01-05 19:10:33619
Yuke Liaodd1ec0592018-02-02 01:26:37620 html_generator.AddLinkToAnotherReport(entry_html_report_path,
621 os.path.basename(entry_path),
622 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33623
Yuke Liaod54030e2018-01-08 17:34:12624 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37625 html_generator.WriteHtmlCoverageReport()
626
627
628def _GenerateDirectoryViewHtmlIndexFile():
629 """Generates the html index file for directory view.
630
631 Note that the index file is already generated under SRC_ROOT_PATH, so this
632 file simply redirects to it, and the reason of this extra layer is for
633 structural consistency with other views.
634 """
Max Moroz7c5354f2018-05-06 00:03:48635 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37636 logging.debug('Generating directory view html index file as: "%s".',
637 directory_view_index_file_path)
638 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
639 SRC_ROOT_PATH)
640 _WriteRedirectHtmlFile(directory_view_index_file_path,
641 src_root_html_report_path)
642 logging.debug('Finished generating directory view html index file.')
643
644
645def _CalculatePerComponentCoverageSummary(component_to_directories,
646 per_directory_coverage_summary):
647 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40648 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37649 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
650
651 for component in component_to_directories:
652 for directory in component_to_directories[component]:
653 absolute_directory_path = os.path.abspath(directory)
654 if absolute_directory_path in per_directory_coverage_summary:
655 per_component_coverage_summary[component].AddSummary(
656 per_directory_coverage_summary[absolute_directory_path])
657
Abhishek Aryafb70b532018-05-06 17:47:40658 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37659 return per_component_coverage_summary
660
661
662def _ExtractComponentToDirectoriesMapping():
663 """Returns a mapping from components to directories."""
664 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
665 directory_to_component = component_mappings['dir-to-component']
666
667 component_to_directories = defaultdict(list)
668 for directory in directory_to_component:
669 component = directory_to_component[directory]
670 component_to_directories[component].append(directory)
671
672 return component_to_directories
673
674
675def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
676 component_to_directories,
677 per_directory_coverage_summary):
678 """Generates per-component coverage reports in html."""
679 logging.debug('Writing per-component coverage html reports.')
680 for component in per_component_coverage_summary:
681 _GenerateCoverageInHtmlForComponent(
682 component, per_component_coverage_summary, component_to_directories,
683 per_directory_coverage_summary)
684
685 logging.debug('Finished writing per-component coverage html reports.')
686
687
688def _GenerateCoverageInHtmlForComponent(
689 component_name, per_component_coverage_summary, component_to_directories,
690 per_directory_coverage_summary):
691 """Generates coverage html report for a component."""
692 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
693 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14694 component_html_report_dir = os.path.dirname(component_html_report_path)
695 if not os.path.exists(component_html_report_dir):
696 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37697
698 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
699 'Path')
700
701 for dir_path in component_to_directories[component_name]:
702 dir_absolute_path = os.path.abspath(dir_path)
703 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14704 # Any directory without an excercised file shouldn't be included into the
705 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37706 continue
707
708 html_generator.AddLinkToAnotherReport(
709 _GetCoverageHtmlReportPathForDirectory(dir_path),
710 os.path.relpath(dir_path, SRC_ROOT_PATH),
711 per_directory_coverage_summary[dir_absolute_path])
712
713 html_generator.CreateTotalsEntry(
714 per_component_coverage_summary[component_name])
715 html_generator.WriteHtmlCoverageReport()
716
717
718def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
719 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48720 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37721 logging.debug('Generating component view html index file as: "%s".',
722 component_view_index_file_path)
723 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
724 'Component')
725 totals_coverage_summary = _CoverageSummary()
726
727 for component in per_component_coverage_summary:
728 totals_coverage_summary.AddSummary(
729 per_component_coverage_summary[component])
730
731 html_generator.AddLinkToAnotherReport(
732 _GetCoverageHtmlReportPathForComponent(component), component,
733 per_component_coverage_summary[component])
734
735 html_generator.CreateTotalsEntry(totals_coverage_summary)
736 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14737 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33738
739
Max Moroz7c5354f2018-05-06 00:03:48740def _MergeTwoDirectories(src_path, dst_path):
741 """Merge src_path directory into dst_path directory."""
742 for filename in os.listdir(src_path):
743 dst_path = os.path.join(dst_path, filename)
744 if os.path.exists(dst_path):
745 shutil.rmtree(dst_path)
746 os.rename(os.path.join(src_path, filename), dst_path)
747 shutil.rmtree(src_path)
748
749
Yuke Liaoea228d02018-01-05 19:10:33750def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37751 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48752 html_index_file_path = _GetHtmlIndexPath()
753 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37754 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
755
756
757def _WriteRedirectHtmlFile(from_html_path, to_html_path):
758 """Writes a html file that redirects to another html file."""
759 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
760 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33761 content = ("""
762 <!DOCTYPE html>
763 <html>
764 <head>
765 <!-- HTML meta refresh URL redirection -->
766 <meta http-equiv="refresh" content="0; url=%s">
767 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37768 </html>""" % to_html_relative_path)
769 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33770 f.write(content)
771
772
Max Moroz7c5354f2018-05-06 00:03:48773def _CleanUpOutputDir():
774 """Perform a cleanup of the output dir."""
775 # Remove the default index.html file produced by llvm-cov.
776 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
777 if os.path.exists(index_path):
778 os.remove(index_path)
779
780
Yuke Liaodd1ec0592018-02-02 01:26:37781def _GetCoverageHtmlReportPathForFile(file_path):
782 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40783 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37784 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
785
786 # '+' is used instead of os.path.join because both of them are absolute paths
787 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14788 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37789 return _GetCoverageReportRootDirPath() + html_report_path
790
791
792def _GetCoverageHtmlReportPathForDirectory(dir_path):
793 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40794 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37795 html_report_path = os.path.join(
796 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
797
798 # '+' is used instead of os.path.join because both of them are absolute paths
799 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14800 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37801 return _GetCoverageReportRootDirPath() + html_report_path
802
803
804def _GetCoverageHtmlReportPathForComponent(component_name):
805 """Given a component, returns the corresponding html report path."""
806 component_file_name = component_name.lower().replace('>', '-')
807 html_report_name = os.extsep.join([component_file_name, 'html'])
808 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
809 html_report_name)
810
811
812def _GetCoverageReportRootDirPath():
813 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48814 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
815
816
817def _GetComponentViewPath():
818 """Path to the HTML file for the component view."""
819 return os.path.join(_GetCoverageReportRootDirPath(),
820 COMPONENT_VIEW_INDEX_FILE)
821
822
823def _GetDirectoryViewPath():
824 """Path to the HTML file for the directory view."""
825 return os.path.join(_GetCoverageReportRootDirPath(),
826 DIRECTORY_VIEW_INDEX_FILE)
827
828
829def _GetFileViewPath():
830 """Path to the HTML file for the file view."""
831 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
832
833
834def _GetLogsDirectoryPath():
835 """Path to the logs directory."""
836 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
837
838
839def _GetHtmlIndexPath():
840 """Path to the main HTML index file."""
841 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
842
843
844def _GetProfdataFilePath():
845 """Path to the resulting .profdata file."""
846 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
847
848
849def _GetSummaryFilePath():
850 """The JSON file that contains coverage summary written by llvm-cov export."""
851 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33852
853
Yuke Liao506e8822017-12-04 16:52:54854def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
855 """Builds and runs target to generate the coverage profile data.
856
857 Args:
858 targets: A list of targets to build with coverage instrumentation.
859 commands: A list of commands used to run the targets.
860 jobs_count: Number of jobs to run in parallel for building. If None, a
861 default value is derived based on CPUs availability.
862
863 Returns:
864 A relative path to the generated profdata file.
865 """
866 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02867 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59868 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02869 coverage_profdata_file_path = (
870 _CreateCoverageProfileDataFromTargetProfDataFiles(
871 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54872
Abhishek Aryac19bc5ef2018-05-04 22:10:02873 for target_profdata_file_path in target_profdata_file_paths:
874 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52875
Abhishek Aryac19bc5ef2018-05-04 22:10:02876 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54877
878
879def _BuildTargets(targets, jobs_count):
880 """Builds target with Clang coverage instrumentation.
881
882 This function requires current working directory to be the root of checkout.
883
884 Args:
885 targets: A list of targets to build with coverage instrumentation.
886 jobs_count: Number of jobs to run in parallel for compilation. If None, a
887 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54888 """
Abhishek Arya1ec832c2017-12-05 18:06:59889
Yuke Liao506e8822017-12-04 16:52:54890 def _IsGomaConfigured():
891 """Returns True if goma is enabled in the gn build args.
892
893 Returns:
894 A boolean indicates whether goma is configured for building or not.
895 """
Yuke Liao80afff32018-03-07 01:26:20896 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54897 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
898
Abhishek Aryafb70b532018-05-06 17:47:40899 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54900 if jobs_count is None and _IsGomaConfigured():
901 jobs_count = DEFAULT_GOMA_JOBS
902
903 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
904 if jobs_count is not None:
905 subprocess_cmd.append('-j' + str(jobs_count))
906
907 subprocess_cmd.extend(targets)
908 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40909 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54910
911
Abhishek Aryac19bc5ef2018-05-04 22:10:02912def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54913 """Runs commands and returns the relative paths to the profraw data files.
914
915 Args:
916 targets: A list of targets built with coverage instrumentation.
917 commands: A list of commands used to run the targets.
918
919 Returns:
920 A list of relative paths to the generated profraw data files.
921 """
Abhishek Aryafb70b532018-05-06 17:47:40922 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10923
Yuke Liao506e8822017-12-04 16:52:54924 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48925 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54926 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48927 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
928
929 # Ensure that logs directory exists.
930 if not os.path.exists(_GetLogsDirectoryPath()):
931 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54932
Abhishek Aryac19bc5ef2018-05-04 22:10:02933 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10934
Yuke Liaod4a9865202018-01-12 23:17:52935 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54936 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48937 output_file_name = os.extsep.join([target + '_output', 'log'])
938 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10939
Abhishek Aryac19bc5ef2018-05-04 22:10:02940 profdata_file_path = None
941 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40942 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02943 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10944
Abhishek Aryac19bc5ef2018-05-04 22:10:02945 if _IsIOSCommand(command):
946 # On iOS platform, due to lack of write permissions, profraw files are
947 # generated outside of the OUTPUT_DIR, and the exact paths are contained
948 # in the output of the command execution.
949 output = _ExecuteIOSCommand(target, command)
950 else:
951 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
952 output = _ExecuteCommand(target, command)
953
954 with open(output_file_path, 'w') as output_file:
955 output_file.write(output)
956
957 profraw_file_paths = []
958 if _IsIOS():
959 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
960 else:
Max Moroz7c5354f2018-05-06 00:03:48961 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02962 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48963 profraw_file_paths.append(
964 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02965
966 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40967 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryad35de7e2018-05-10 22:23:04968 'please make sure the binary exists, is properly instrumented and '
969 'does not crash. %s' % (target, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:02970
971 try:
972 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
973 target, profraw_file_paths)
974 break
975 except Exception:
Abhishek Aryad35de7e2018-05-10 22:23:04976 logging.info('Retrying...')
Abhishek Aryac19bc5ef2018-05-04 22:10:02977 finally:
978 # Remove profraw files now so that they are not used in next iteration.
979 for profraw_file_path in profraw_file_paths:
980 os.remove(profraw_file_path)
981
982 assert profdata_file_path, (
Abhishek Aryad35de7e2018-05-10 22:23:04983 'Failed to merge target "%s" profraw files after %d retries. %s' %
984 (target, MERGE_RETRIES, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:02985 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54986
Abhishek Aryafb70b532018-05-06 17:47:40987 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10988
Abhishek Aryac19bc5ef2018-05-04 22:10:02989 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54990
991
992def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10993 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52994 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01995 #
Max Morozd73e45f2018-04-24 18:32:47996 # "%p" expands out to the process ID. It's not used by this scripts due to:
997 # 1) If a target program spawns too many processess, it may exhaust all disk
998 # space available. For example, unit_tests writes thousands of .profraw
999 # files each of size 1GB+.
1000 # 2) If a target binary uses shared libraries, coverage profile data for them
1001 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011002 #
Yuke Liaod4a9865202018-01-12 23:17:521003 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1004 # is specified, the runtime creates a pool of N raw profiles which are used
1005 # for on-line profile merging. The runtime takes care of selecting a raw
1006 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521007 # N must be between 1 and 9. The merge pool specifier can only occur once per
1008 # filename pattern.
1009 #
Max Morozd73e45f2018-04-24 18:32:471010 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011011 #
Max Morozd73e45f2018-04-24 18:32:471012 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1013 # but it's not too big to consume too much computing resource or disk space.
1014 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591015 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011016 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481017 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541018 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541019
Yuke Liaoa0c8c2f2018-02-28 20:14:101020 try:
Max Moroz7c5354f2018-05-06 00:03:481021 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101022 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291023 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481024 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191025 env={
1026 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1027 'PATH': _GetPathWithLLVMSymbolizerDir()
1028 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101029 except subprocess.CalledProcessError as e:
1030 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191031 logging.warning(
1032 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1033 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101034
1035 return output
1036
1037
Yuke Liao27349c92018-03-22 21:10:011038def _IsFuzzerTarget(target):
1039 """Returns true if the target is a fuzzer target."""
1040 build_args = _GetBuildArgs()
1041 use_libfuzzer = ('use_libfuzzer' in build_args and
1042 build_args['use_libfuzzer'] == 'true')
1043 return use_libfuzzer and target.endswith('_fuzzer')
1044
1045
Yuke Liaob2926832018-03-02 17:34:291046def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101047 """Runs a single iOS command and generates a profraw data file.
1048
1049 iOS application doesn't have write access to folders outside of the app, so
1050 it's impossible to instruct the app to flush the profraw data file to the
1051 desired location. The profraw data file will be generated somewhere within the
1052 application's Documents folder, and the full path can be obtained by parsing
1053 the output.
1054 """
Yuke Liaob2926832018-03-02 17:34:291055 assert _IsIOSCommand(command)
1056
1057 # After running tests, iossim generates a profraw data file, it won't be
1058 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1059 # checkout.
1060 iossim_profraw_file_path = os.path.join(
1061 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101062
1063 try:
Yuke Liaob2926832018-03-02 17:34:291064 output = subprocess.check_output(
1065 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191066 env={
1067 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1068 'PATH': _GetPathWithLLVMSymbolizerDir()
1069 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101070 except subprocess.CalledProcessError as e:
1071 # iossim emits non-zero return code even if tests run successfully, so
1072 # ignore the return code.
1073 output = e.output
1074
1075 return output
1076
1077
1078def _GetProfrawDataFileByParsingOutput(output):
1079 """Returns the path to the profraw data file obtained by parsing the output.
1080
1081 The output of running the test target has no format, but it is guaranteed to
1082 have a single line containing the path to the generated profraw data file.
1083 NOTE: This should only be called when target os is iOS.
1084 """
Yuke Liaob2926832018-03-02 17:34:291085 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101086
Yuke Liaob2926832018-03-02 17:34:291087 output_by_lines = ''.join(output).splitlines()
1088 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101089
1090 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291091 result = profraw_file_pattern.match(line)
1092 if result:
1093 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101094
1095 assert False, ('No profraw data file was generated, did you call '
1096 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1097 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541098
1099
Abhishek Aryac19bc5ef2018-05-04 22:10:021100def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1101 """Returns a relative path to coverage profdata file by merging target
1102 profdata files.
Yuke Liao506e8822017-12-04 16:52:541103
1104 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021105 profdata_file_paths: A list of relative paths to the profdata data files
1106 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541107
1108 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021109 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541110
1111 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021112 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541113 """
Abhishek Aryafb70b532018-05-06 17:47:401114 logging.info('Creating the coverage profile data file.')
1115 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481116 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541117 try:
Abhishek Arya1ec832c2017-12-05 18:06:591118 subprocess_cmd = [
1119 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1120 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021121 subprocess_cmd.extend(profdata_file_paths)
1122 subprocess.check_call(subprocess_cmd)
1123 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041124 logging.error(
1125 'Failed to merge target profdata files to create coverage profdata. %s',
1126 FILE_BUG_MESSAGE)
Abhishek Aryac19bc5ef2018-05-04 22:10:021127 raise error
1128
Abhishek Aryafb70b532018-05-06 17:47:401129 logging.debug('Finished merging target profdata files.')
1130 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021131 profdata_file_path)
1132 return profdata_file_path
1133
1134
1135def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1136 """Returns a relative path to target profdata file by merging target
1137 profraw files.
1138
1139 Args:
1140 profraw_file_paths: A list of relative paths to the profdata data files
1141 that are to be merged.
1142
1143 Returns:
1144 A relative path to the merged coverage profdata file.
1145
1146 Raises:
1147 CalledProcessError: An error occurred merging profdata files.
1148 """
Abhishek Aryafb70b532018-05-06 17:47:401149 logging.info('Creating target profile data file.')
1150 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021151 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1152
1153 try:
1154 subprocess_cmd = [
1155 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1156 ]
Yuke Liao506e8822017-12-04 16:52:541157 subprocess_cmd.extend(profraw_file_paths)
1158 subprocess.check_call(subprocess_cmd)
1159 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041160 logging.error(
1161 'Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541162 raise error
1163
Abhishek Aryafb70b532018-05-06 17:47:401164 logging.debug('Finished merging target profraw files.')
1165 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101166 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541167 return profdata_file_path
1168
1169
Yuke Liao0e4c8682018-04-18 21:06:591170def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1171 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331172 """Generates per file coverage summary using "llvm-cov export" command."""
1173 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1174 # [[-object BIN]] [SOURCES].
1175 # NOTE: For object files, the first one is specified as a positional argument,
1176 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101177 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401178 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331179 subprocess_cmd = [
1180 LLVM_COV_PATH, 'export', '-summary-only',
1181 '-instr-profile=' + profdata_file_path, binary_paths[0]
1182 ]
1183 subprocess_cmd.extend(
1184 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291185 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331186 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591187 if ignore_filename_regex:
1188 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331189
Max Moroz7c5354f2018-05-06 00:03:481190 export_output = subprocess.check_output(subprocess_cmd)
1191
1192 # Write output on the disk to be used by code coverage bot.
1193 with open(_GetSummaryFilePath(), 'w') as f:
1194 f.write(export_output)
1195
1196 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331197 assert len(json_output['data']) == 1
1198 files_coverage_data = json_output['data'][0]['files']
1199
1200 per_file_coverage_summary = {}
1201 for file_coverage_data in files_coverage_data:
1202 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401203 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1204 'File path "%s" in coverage summary is outside source checkout.' %
1205 file_path)
Yuke Liaoea228d02018-01-05 19:10:331206
Abhishek Aryafb70b532018-05-06 17:47:401207 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331208 if summary['lines']['count'] == 0:
1209 continue
1210
1211 per_file_coverage_summary[file_path] = _CoverageSummary(
1212 regions_total=summary['regions']['count'],
1213 regions_covered=summary['regions']['covered'],
1214 functions_total=summary['functions']['count'],
1215 functions_covered=summary['functions']['covered'],
1216 lines_total=summary['lines']['count'],
1217 lines_covered=summary['lines']['covered'])
1218
Abhishek Aryafb70b532018-05-06 17:47:401219 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331220 return per_file_coverage_summary
1221
1222
Yuke Liaob2926832018-03-02 17:34:291223def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1224 """Appends -arch arguments to the command list if it's ios platform.
1225
1226 iOS binaries are universal binaries, and require specifying the architecture
1227 to use, and one architecture needs to be specified for each binary.
1228 """
1229 if _IsIOS():
1230 cmd_list.extend(['-arch=x86_64'] * num_archs)
1231
1232
Yuke Liao506e8822017-12-04 16:52:541233def _GetBinaryPath(command):
1234 """Returns a relative path to the binary to be run by the command.
1235
Yuke Liao545db322018-02-15 17:12:011236 Currently, following types of commands are supported (e.g. url_unittests):
1237 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1238 2. Use xvfb.
1239 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1240 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371241 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1242 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101243 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371244 <iossim_arguments> -c <app_arguments>
1245 out/Coverage-iphonesimulator/url_unittests.app"
1246
Yuke Liao545db322018-02-15 17:12:011247
Yuke Liao506e8822017-12-04 16:52:541248 Args:
1249 command: A command used to run a target.
1250
1251 Returns:
1252 A relative path to the binary.
1253 """
Yuke Liao545db322018-02-15 17:12:011254 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1255
Yuke Liaob2926832018-03-02 17:34:291256 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011257 if os.path.basename(command_parts[0]) == 'python':
1258 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401259 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011260 return command_parts[2]
1261
1262 if os.path.basename(command_parts[0]) == xvfb_script_name:
1263 return command_parts[1]
1264
Yuke Liaob2926832018-03-02 17:34:291265 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101266 # For a given application bundle, the binary resides in the bundle and has
1267 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371268 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101269 app_name = os.path.splitext(os.path.basename(app_path))[0]
1270 return os.path.join(app_path, app_name)
1271
Yuke Liaob2926832018-03-02 17:34:291272 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541273
1274
Yuke Liaob2926832018-03-02 17:34:291275def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101276 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291277 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101278
1279
Yuke Liao95d13d72017-12-07 18:18:501280def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1281 """Verifies that the target executables specified in the commands are inside
1282 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541283 for command in commands:
1284 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501285 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481286 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501287 'Target executable "%s" in command: "%s" is outside of '
1288 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541289
1290
1291def _ValidateBuildingWithClangCoverage():
1292 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201293 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541294
1295 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1296 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591297 assert False, ('\'{} = true\' is required in args.gn.'
1298 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541299
1300
Yuke Liaoc60b2d02018-03-02 21:40:431301def _ValidateCurrentPlatformIsSupported():
1302 """Asserts that this script suports running on the current platform"""
1303 target_os = _GetTargetOS()
1304 if target_os:
1305 current_platform = target_os
1306 else:
1307 current_platform = _GetHostPlatform()
1308
1309 assert current_platform in [
1310 'linux', 'mac', 'chromeos', 'ios'
1311 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1312
1313
Yuke Liao80afff32018-03-07 01:26:201314def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541315 """Parses args.gn file and returns results as a dictionary.
1316
1317 Returns:
1318 A dictionary representing the build args.
1319 """
Yuke Liao80afff32018-03-07 01:26:201320 global _BUILD_ARGS
1321 if _BUILD_ARGS is not None:
1322 return _BUILD_ARGS
1323
1324 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541325 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1326 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1327 'missing args.gn file.' % BUILD_DIR)
1328 with open(build_args_path) as build_args_file:
1329 build_args_lines = build_args_file.readlines()
1330
Yuke Liao506e8822017-12-04 16:52:541331 for build_arg_line in build_args_lines:
1332 build_arg_without_comments = build_arg_line.split('#')[0]
1333 key_value_pair = build_arg_without_comments.split('=')
1334 if len(key_value_pair) != 2:
1335 continue
1336
1337 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431338
1339 # Values are wrapped within a pair of double-quotes, so remove the leading
1340 # and trailing double-quotes.
1341 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201342 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541343
Yuke Liao80afff32018-03-07 01:26:201344 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541345
1346
Abhishek Arya16f059a2017-12-07 17:47:321347def _VerifyPathsAndReturnAbsolutes(paths):
1348 """Verifies that the paths specified in |paths| exist and returns absolute
1349 versions.
Yuke Liao66da1732017-12-05 22:19:421350
1351 Args:
1352 paths: A list of files or directories.
1353 """
Abhishek Arya16f059a2017-12-07 17:47:321354 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421355 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321356 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1357 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1358
1359 absolute_paths.append(absolute_path)
1360
1361 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421362
1363
Yuke Liaodd1ec0592018-02-02 01:26:371364def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1365 """Returns a target path relative to the directory of base_path.
1366
1367 This method requires base_path to be a file, otherwise, one should call
1368 os.path.relpath directly.
1369 """
1370 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141371 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371372 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141373 base_dir = os.path.dirname(base_path)
1374 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371375
1376
Abhishek Arya64636af2018-05-04 14:42:131377def _GetBinaryPathsFromTargets(targets, build_dir):
1378 """Return binary paths from target names."""
1379 # FIXME: Derive output binary from target build definitions rather than
1380 # assuming that it is always the same name.
1381 binary_paths = []
1382 for target in targets:
1383 binary_path = os.path.join(build_dir, target)
1384 if _GetHostPlatform() == 'win':
1385 binary_path += '.exe'
1386
1387 if os.path.exists(binary_path):
1388 binary_paths.append(binary_path)
1389 else:
1390 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401391 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131392 os.path.basename(binary_path))
1393
1394 return binary_paths
1395
1396
Yuke Liao506e8822017-12-04 16:52:541397def _ParseCommandArguments():
1398 """Adds and parses relevant arguments for tool comands.
1399
1400 Returns:
1401 A dictionary representing the arguments.
1402 """
1403 arg_parser = argparse.ArgumentParser()
1404 arg_parser.usage = __doc__
1405
Abhishek Arya1ec832c2017-12-05 18:06:591406 arg_parser.add_argument(
1407 '-b',
1408 '--build-dir',
1409 type=str,
1410 required=True,
1411 help='The build directory, the path needs to be relative to the root of '
1412 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541413
Abhishek Arya1ec832c2017-12-05 18:06:591414 arg_parser.add_argument(
1415 '-o',
1416 '--output-dir',
1417 type=str,
1418 required=True,
1419 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541420
Abhishek Arya1ec832c2017-12-05 18:06:591421 arg_parser.add_argument(
1422 '-c',
1423 '--command',
1424 action='append',
Abhishek Arya64636af2018-05-04 14:42:131425 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591426 help='Commands used to run test targets, one test target needs one and '
1427 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131428 'current working directory is the root of the checkout. This option is '
1429 'incompatible with -p/--profdata-file option.')
1430
1431 arg_parser.add_argument(
1432 '-p',
1433 '--profdata-file',
1434 type=str,
1435 required=False,
1436 help='Path to profdata file to use for generating code coverage reports. '
1437 'This can be useful if you generated the profdata file seperately in '
1438 'your own test harness. This option is ignored if run command(s) are '
1439 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541440
Abhishek Arya1ec832c2017-12-05 18:06:591441 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421442 '-f',
1443 '--filters',
1444 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321445 required=False,
Yuke Liao66da1732017-12-05 22:19:421446 help='Directories or files to get code coverage for, and all files under '
1447 'the directories are included recursively.')
1448
1449 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591450 '-i',
1451 '--ignore-filename-regex',
1452 type=str,
1453 help='Skip source code files with file paths that match the given '
1454 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1455 'to exclude files in third_party/ and out/ folders from the report.')
1456
1457 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591458 '-j',
1459 '--jobs',
1460 type=int,
1461 default=None,
1462 help='Run N jobs to build in parallel. If not specified, a default value '
1463 'will be derived based on CPUs availability. Please refer to '
1464 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541465
Abhishek Arya1ec832c2017-12-05 18:06:591466 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101467 '-v',
1468 '--verbose',
1469 action='store_true',
1470 help='Prints additional output for diagnostics.')
1471
1472 arg_parser.add_argument(
1473 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1474
1475 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021476 'targets',
1477 nargs='+',
1478 help='The names of the test targets to run. If multiple run commands are '
1479 'specified using the -c/--command option, then the order of targets and '
1480 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541481
1482 args = arg_parser.parse_args()
1483 return args
1484
1485
1486def Main():
1487 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131488 # Change directory to source root to aid in relative paths calculations.
1489 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111490
Abhishek Arya64636af2018-05-04 14:42:131491 # Setup coverage binaries even when script is called with empty params. This
1492 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111493 DownloadCoverageToolsIfNeeded()
1494
Yuke Liao506e8822017-12-04 16:52:541495 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131496 _ConfigureLogging(args)
1497
Yuke Liao506e8822017-12-04 16:52:541498 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481499 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541500 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481501 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541502
Abhishek Arya64636af2018-05-04 14:42:131503 assert args.command or args.profdata_file, (
1504 'Need to either provide commands to run using -c/--command option OR '
1505 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431506
Abhishek Arya64636af2018-05-04 14:42:131507 assert not args.command or (len(args.targets) == len(args.command)), (
1508 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431509
Abhishek Arya1ec832c2017-12-05 18:06:591510 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401511 'Build directory: "%s" doesn\'t exist. '
1512 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131513
Yuke Liaoc60b2d02018-03-02 21:40:431514 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541515 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321516
1517 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421518 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321519 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421520
Max Moroz7c5354f2018-05-06 00:03:481521 if not os.path.exists(_GetCoverageReportRootDirPath()):
1522 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541523
Abhishek Arya64636af2018-05-04 14:42:131524 # Get profdate file and list of binary paths.
1525 if args.command:
1526 # A list of commands are provided. Run them to generate profdata file, and
1527 # create a list of binary paths from parsing commands.
1528 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1529 profdata_file_path = _CreateCoverageProfileDataForTargets(
1530 args.targets, args.command, args.jobs)
1531 binary_paths = [_GetBinaryPath(command) for command in args.command]
1532 else:
1533 # An input prof-data file is already provided. Just calculate binary paths.
1534 profdata_file_path = args.profdata_file
1535 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331536
Abhishek Arya78120bc2018-05-07 20:53:541537 binary_paths.extend(_GetSharedLibraries(binary_paths))
1538
Yuke Liao481d3482018-01-29 19:17:101539 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401540 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371541 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591542 binary_paths, profdata_file_path, absolute_filter_paths,
1543 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371544 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591545 absolute_filter_paths,
1546 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371547 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1548
1549 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1550 per_file_coverage_summary)
1551 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1552 per_file_coverage_summary)
1553 _GenerateDirectoryViewHtmlIndexFile()
1554
1555 component_to_directories = _ExtractComponentToDirectoriesMapping()
1556 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1557 component_to_directories, per_directory_coverage_summary)
1558 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1559 component_to_directories,
1560 per_directory_coverage_summary)
1561 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331562
1563 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371564 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331565 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481566 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331567
Max Moroz7c5354f2018-05-06 00:03:481568 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401569 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101570 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541571
Abhishek Arya1ec832c2017-12-05 18:06:591572
Yuke Liao506e8822017-12-04 16:52:541573if __name__ == '__main__':
1574 sys.exit(Main())