blob: 2b5128ae06c5baf73a3b3f37d0218c04a470f21a [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
Yuke Liao082e99632018-05-18 15:40:4088# Absolute path to the code coverage tools binary. These paths can be
89# overwritten by user specified coverage tool paths.
Yuke Liao506e8822017-12-04 16:52:5490LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:1991LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
92LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
93LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:5494
95# Build directory, the value is parsed from command line arguments.
96BUILD_DIR = None
97
98# Output directory for generated artifacts, the value is parsed from command
99# line arguemnts.
100OUTPUT_DIR = None
101
102# Default number of jobs used to build when goma is configured and enabled.
103DEFAULT_GOMA_JOBS = 100
104
105# Name of the file extension for profraw data files.
106PROFRAW_FILE_EXTENSION = 'profraw'
107
108# Name of the final profdata file, and this file needs to be passed to
109# "llvm-cov" command in order to call "llvm-cov show" to inspect the
110# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48111PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
112
113# Name of the file with summary information generated by llvm-cov export.
114SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54115
116# Build arg required for generating code coverage data.
117CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
118
Yuke Liaoea228d02018-01-05 19:10:33119# The default name of the html coverage report for a directory.
120DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
121
Yuke Liaodd1ec0592018-02-02 01:26:37122# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37123COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48124DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37125FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48126INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
127
128LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37129
130# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19131COMPONENT_MAPPING_URL = (
132 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37133
Yuke Liao80afff32018-03-07 01:26:20134# Caches the results returned by _GetBuildArgs, don't use this variable
135# directly, call _GetBuildArgs instead.
136_BUILD_ARGS = None
137
Abhishek Aryac19bc5ef2018-05-04 22:10:02138# Retry failed merges.
139MERGE_RETRIES = 3
140
Abhishek Aryad35de7e2018-05-10 22:23:04141# Message to guide user to file a bug when everything else fails.
142FILE_BUG_MESSAGE = (
143 'If it persists, please file a bug with the command you used, git revision '
144 'and args.gn config here: '
145 'https://bugs.chromium.org/p/chromium/issues/entry?'
146 'components=Tools%3ECodeCoverage')
147
Yuke Liaoea228d02018-01-05 19:10:33148
149class _CoverageSummary(object):
150 """Encapsulates coverage summary representation."""
151
Yuke Liaodd1ec0592018-02-02 01:26:37152 def __init__(self,
153 regions_total=0,
154 regions_covered=0,
155 functions_total=0,
156 functions_covered=0,
157 lines_total=0,
158 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33159 """Initializes _CoverageSummary object."""
160 self._summary = {
161 'regions': {
162 'total': regions_total,
163 'covered': regions_covered
164 },
165 'functions': {
166 'total': functions_total,
167 'covered': functions_covered
168 },
169 'lines': {
170 'total': lines_total,
171 'covered': lines_covered
172 }
173 }
174
175 def Get(self):
176 """Returns summary as a dictionary."""
177 return self._summary
178
179 def AddSummary(self, other_summary):
180 """Adds another summary to this one element-wise."""
181 for feature in self._summary:
182 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
183 self._summary[feature]['covered'] += other_summary.Get()[feature][
184 'covered']
185
186
Yuke Liaodd1ec0592018-02-02 01:26:37187class _CoverageReportHtmlGenerator(object):
188 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33189
Yuke Liaodd1ec0592018-02-02 01:26:37190 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33191 """
192
Yuke Liaodd1ec0592018-02-02 01:26:37193 def __init__(self, output_path, table_entry_type):
194 """Initializes _CoverageReportHtmlGenerator object.
195
196 Args:
197 output_path: Path to the html report that will be generated.
198 table_entry_type: Type of the table entries to be displayed in the table
199 header. For example: 'Path', 'Component'.
200 """
Yuke Liaoea228d02018-01-05 19:10:33201 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48202 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33203 assert os.path.exists(css_absolute_path), (
204 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40205 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33206 css_absolute_path)
207
208 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37209 self._output_path = output_path
210 self._table_entry_type = table_entry_type
211
Yuke Liaoea228d02018-01-05 19:10:33212 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12213 self._total_entry = {}
Abhishek Arya302b67a2018-05-10 19:43:23214
215 source_dir = os.path.dirname(os.path.realpath(__file__))
216 template_dir = os.path.join(source_dir, 'html_templates')
Yuke Liaoea228d02018-01-05 19:10:33217
218 jinja_env = jinja2.Environment(
219 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
220 self._header_template = jinja_env.get_template('header.html')
221 self._table_template = jinja_env.get_template('table.html')
222 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya302b67a2018-05-10 19:43:23223
Abhishek Arya865fffd2018-05-08 22:16:01224 self._style_overrides = open(
Abhishek Arya302b67a2018-05-10 19:43:23225 os.path.join(source_dir, 'static', 'css', 'style.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33226
227 def AddLinkToAnotherReport(self, html_report_path, name, summary):
228 """Adds a link to another html report in this report.
229
230 The link to be added is assumed to be an entry in this directory.
231 """
Yuke Liaodd1ec0592018-02-02 01:26:37232 # Use relative paths instead of absolute paths to make the generated reports
233 # portable.
234 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
235 html_report_path, self._output_path)
236
Yuke Liaod54030e2018-01-08 17:34:12237 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37238 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12239 os.path.basename(html_report_path) ==
240 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
241 self._table_entries.append(table_entry)
242
243 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35244 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12245 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
246
247 def _CreateTableEntryFromCoverageSummary(self,
248 summary,
249 href=None,
250 name=None,
251 is_dir=None):
252 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37253 assert (href is None and name is None and is_dir is None) or (
254 href is not None and name is not None and is_dir is not None), (
255 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35256 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37257 'attributes must be None.')
258
Yuke Liaod54030e2018-01-08 17:34:12259 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37260 if href is not None:
261 entry['href'] = href
262 if name is not None:
263 entry['name'] = name
264 if is_dir is not None:
265 entry['is_dir'] = is_dir
266
Yuke Liaoea228d02018-01-05 19:10:33267 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12268 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37269 if summary_dict[feature]['total'] == 0:
270 percentage = 0.0
271 else:
Yuke Liao0e4c8682018-04-18 21:06:59272 percentage = float(summary_dict[feature]
273 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35274
Yuke Liaoea228d02018-01-05 19:10:33275 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12276 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33277 'total': summary_dict[feature]['total'],
278 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35279 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33280 'color_class': color_class
281 }
Yuke Liaod54030e2018-01-08 17:34:12282
Yuke Liaod54030e2018-01-08 17:34:12283 return entry
Yuke Liaoea228d02018-01-05 19:10:33284
285 def _GetColorClass(self, percentage):
286 """Returns the css color class based on coverage percentage."""
287 if percentage >= 0 and percentage < 80:
288 return 'red'
289 if percentage >= 80 and percentage < 100:
290 return 'yellow'
291 if percentage == 100:
292 return 'green'
293
Abhishek Aryafb70b532018-05-06 17:47:40294 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33295
Yuke Liao1b852fd2018-05-11 17:07:32296 def WriteHtmlCoverageReport(self, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37297 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33298
299 In the report, sub-directories are displayed before files and within each
300 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33301 """
302
303 def EntryCmp(left, right):
304 """Compare function for table entries."""
305 if left['is_dir'] != right['is_dir']:
306 return -1 if left['is_dir'] == True else 1
307
Yuke Liaodd1ec0592018-02-02 01:26:37308 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33309
310 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
311
312 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48313
314 directory_view_path = _GetDirectoryViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32315 directory_view_href = _GetRelativePathToDirectoryOfFile(
316 directory_view_path, self._output_path)
Max Moroz7c5354f2018-05-06 00:03:48317 component_view_path = _GetComponentViewPath()
Yuke Liao1b852fd2018-05-11 17:07:32318 component_view_href = _GetRelativePathToDirectoryOfFile(
319 component_view_path, self._output_path)
320
321 # File view is optional in the report.
322 file_view_href = None
323 if not no_file_view:
324 file_view_path = _GetFileViewPath()
325 file_view_href = _GetRelativePathToDirectoryOfFile(
326 file_view_path, self._output_path)
Yuke Liaodd1ec0592018-02-02 01:26:37327
Yuke Liaoea228d02018-01-05 19:10:33328 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37329 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
Yuke Liao1b852fd2018-05-11 17:07:32330 directory_view_href=directory_view_href,
331 component_view_href=component_view_href,
332 file_view_href=file_view_href,
Abhishek Arya865fffd2018-05-08 22:16:01333 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37334
Yuke Liaod54030e2018-01-08 17:34:12335 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37336 entries=self._table_entries,
337 total_entry=self._total_entry,
338 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33339 html_footer = self._footer_template.render()
340
Yuke Liaodd1ec0592018-02-02 01:26:37341 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33342 html_file.write(html_header + html_table + html_footer)
343
Yuke Liao506e8822017-12-04 16:52:54344
Abhishek Arya64636af2018-05-04 14:42:13345def _ConfigureLogging(args):
346 """Configures logging settings for later use."""
347 log_level = logging.DEBUG if args.verbose else logging.INFO
348 log_format = '[%(asctime)s %(levelname)s] %(message)s'
349 log_file = args.log_file if args.log_file else None
350 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
351
352
Yuke Liao082e99632018-05-18 15:40:40353def _ConfigureLLVMCoverageTools(args):
354 """Configures llvm coverage tools."""
355 if args.coverage_tools_dir:
356 llvm_bin_dir = os.path.abspath(args.coverage_tools_dir)
357 global LLVM_COV_PATH
358 global LLVM_PROFDATA_PATH
359 LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
360 LLVM_PROFDATA_PATH = os.path.join(llvm_bin_dir, 'llvm-profdata')
361 else:
362 DownloadCoverageToolsIfNeeded()
363
364 coverage_tools_exist = (
365 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
366 assert coverage_tools_exist, ('Cannot find coverage tools, please make sure '
367 'both \'%s\' and \'%s\' exist.') % (
368 LLVM_COV_PATH, LLVM_PROFDATA_PATH)
369
370
Max Morozd73e45f2018-04-24 18:32:47371def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54372 """Returns list of shared libraries used by specified binaries."""
373 logging.info('Finding shared libraries for targets (if any).')
374 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47375 cmd = []
376 shared_library_re = None
377
378 if sys.platform.startswith('linux'):
379 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13380 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
381 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47382 elif sys.platform.startswith('darwin'):
383 cmd.extend(['otool', '-L'])
384 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
385 else:
Abhishek Aryafb70b532018-05-06 17:47:40386 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47387
388 assert shared_library_re is not None
389
390 cmd.extend(binary_paths)
391 output = subprocess.check_output(cmd)
392
393 for line in output.splitlines():
394 m = shared_library_re.match(line)
395 if not m:
396 continue
397
398 shared_library_path = m.group(1)
399 if sys.platform.startswith('darwin'):
400 # otool outputs "@rpath" macro instead of the dirname of the given binary.
401 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
402
Abhishek Arya78120bc2018-05-07 20:53:54403 if shared_library_path in shared_libraries:
404 continue
405
Max Morozd73e45f2018-04-24 18:32:47406 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
407 'the given target(s) does not '
408 'exist.' % shared_library_path)
409 with open(shared_library_path) as f:
410 data = f.read()
411
412 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
413 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54414 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47415
Abhishek Arya78120bc2018-05-07 20:53:54416 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
417 shared_libraries)
418 logging.info('Finished finding shared libraries for targets.')
419 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47420
421
Yuke Liaoc60b2d02018-03-02 21:40:43422def _GetHostPlatform():
423 """Returns the host platform.
424
425 This is separate from the target platform/os that coverage is running for.
426 """
Abhishek Arya1ec832c2017-12-05 18:06:59427 if sys.platform == 'win32' or sys.platform == 'cygwin':
428 return 'win'
429 if sys.platform.startswith('linux'):
430 return 'linux'
431 else:
432 assert sys.platform == 'darwin'
433 return 'mac'
434
435
Abhishek Arya1c97ea542018-05-10 03:53:19436def _GetPathWithLLVMSymbolizerDir():
437 """Add llvm-symbolizer directory to path for symbolized stacks."""
438 path = os.getenv('PATH')
439 dirs = path.split(os.pathsep)
440 if LLVM_BIN_DIR in dirs:
441 return path
442
443 return path + os.pathsep + LLVM_BIN_DIR
444
445
Yuke Liaoc60b2d02018-03-02 21:40:43446def _GetTargetOS():
447 """Returns the target os specified in args.gn file.
448
449 Returns an empty string is target_os is not specified.
450 """
Yuke Liao80afff32018-03-07 01:26:20451 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43452 return build_args['target_os'] if 'target_os' in build_args else ''
453
454
Yuke Liaob2926832018-03-02 17:34:29455def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10456 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43457 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10458
459
Yuke Liao506e8822017-12-04 16:52:54460# TODO(crbug.com/759794): remove this function once tools get included to
461# Clang bundle:
462# https://chromium-review.googlesource.com/c/chromium/src/+/688221
463def DownloadCoverageToolsIfNeeded():
464 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59465
Yuke Liaoc60b2d02018-03-02 21:40:43466 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54467 """Returns a pair of revision number by reading the build stamp file.
468
469 Args:
470 stamp_file_path: A path the build stamp file created by
471 tools/clang/scripts/update.py.
472 Returns:
473 A pair of integers represeting the main and sub revision respectively.
474 """
475 if not os.path.exists(stamp_file_path):
476 return 0, 0
477
478 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43479 stamp_file_line = stamp_file.readline()
480 if ',' in stamp_file_line:
481 package_version = stamp_file_line.rstrip().split(',')[0]
482 else:
483 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54484
Yuke Liaoc60b2d02018-03-02 21:40:43485 clang_revision_str, clang_sub_revision_str = package_version.split('-')
486 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59487
Yuke Liaoc60b2d02018-03-02 21:40:43488 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54489 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43490 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54491
492 coverage_revision_stamp_file = os.path.join(
493 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
494 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43495 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54496
Yuke Liaoea228d02018-01-05 19:10:33497 has_coverage_tools = (
498 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32499
Yuke Liaoea228d02018-01-05 19:10:33500 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54501 coverage_sub_revision == clang_sub_revision):
502 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43503 return
Yuke Liao506e8822017-12-04 16:52:54504
505 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
506 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
507
508 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43509 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54510 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43511 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54512 coverage_tools_url = (
513 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43514 else:
515 assert host_platform == 'win'
516 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54517
518 try:
519 clang_update.DownloadAndUnpack(coverage_tools_url,
520 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54521 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43522 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54523 file_handle.write('\n')
524 except urllib2.URLError:
525 raise Exception(
526 'Failed to download coverage tools: %s.' % coverage_tools_url)
527
528
Yuke Liaodd1ec0592018-02-02 01:26:37529def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59530 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54531 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
532
533 For a file with absolute path /a/b/x.cc, a html report is generated as:
534 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
535 OUTPUT_DIR/index.html.
536
537 Args:
538 binary_paths: A list of paths to the instrumented binaries.
539 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42540 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54541 """
Yuke Liao506e8822017-12-04 16:52:54542 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
543 # [[-object BIN]] [SOURCES]
544 # NOTE: For object files, the first one is specified as a positional argument,
545 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10546 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40547 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59548 subprocess_cmd = [
549 LLVM_COV_PATH, 'show', '-format=html',
550 '-output-dir={}'.format(OUTPUT_DIR),
551 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
552 ]
553 subprocess_cmd.extend(
554 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29555 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Ryan Sleeviae19b2c32018-05-15 22:36:17556 if _GetHostPlatform() in ['linux', 'mac']:
557 subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
Yuke Liao66da1732017-12-05 22:19:42558 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59559 if ignore_filename_regex:
560 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
561
Yuke Liao506e8822017-12-04 16:52:54562 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34563
564 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
565 # the platform name instead, as it simplifies the report dir structure when
566 # the same report is generated for different platforms.
567 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48568 platform_report_subdir_path = _GetCoverageReportRootDirPath()
569 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34570
Abhishek Aryafb70b532018-05-06 17:47:40571 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54572
573
Yuke Liaodd1ec0592018-02-02 01:26:37574def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
575 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48576 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37577 logging.debug('Generating file view html index file as: "%s".',
578 file_view_index_file_path)
579 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
580 'Path')
581 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33582
Yuke Liaodd1ec0592018-02-02 01:26:37583 for file_path in per_file_coverage_summary:
584 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
585
586 html_generator.AddLinkToAnotherReport(
587 _GetCoverageHtmlReportPathForFile(file_path),
588 os.path.relpath(file_path, SRC_ROOT_PATH),
589 per_file_coverage_summary[file_path])
590
591 html_generator.CreateTotalsEntry(totals_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:32592 html_generator.WriteHtmlCoverageReport(no_file_view=False)
Yuke Liaodd1ec0592018-02-02 01:26:37593 logging.debug('Finished generating file view html index file.')
594
595
596def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
597 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40598 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37599 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
600
Yuke Liaoea228d02018-01-05 19:10:33601 for file_path in per_file_coverage_summary:
602 summary = per_file_coverage_summary[file_path]
603 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40604
Yuke Liaoea228d02018-01-05 19:10:33605 while True:
606 per_directory_coverage_summary[parent_dir].AddSummary(summary)
607
608 if parent_dir == SRC_ROOT_PATH:
609 break
610 parent_dir = os.path.dirname(parent_dir)
611
Abhishek Aryafb70b532018-05-06 17:47:40612 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37613 return per_directory_coverage_summary
614
615
Yuke Liao1b852fd2018-05-11 17:07:32616def _GeneratePerDirectoryCoverageInHtml(
617 per_directory_coverage_summary, per_file_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37618 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40619 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33620 for dir_path in per_directory_coverage_summary:
Yuke Liao1b852fd2018-05-11 17:07:32621 _GenerateCoverageInHtmlForDirectory(dir_path,
622 per_directory_coverage_summary,
623 per_file_coverage_summary, no_file_view)
Yuke Liaoea228d02018-01-05 19:10:33624
Abhishek Aryafb70b532018-05-06 17:47:40625 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10626
Yuke Liaoea228d02018-01-05 19:10:33627
628def _GenerateCoverageInHtmlForDirectory(
Yuke Liao1b852fd2018-05-11 17:07:32629 dir_path, per_directory_coverage_summary, per_file_coverage_summary,
630 no_file_view):
Yuke Liaoea228d02018-01-05 19:10:33631 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37632 html_generator = _CoverageReportHtmlGenerator(
633 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33634
635 for entry_name in os.listdir(dir_path):
636 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33637
Yuke Liaodd1ec0592018-02-02 01:26:37638 if entry_path in per_file_coverage_summary:
639 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
640 entry_coverage_summary = per_file_coverage_summary[entry_path]
641 elif entry_path in per_directory_coverage_summary:
642 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
643 entry_path)
644 entry_coverage_summary = per_directory_coverage_summary[entry_path]
645 else:
Yuke Liaoc7e607142018-02-05 20:26:14646 # Any file without executable lines shouldn't be included into the report.
647 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37648 continue
Yuke Liaoea228d02018-01-05 19:10:33649
Yuke Liaodd1ec0592018-02-02 01:26:37650 html_generator.AddLinkToAnotherReport(entry_html_report_path,
651 os.path.basename(entry_path),
652 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33653
Yuke Liaod54030e2018-01-08 17:34:12654 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liao1b852fd2018-05-11 17:07:32655 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37656
657
658def _GenerateDirectoryViewHtmlIndexFile():
659 """Generates the html index file for directory view.
660
661 Note that the index file is already generated under SRC_ROOT_PATH, so this
662 file simply redirects to it, and the reason of this extra layer is for
663 structural consistency with other views.
664 """
Max Moroz7c5354f2018-05-06 00:03:48665 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37666 logging.debug('Generating directory view html index file as: "%s".',
667 directory_view_index_file_path)
668 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
669 SRC_ROOT_PATH)
670 _WriteRedirectHtmlFile(directory_view_index_file_path,
671 src_root_html_report_path)
672 logging.debug('Finished generating directory view html index file.')
673
674
675def _CalculatePerComponentCoverageSummary(component_to_directories,
676 per_directory_coverage_summary):
677 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40678 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37679 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
680
681 for component in component_to_directories:
682 for directory in component_to_directories[component]:
683 absolute_directory_path = os.path.abspath(directory)
684 if absolute_directory_path in per_directory_coverage_summary:
685 per_component_coverage_summary[component].AddSummary(
686 per_directory_coverage_summary[absolute_directory_path])
687
Abhishek Aryafb70b532018-05-06 17:47:40688 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37689 return per_component_coverage_summary
690
691
692def _ExtractComponentToDirectoriesMapping():
693 """Returns a mapping from components to directories."""
694 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
695 directory_to_component = component_mappings['dir-to-component']
696
697 component_to_directories = defaultdict(list)
Abhishek Arya8c3a1ce322018-05-13 04:14:01698 for directory in sorted(directory_to_component):
Yuke Liaodd1ec0592018-02-02 01:26:37699 component = directory_to_component[directory]
Abhishek Arya8c3a1ce322018-05-13 04:14:01700
701 # Check if we already added the parent directory of this directory. If yes,
702 # skip this sub-directory to avoid double-counting.
703 found_parent_directory = False
704 for component_directory in component_to_directories[component]:
705 if directory.startswith(component_directory + '/'):
706 found_parent_directory = True
707 break
708
709 if not found_parent_directory:
710 component_to_directories[component].append(directory)
Yuke Liaodd1ec0592018-02-02 01:26:37711
712 return component_to_directories
713
714
Yuke Liao1b852fd2018-05-11 17:07:32715def _GeneratePerComponentCoverageInHtml(
716 per_component_coverage_summary, component_to_directories,
717 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37718 """Generates per-component coverage reports in html."""
719 logging.debug('Writing per-component coverage html reports.')
720 for component in per_component_coverage_summary:
721 _GenerateCoverageInHtmlForComponent(
722 component, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32723 per_directory_coverage_summary, no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37724
725 logging.debug('Finished writing per-component coverage html reports.')
726
727
728def _GenerateCoverageInHtmlForComponent(
729 component_name, per_component_coverage_summary, component_to_directories,
Yuke Liao1b852fd2018-05-11 17:07:32730 per_directory_coverage_summary, no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37731 """Generates coverage html report for a component."""
732 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
733 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14734 component_html_report_dir = os.path.dirname(component_html_report_path)
735 if not os.path.exists(component_html_report_dir):
736 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37737
738 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
739 'Path')
740
741 for dir_path in component_to_directories[component_name]:
742 dir_absolute_path = os.path.abspath(dir_path)
743 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14744 # Any directory without an excercised file shouldn't be included into the
745 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37746 continue
747
748 html_generator.AddLinkToAnotherReport(
749 _GetCoverageHtmlReportPathForDirectory(dir_path),
750 os.path.relpath(dir_path, SRC_ROOT_PATH),
751 per_directory_coverage_summary[dir_absolute_path])
752
753 html_generator.CreateTotalsEntry(
754 per_component_coverage_summary[component_name])
Yuke Liao1b852fd2018-05-11 17:07:32755 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:37756
757
Yuke Liao1b852fd2018-05-11 17:07:32758def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
759 no_file_view):
Yuke Liaodd1ec0592018-02-02 01:26:37760 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48761 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37762 logging.debug('Generating component view html index file as: "%s".',
763 component_view_index_file_path)
764 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
765 'Component')
Yuke Liaodd1ec0592018-02-02 01:26:37766 for component in per_component_coverage_summary:
Yuke Liaodd1ec0592018-02-02 01:26:37767 html_generator.AddLinkToAnotherReport(
768 _GetCoverageHtmlReportPathForComponent(component), component,
769 per_component_coverage_summary[component])
770
Abhishek Aryaefbe1df2018-05-14 20:19:48771 # Do not create a totals row for the component view as the value is incorrect
772 # due to failure to account for UNKNOWN component and some paths belonging to
773 # multiple components.
774
Yuke Liao1b852fd2018-05-11 17:07:32775 html_generator.WriteHtmlCoverageReport(no_file_view)
Yuke Liaoc7e607142018-02-05 20:26:14776 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33777
778
Max Moroz7c5354f2018-05-06 00:03:48779def _MergeTwoDirectories(src_path, dst_path):
780 """Merge src_path directory into dst_path directory."""
781 for filename in os.listdir(src_path):
782 dst_path = os.path.join(dst_path, filename)
783 if os.path.exists(dst_path):
784 shutil.rmtree(dst_path)
785 os.rename(os.path.join(src_path, filename), dst_path)
786 shutil.rmtree(src_path)
787
788
Yuke Liaoea228d02018-01-05 19:10:33789def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37790 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48791 html_index_file_path = _GetHtmlIndexPath()
792 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37793 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
794
795
796def _WriteRedirectHtmlFile(from_html_path, to_html_path):
797 """Writes a html file that redirects to another html file."""
798 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
799 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33800 content = ("""
801 <!DOCTYPE html>
802 <html>
803 <head>
804 <!-- HTML meta refresh URL redirection -->
805 <meta http-equiv="refresh" content="0; url=%s">
806 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37807 </html>""" % to_html_relative_path)
808 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33809 f.write(content)
810
811
Max Moroz7c5354f2018-05-06 00:03:48812def _CleanUpOutputDir():
813 """Perform a cleanup of the output dir."""
814 # Remove the default index.html file produced by llvm-cov.
815 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
816 if os.path.exists(index_path):
817 os.remove(index_path)
818
819
Yuke Liaodd1ec0592018-02-02 01:26:37820def _GetCoverageHtmlReportPathForFile(file_path):
821 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40822 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37823 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
824
825 # '+' is used instead of os.path.join because both of them are absolute paths
826 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14827 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37828 return _GetCoverageReportRootDirPath() + html_report_path
829
830
831def _GetCoverageHtmlReportPathForDirectory(dir_path):
832 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40833 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37834 html_report_path = os.path.join(
835 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
836
837 # '+' is used instead of os.path.join because both of them are absolute paths
838 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14839 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37840 return _GetCoverageReportRootDirPath() + html_report_path
841
842
843def _GetCoverageHtmlReportPathForComponent(component_name):
844 """Given a component, returns the corresponding html report path."""
845 component_file_name = component_name.lower().replace('>', '-')
846 html_report_name = os.extsep.join([component_file_name, 'html'])
847 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
848 html_report_name)
849
850
851def _GetCoverageReportRootDirPath():
852 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48853 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
854
855
856def _GetComponentViewPath():
857 """Path to the HTML file for the component view."""
858 return os.path.join(_GetCoverageReportRootDirPath(),
859 COMPONENT_VIEW_INDEX_FILE)
860
861
862def _GetDirectoryViewPath():
863 """Path to the HTML file for the directory view."""
864 return os.path.join(_GetCoverageReportRootDirPath(),
865 DIRECTORY_VIEW_INDEX_FILE)
866
867
868def _GetFileViewPath():
869 """Path to the HTML file for the file view."""
870 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
871
872
873def _GetLogsDirectoryPath():
874 """Path to the logs directory."""
875 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
876
877
878def _GetHtmlIndexPath():
879 """Path to the main HTML index file."""
880 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
881
882
883def _GetProfdataFilePath():
884 """Path to the resulting .profdata file."""
885 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
886
887
888def _GetSummaryFilePath():
889 """The JSON file that contains coverage summary written by llvm-cov export."""
890 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33891
892
Yuke Liao506e8822017-12-04 16:52:54893def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
894 """Builds and runs target to generate the coverage profile data.
895
896 Args:
897 targets: A list of targets to build with coverage instrumentation.
898 commands: A list of commands used to run the targets.
899 jobs_count: Number of jobs to run in parallel for building. If None, a
900 default value is derived based on CPUs availability.
901
902 Returns:
903 A relative path to the generated profdata file.
904 """
905 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02906 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59907 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02908 coverage_profdata_file_path = (
909 _CreateCoverageProfileDataFromTargetProfDataFiles(
910 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54911
Abhishek Aryac19bc5ef2018-05-04 22:10:02912 for target_profdata_file_path in target_profdata_file_paths:
913 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52914
Abhishek Aryac19bc5ef2018-05-04 22:10:02915 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54916
917
918def _BuildTargets(targets, jobs_count):
919 """Builds target with Clang coverage instrumentation.
920
921 This function requires current working directory to be the root of checkout.
922
923 Args:
924 targets: A list of targets to build with coverage instrumentation.
925 jobs_count: Number of jobs to run in parallel for compilation. If None, a
926 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54927 """
Abhishek Arya1ec832c2017-12-05 18:06:59928
Yuke Liao506e8822017-12-04 16:52:54929 def _IsGomaConfigured():
930 """Returns True if goma is enabled in the gn build args.
931
932 Returns:
933 A boolean indicates whether goma is configured for building or not.
934 """
Yuke Liao80afff32018-03-07 01:26:20935 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54936 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
937
Abhishek Aryafb70b532018-05-06 17:47:40938 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54939 if jobs_count is None and _IsGomaConfigured():
940 jobs_count = DEFAULT_GOMA_JOBS
941
942 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
943 if jobs_count is not None:
944 subprocess_cmd.append('-j' + str(jobs_count))
945
946 subprocess_cmd.extend(targets)
947 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40948 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54949
950
Abhishek Aryac19bc5ef2018-05-04 22:10:02951def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54952 """Runs commands and returns the relative paths to the profraw data files.
953
954 Args:
955 targets: A list of targets built with coverage instrumentation.
956 commands: A list of commands used to run the targets.
957
958 Returns:
959 A list of relative paths to the generated profraw data files.
960 """
Abhishek Aryafb70b532018-05-06 17:47:40961 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10962
Yuke Liao506e8822017-12-04 16:52:54963 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48964 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54965 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48966 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
967
968 # Ensure that logs directory exists.
969 if not os.path.exists(_GetLogsDirectoryPath()):
970 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54971
Abhishek Aryac19bc5ef2018-05-04 22:10:02972 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10973
Yuke Liaod4a9865202018-01-12 23:17:52974 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54975 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48976 output_file_name = os.extsep.join([target + '_output', 'log'])
977 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10978
Abhishek Aryac19bc5ef2018-05-04 22:10:02979 profdata_file_path = None
980 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40981 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02982 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10983
Abhishek Aryac19bc5ef2018-05-04 22:10:02984 if _IsIOSCommand(command):
985 # On iOS platform, due to lack of write permissions, profraw files are
986 # generated outside of the OUTPUT_DIR, and the exact paths are contained
987 # in the output of the command execution.
988 output = _ExecuteIOSCommand(target, command)
989 else:
990 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
991 output = _ExecuteCommand(target, command)
992
993 with open(output_file_path, 'w') as output_file:
994 output_file.write(output)
995
996 profraw_file_paths = []
997 if _IsIOS():
998 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
999 else:
Max Moroz7c5354f2018-05-06 00:03:481000 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:021001 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:481002 profraw_file_paths.append(
1003 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:021004
1005 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:401006 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryad35de7e2018-05-10 22:23:041007 'please make sure the binary exists, is properly instrumented and '
1008 'does not crash. %s' % (target, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:021009
1010 try:
1011 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
1012 target, profraw_file_paths)
1013 break
1014 except Exception:
Abhishek Aryad35de7e2018-05-10 22:23:041015 logging.info('Retrying...')
Abhishek Aryac19bc5ef2018-05-04 22:10:021016 finally:
1017 # Remove profraw files now so that they are not used in next iteration.
1018 for profraw_file_path in profraw_file_paths:
1019 os.remove(profraw_file_path)
1020
1021 assert profdata_file_path, (
Abhishek Aryad35de7e2018-05-10 22:23:041022 'Failed to merge target "%s" profraw files after %d retries. %s' %
1023 (target, MERGE_RETRIES, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:021024 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541025
Abhishek Aryafb70b532018-05-06 17:47:401026 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:101027
Abhishek Aryac19bc5ef2018-05-04 22:10:021028 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:541029
1030
1031def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101032 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:521033 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:011034 #
Max Morozd73e45f2018-04-24 18:32:471035 # "%p" expands out to the process ID. It's not used by this scripts due to:
1036 # 1) If a target program spawns too many processess, it may exhaust all disk
1037 # space available. For example, unit_tests writes thousands of .profraw
1038 # files each of size 1GB+.
1039 # 2) If a target binary uses shared libraries, coverage profile data for them
1040 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:011041 #
Yuke Liaod4a9865202018-01-12 23:17:521042 # "%Nm" expands out to the instrumented binary's signature. When this pattern
1043 # is specified, the runtime creates a pool of N raw profiles which are used
1044 # for on-line profile merging. The runtime takes care of selecting a raw
1045 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:521046 # N must be between 1 and 9. The merge pool specifier can only occur once per
1047 # filename pattern.
1048 #
Max Morozd73e45f2018-04-24 18:32:471049 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:011050 #
Max Morozd73e45f2018-04-24 18:32:471051 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1052 # but it's not too big to consume too much computing resource or disk space.
1053 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591054 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011055 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481056 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541057 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541058
Yuke Liaoa0c8c2f2018-02-28 20:14:101059 try:
Max Moroz7c5354f2018-05-06 00:03:481060 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101061 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291062 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481063 stderr=subprocess.STDOUT,
Abhishek Arya1c97ea542018-05-10 03:53:191064 env={
1065 'LLVM_PROFILE_FILE': expected_profraw_file_path,
1066 'PATH': _GetPathWithLLVMSymbolizerDir()
1067 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101068 except subprocess.CalledProcessError as e:
1069 output = e.output
Abhishek Arya1c97ea542018-05-10 03:53:191070 logging.warning(
1071 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
1072 output)
Yuke Liaoa0c8c2f2018-02-28 20:14:101073
1074 return output
1075
1076
Yuke Liao27349c92018-03-22 21:10:011077def _IsFuzzerTarget(target):
1078 """Returns true if the target is a fuzzer target."""
1079 build_args = _GetBuildArgs()
1080 use_libfuzzer = ('use_libfuzzer' in build_args and
1081 build_args['use_libfuzzer'] == 'true')
1082 return use_libfuzzer and target.endswith('_fuzzer')
1083
1084
Yuke Liaob2926832018-03-02 17:34:291085def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101086 """Runs a single iOS command and generates a profraw data file.
1087
1088 iOS application doesn't have write access to folders outside of the app, so
1089 it's impossible to instruct the app to flush the profraw data file to the
1090 desired location. The profraw data file will be generated somewhere within the
1091 application's Documents folder, and the full path can be obtained by parsing
1092 the output.
1093 """
Yuke Liaob2926832018-03-02 17:34:291094 assert _IsIOSCommand(command)
1095
1096 # After running tests, iossim generates a profraw data file, it won't be
1097 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1098 # checkout.
1099 iossim_profraw_file_path = os.path.join(
1100 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101101
1102 try:
Yuke Liaob2926832018-03-02 17:34:291103 output = subprocess.check_output(
1104 shlex.split(command),
Abhishek Arya1c97ea542018-05-10 03:53:191105 env={
1106 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
1107 'PATH': _GetPathWithLLVMSymbolizerDir()
1108 })
Yuke Liaoa0c8c2f2018-02-28 20:14:101109 except subprocess.CalledProcessError as e:
1110 # iossim emits non-zero return code even if tests run successfully, so
1111 # ignore the return code.
1112 output = e.output
1113
1114 return output
1115
1116
1117def _GetProfrawDataFileByParsingOutput(output):
1118 """Returns the path to the profraw data file obtained by parsing the output.
1119
1120 The output of running the test target has no format, but it is guaranteed to
1121 have a single line containing the path to the generated profraw data file.
1122 NOTE: This should only be called when target os is iOS.
1123 """
Yuke Liaob2926832018-03-02 17:34:291124 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101125
Yuke Liaob2926832018-03-02 17:34:291126 output_by_lines = ''.join(output).splitlines()
1127 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101128
1129 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291130 result = profraw_file_pattern.match(line)
1131 if result:
1132 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101133
1134 assert False, ('No profraw data file was generated, did you call '
1135 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1136 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541137
1138
Abhishek Aryac19bc5ef2018-05-04 22:10:021139def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1140 """Returns a relative path to coverage profdata file by merging target
1141 profdata files.
Yuke Liao506e8822017-12-04 16:52:541142
1143 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021144 profdata_file_paths: A list of relative paths to the profdata data files
1145 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541146
1147 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021148 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541149
1150 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021151 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541152 """
Abhishek Aryafb70b532018-05-06 17:47:401153 logging.info('Creating the coverage profile data file.')
1154 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481155 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541156 try:
Abhishek Arya1ec832c2017-12-05 18:06:591157 subprocess_cmd = [
1158 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1159 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021160 subprocess_cmd.extend(profdata_file_paths)
1161 subprocess.check_call(subprocess_cmd)
1162 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041163 logging.error(
1164 'Failed to merge target profdata files to create coverage profdata. %s',
1165 FILE_BUG_MESSAGE)
Abhishek Aryac19bc5ef2018-05-04 22:10:021166 raise error
1167
Abhishek Aryafb70b532018-05-06 17:47:401168 logging.debug('Finished merging target profdata files.')
1169 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021170 profdata_file_path)
1171 return profdata_file_path
1172
1173
1174def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1175 """Returns a relative path to target profdata file by merging target
1176 profraw files.
1177
1178 Args:
1179 profraw_file_paths: A list of relative paths to the profdata data files
1180 that are to be merged.
1181
1182 Returns:
1183 A relative path to the merged coverage profdata file.
1184
1185 Raises:
1186 CalledProcessError: An error occurred merging profdata files.
1187 """
Abhishek Aryafb70b532018-05-06 17:47:401188 logging.info('Creating target profile data file.')
1189 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021190 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1191
1192 try:
1193 subprocess_cmd = [
1194 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1195 ]
Yuke Liao506e8822017-12-04 16:52:541196 subprocess_cmd.extend(profraw_file_paths)
1197 subprocess.check_call(subprocess_cmd)
1198 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:041199 logging.error(
1200 'Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541201 raise error
1202
Abhishek Aryafb70b532018-05-06 17:47:401203 logging.debug('Finished merging target profraw files.')
1204 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101205 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541206 return profdata_file_path
1207
1208
Yuke Liao0e4c8682018-04-18 21:06:591209def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1210 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331211 """Generates per file coverage summary using "llvm-cov export" command."""
1212 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1213 # [[-object BIN]] [SOURCES].
1214 # NOTE: For object files, the first one is specified as a positional argument,
1215 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101216 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401217 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331218 subprocess_cmd = [
1219 LLVM_COV_PATH, 'export', '-summary-only',
1220 '-instr-profile=' + profdata_file_path, binary_paths[0]
1221 ]
1222 subprocess_cmd.extend(
1223 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291224 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331225 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591226 if ignore_filename_regex:
1227 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331228
Max Moroz7c5354f2018-05-06 00:03:481229 export_output = subprocess.check_output(subprocess_cmd)
1230
1231 # Write output on the disk to be used by code coverage bot.
1232 with open(_GetSummaryFilePath(), 'w') as f:
1233 f.write(export_output)
1234
1235 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331236 assert len(json_output['data']) == 1
1237 files_coverage_data = json_output['data'][0]['files']
1238
1239 per_file_coverage_summary = {}
1240 for file_coverage_data in files_coverage_data:
1241 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401242 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1243 'File path "%s" in coverage summary is outside source checkout.' %
1244 file_path)
Yuke Liaoea228d02018-01-05 19:10:331245
Abhishek Aryafb70b532018-05-06 17:47:401246 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331247 if summary['lines']['count'] == 0:
1248 continue
1249
1250 per_file_coverage_summary[file_path] = _CoverageSummary(
1251 regions_total=summary['regions']['count'],
1252 regions_covered=summary['regions']['covered'],
1253 functions_total=summary['functions']['count'],
1254 functions_covered=summary['functions']['covered'],
1255 lines_total=summary['lines']['count'],
1256 lines_covered=summary['lines']['covered'])
1257
Abhishek Aryafb70b532018-05-06 17:47:401258 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331259 return per_file_coverage_summary
1260
1261
Yuke Liaob2926832018-03-02 17:34:291262def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1263 """Appends -arch arguments to the command list if it's ios platform.
1264
1265 iOS binaries are universal binaries, and require specifying the architecture
1266 to use, and one architecture needs to be specified for each binary.
1267 """
1268 if _IsIOS():
1269 cmd_list.extend(['-arch=x86_64'] * num_archs)
1270
1271
Yuke Liao506e8822017-12-04 16:52:541272def _GetBinaryPath(command):
1273 """Returns a relative path to the binary to be run by the command.
1274
Yuke Liao545db322018-02-15 17:12:011275 Currently, following types of commands are supported (e.g. url_unittests):
1276 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1277 2. Use xvfb.
1278 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1279 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371280 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1281 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101282 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371283 <iossim_arguments> -c <app_arguments>
1284 out/Coverage-iphonesimulator/url_unittests.app"
1285
Yuke Liao545db322018-02-15 17:12:011286
Yuke Liao506e8822017-12-04 16:52:541287 Args:
1288 command: A command used to run a target.
1289
1290 Returns:
1291 A relative path to the binary.
1292 """
Yuke Liao545db322018-02-15 17:12:011293 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1294
Yuke Liaob2926832018-03-02 17:34:291295 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011296 if os.path.basename(command_parts[0]) == 'python':
1297 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401298 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011299 return command_parts[2]
1300
1301 if os.path.basename(command_parts[0]) == xvfb_script_name:
1302 return command_parts[1]
1303
Yuke Liaob2926832018-03-02 17:34:291304 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101305 # For a given application bundle, the binary resides in the bundle and has
1306 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371307 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101308 app_name = os.path.splitext(os.path.basename(app_path))[0]
1309 return os.path.join(app_path, app_name)
1310
Yuke Liaob2926832018-03-02 17:34:291311 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541312
1313
Yuke Liaob2926832018-03-02 17:34:291314def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101315 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291316 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101317
1318
Yuke Liao95d13d72017-12-07 18:18:501319def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1320 """Verifies that the target executables specified in the commands are inside
1321 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541322 for command in commands:
1323 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501324 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481325 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501326 'Target executable "%s" in command: "%s" is outside of '
1327 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541328
1329
1330def _ValidateBuildingWithClangCoverage():
1331 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201332 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541333
1334 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1335 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591336 assert False, ('\'{} = true\' is required in args.gn.'
1337 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541338
1339
Yuke Liaoc60b2d02018-03-02 21:40:431340def _ValidateCurrentPlatformIsSupported():
1341 """Asserts that this script suports running on the current platform"""
1342 target_os = _GetTargetOS()
1343 if target_os:
1344 current_platform = target_os
1345 else:
1346 current_platform = _GetHostPlatform()
1347
1348 assert current_platform in [
1349 'linux', 'mac', 'chromeos', 'ios'
1350 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1351
1352
Yuke Liao80afff32018-03-07 01:26:201353def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541354 """Parses args.gn file and returns results as a dictionary.
1355
1356 Returns:
1357 A dictionary representing the build args.
1358 """
Yuke Liao80afff32018-03-07 01:26:201359 global _BUILD_ARGS
1360 if _BUILD_ARGS is not None:
1361 return _BUILD_ARGS
1362
1363 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541364 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1365 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1366 'missing args.gn file.' % BUILD_DIR)
1367 with open(build_args_path) as build_args_file:
1368 build_args_lines = build_args_file.readlines()
1369
Yuke Liao506e8822017-12-04 16:52:541370 for build_arg_line in build_args_lines:
1371 build_arg_without_comments = build_arg_line.split('#')[0]
1372 key_value_pair = build_arg_without_comments.split('=')
1373 if len(key_value_pair) != 2:
1374 continue
1375
1376 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431377
1378 # Values are wrapped within a pair of double-quotes, so remove the leading
1379 # and trailing double-quotes.
1380 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201381 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541382
Yuke Liao80afff32018-03-07 01:26:201383 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541384
1385
Abhishek Arya16f059a2017-12-07 17:47:321386def _VerifyPathsAndReturnAbsolutes(paths):
1387 """Verifies that the paths specified in |paths| exist and returns absolute
1388 versions.
Yuke Liao66da1732017-12-05 22:19:421389
1390 Args:
1391 paths: A list of files or directories.
1392 """
Abhishek Arya16f059a2017-12-07 17:47:321393 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421394 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321395 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1396 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1397
1398 absolute_paths.append(absolute_path)
1399
1400 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421401
1402
Yuke Liaodd1ec0592018-02-02 01:26:371403def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1404 """Returns a target path relative to the directory of base_path.
1405
1406 This method requires base_path to be a file, otherwise, one should call
1407 os.path.relpath directly.
1408 """
1409 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141410 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371411 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141412 base_dir = os.path.dirname(base_path)
1413 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371414
1415
Abhishek Arya64636af2018-05-04 14:42:131416def _GetBinaryPathsFromTargets(targets, build_dir):
1417 """Return binary paths from target names."""
1418 # FIXME: Derive output binary from target build definitions rather than
1419 # assuming that it is always the same name.
1420 binary_paths = []
1421 for target in targets:
1422 binary_path = os.path.join(build_dir, target)
1423 if _GetHostPlatform() == 'win':
1424 binary_path += '.exe'
1425
1426 if os.path.exists(binary_path):
1427 binary_paths.append(binary_path)
1428 else:
1429 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401430 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131431 os.path.basename(binary_path))
1432
1433 return binary_paths
1434
1435
Yuke Liao506e8822017-12-04 16:52:541436def _ParseCommandArguments():
1437 """Adds and parses relevant arguments for tool comands.
1438
1439 Returns:
1440 A dictionary representing the arguments.
1441 """
1442 arg_parser = argparse.ArgumentParser()
1443 arg_parser.usage = __doc__
1444
Abhishek Arya1ec832c2017-12-05 18:06:591445 arg_parser.add_argument(
1446 '-b',
1447 '--build-dir',
1448 type=str,
1449 required=True,
1450 help='The build directory, the path needs to be relative to the root of '
1451 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541452
Abhishek Arya1ec832c2017-12-05 18:06:591453 arg_parser.add_argument(
1454 '-o',
1455 '--output-dir',
1456 type=str,
1457 required=True,
1458 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541459
Abhishek Arya1ec832c2017-12-05 18:06:591460 arg_parser.add_argument(
1461 '-c',
1462 '--command',
1463 action='append',
Abhishek Arya64636af2018-05-04 14:42:131464 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591465 help='Commands used to run test targets, one test target needs one and '
1466 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131467 'current working directory is the root of the checkout. This option is '
1468 'incompatible with -p/--profdata-file option.')
1469
1470 arg_parser.add_argument(
1471 '-p',
1472 '--profdata-file',
1473 type=str,
1474 required=False,
1475 help='Path to profdata file to use for generating code coverage reports. '
1476 'This can be useful if you generated the profdata file seperately in '
1477 'your own test harness. This option is ignored if run command(s) are '
1478 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541479
Abhishek Arya1ec832c2017-12-05 18:06:591480 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421481 '-f',
1482 '--filters',
1483 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321484 required=False,
Yuke Liao66da1732017-12-05 22:19:421485 help='Directories or files to get code coverage for, and all files under '
1486 'the directories are included recursively.')
1487
1488 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591489 '-i',
1490 '--ignore-filename-regex',
1491 type=str,
1492 help='Skip source code files with file paths that match the given '
1493 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1494 'to exclude files in third_party/ and out/ folders from the report.')
1495
1496 arg_parser.add_argument(
Yuke Liao1b852fd2018-05-11 17:07:321497 '--no-file-view',
1498 action='store_true',
1499 help='Don\'t generate the file view in the coverage report. When there '
1500 'are large number of html files, the file view becomes heavy and may '
1501 'cause the browser to freeze, and this argument comes handy.')
1502
1503 arg_parser.add_argument(
Yuke Liao082e99632018-05-18 15:40:401504 '--coverage-tools-dir',
1505 type=str,
1506 help='Path of the directory where LLVM coverage tools (llvm-cov, '
1507 'llvm-profdata) exist. This should be only needed if you are testing '
1508 'against a custom built clang revision. Otherwise, we pick coverage '
1509 'tools automatically from your current source checkout.')
1510
1511 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591512 '-j',
1513 '--jobs',
1514 type=int,
1515 default=None,
1516 help='Run N jobs to build in parallel. If not specified, a default value '
1517 'will be derived based on CPUs availability. Please refer to '
1518 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541519
Abhishek Arya1ec832c2017-12-05 18:06:591520 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101521 '-v',
1522 '--verbose',
1523 action='store_true',
1524 help='Prints additional output for diagnostics.')
1525
1526 arg_parser.add_argument(
1527 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1528
1529 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021530 'targets',
1531 nargs='+',
1532 help='The names of the test targets to run. If multiple run commands are '
1533 'specified using the -c/--command option, then the order of targets and '
1534 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541535
1536 args = arg_parser.parse_args()
1537 return args
1538
1539
1540def Main():
1541 """Execute tool commands."""
Yuke Liao082e99632018-05-18 15:40:401542 # Setup coverage binaries even when script is called with empty params. This
1543 # is used by coverage bot for initial setup.
1544 if len(sys.argv) == 1:
1545 DownloadCoverageToolsIfNeeded()
1546 print(__doc__)
1547 return
1548
Abhishek Arya64636af2018-05-04 14:42:131549 # Change directory to source root to aid in relative paths calculations.
1550 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111551
Yuke Liao506e8822017-12-04 16:52:541552 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131553 _ConfigureLogging(args)
Yuke Liao082e99632018-05-18 15:40:401554 _ConfigureLLVMCoverageTools(args)
Abhishek Arya64636af2018-05-04 14:42:131555
Yuke Liao506e8822017-12-04 16:52:541556 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481557 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541558 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481559 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541560
Abhishek Arya64636af2018-05-04 14:42:131561 assert args.command or args.profdata_file, (
1562 'Need to either provide commands to run using -c/--command option OR '
1563 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431564
Abhishek Arya64636af2018-05-04 14:42:131565 assert not args.command or (len(args.targets) == len(args.command)), (
1566 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431567
Abhishek Arya1ec832c2017-12-05 18:06:591568 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401569 'Build directory: "%s" doesn\'t exist. '
1570 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131571
Yuke Liaoc60b2d02018-03-02 21:40:431572 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541573 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321574
1575 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421576 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321577 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421578
Max Moroz7c5354f2018-05-06 00:03:481579 if not os.path.exists(_GetCoverageReportRootDirPath()):
1580 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541581
Abhishek Arya64636af2018-05-04 14:42:131582 # Get profdate file and list of binary paths.
1583 if args.command:
1584 # A list of commands are provided. Run them to generate profdata file, and
1585 # create a list of binary paths from parsing commands.
1586 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1587 profdata_file_path = _CreateCoverageProfileDataForTargets(
1588 args.targets, args.command, args.jobs)
1589 binary_paths = [_GetBinaryPath(command) for command in args.command]
1590 else:
1591 # An input prof-data file is already provided. Just calculate binary paths.
1592 profdata_file_path = args.profdata_file
1593 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331594
Abhishek Arya78120bc2018-05-07 20:53:541595 binary_paths.extend(_GetSharedLibraries(binary_paths))
1596
Yuke Liao481d3482018-01-29 19:17:101597 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401598 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371599 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591600 binary_paths, profdata_file_path, absolute_filter_paths,
1601 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371602 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591603 absolute_filter_paths,
1604 args.ignore_filename_regex)
Yuke Liao1b852fd2018-05-11 17:07:321605 if not args.no_file_view:
1606 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
Yuke Liaodd1ec0592018-02-02 01:26:371607
1608 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1609 per_file_coverage_summary)
1610 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
Yuke Liao1b852fd2018-05-11 17:07:321611 per_file_coverage_summary,
1612 args.no_file_view)
Yuke Liaodd1ec0592018-02-02 01:26:371613 _GenerateDirectoryViewHtmlIndexFile()
1614
1615 component_to_directories = _ExtractComponentToDirectoriesMapping()
1616 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1617 component_to_directories, per_directory_coverage_summary)
Yuke Liao1b852fd2018-05-11 17:07:321618 _GeneratePerComponentCoverageInHtml(
1619 per_component_coverage_summary, component_to_directories,
1620 per_directory_coverage_summary, args.no_file_view)
1621 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary,
1622 args.no_file_view)
Yuke Liaoea228d02018-01-05 19:10:331623
1624 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371625 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331626 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481627 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331628
Max Moroz7c5354f2018-05-06 00:03:481629 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401630 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101631 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541632
Abhishek Arya1ec832c2017-12-05 18:06:591633
Yuke Liao506e8822017-12-04 16:52:541634if __name__ == '__main__':
1635 sys.exit(Main())