blob: 962f85156bf51f7161e8bae11ddf3a1af7bd0e99 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3856
57 For an overview of how code coverage works in Chromium, please refer to
58 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5459"""
60
61from __future__ import print_function
62
63import sys
64
65import argparse
Yuke Liaoea228d02018-01-05 19:10:3366import json
Yuke Liao481d3482018-01-29 19:17:1067import logging
Yuke Liao506e8822017-12-04 16:52:5468import os
Yuke Liaob2926832018-03-02 17:34:2969import re
70import shlex
Max Moroz025d8952018-05-03 16:33:3471import shutil
Yuke Liao506e8822017-12-04 16:52:5472import subprocess
Yuke Liao506e8822017-12-04 16:52:5473import urllib2
74
Abhishek Arya1ec832c2017-12-05 18:06:5975sys.path.append(
76 os.path.join(
77 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
78 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5479import update as clang_update
80
Yuke Liaoea228d02018-01-05 19:10:3381sys.path.append(
82 os.path.join(
83 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
84 'third_party'))
85import jinja2
86from collections import defaultdict
87
Yuke Liao506e8822017-12-04 16:52:5488# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5989SRC_ROOT_PATH = os.path.abspath(
90 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5491
92# Absolute path to the code coverage tools binary.
93LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
94LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
95LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
96
97# Build directory, the value is parsed from command line arguments.
98BUILD_DIR = None
99
100# Output directory for generated artifacts, the value is parsed from command
101# line arguemnts.
102OUTPUT_DIR = None
103
104# Default number of jobs used to build when goma is configured and enabled.
105DEFAULT_GOMA_JOBS = 100
106
107# Name of the file extension for profraw data files.
108PROFRAW_FILE_EXTENSION = 'profraw'
109
110# Name of the final profdata file, and this file needs to be passed to
111# "llvm-cov" command in order to call "llvm-cov show" to inspect the
112# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48113PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
114
115# Name of the file with summary information generated by llvm-cov export.
116SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54117
118# Build arg required for generating code coverage data.
119CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
120
Yuke Liaoea228d02018-01-05 19:10:33121# The default name of the html coverage report for a directory.
122DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
123
Yuke Liaodd1ec0592018-02-02 01:26:37124# Name of the html index files for different views.
Yuke Liaodd1ec0592018-02-02 01:26:37125COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48126DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
Yuke Liaodd1ec0592018-02-02 01:26:37127FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
Max Moroz7c5354f2018-05-06 00:03:48128INDEX_HTML_FILE = os.extsep.join(['index', 'html'])
129
130LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37131
132# Used to extract a mapping between directories and components.
133COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
134
Yuke Liao80afff32018-03-07 01:26:20135# Caches the results returned by _GetBuildArgs, don't use this variable
136# directly, call _GetBuildArgs instead.
137_BUILD_ARGS = None
138
Abhishek Aryac19bc5ef2018-05-04 22:10:02139# Retry failed merges.
140MERGE_RETRIES = 3
141
Yuke Liaoea228d02018-01-05 19:10:33142
143class _CoverageSummary(object):
144 """Encapsulates coverage summary representation."""
145
Yuke Liaodd1ec0592018-02-02 01:26:37146 def __init__(self,
147 regions_total=0,
148 regions_covered=0,
149 functions_total=0,
150 functions_covered=0,
151 lines_total=0,
152 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33153 """Initializes _CoverageSummary object."""
154 self._summary = {
155 'regions': {
156 'total': regions_total,
157 'covered': regions_covered
158 },
159 'functions': {
160 'total': functions_total,
161 'covered': functions_covered
162 },
163 'lines': {
164 'total': lines_total,
165 'covered': lines_covered
166 }
167 }
168
169 def Get(self):
170 """Returns summary as a dictionary."""
171 return self._summary
172
173 def AddSummary(self, other_summary):
174 """Adds another summary to this one element-wise."""
175 for feature in self._summary:
176 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
177 self._summary[feature]['covered'] += other_summary.Get()[feature][
178 'covered']
179
180
Yuke Liaodd1ec0592018-02-02 01:26:37181class _CoverageReportHtmlGenerator(object):
182 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33183
Yuke Liaodd1ec0592018-02-02 01:26:37184 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33185 """
186
Yuke Liaodd1ec0592018-02-02 01:26:37187 def __init__(self, output_path, table_entry_type):
188 """Initializes _CoverageReportHtmlGenerator object.
189
190 Args:
191 output_path: Path to the html report that will be generated.
192 table_entry_type: Type of the table entries to be displayed in the table
193 header. For example: 'Path', 'Component'.
194 """
Yuke Liaoea228d02018-01-05 19:10:33195 css_file_name = os.extsep.join(['style', 'css'])
Max Moroz7c5354f2018-05-06 00:03:48196 css_absolute_path = os.path.join(OUTPUT_DIR, css_file_name)
Yuke Liaoea228d02018-01-05 19:10:33197 assert os.path.exists(css_absolute_path), (
198 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
Abhishek Aryafb70b532018-05-06 17:47:40199 'is called first, and the css file is generated at: "%s".' %
Yuke Liaoea228d02018-01-05 19:10:33200 css_absolute_path)
201
202 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37203 self._output_path = output_path
204 self._table_entry_type = table_entry_type
205
Yuke Liaoea228d02018-01-05 19:10:33206 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12207 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33208 template_dir = os.path.join(
209 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
210
211 jinja_env = jinja2.Environment(
212 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
213 self._header_template = jinja_env.get_template('header.html')
214 self._table_template = jinja_env.get_template('table.html')
215 self._footer_template = jinja_env.get_template('footer.html')
Abhishek Arya865fffd2018-05-08 22:16:01216 self._style_overrides = open(
217 os.path.join(template_dir, 'style_overrides.css')).read()
Yuke Liaoea228d02018-01-05 19:10:33218
219 def AddLinkToAnotherReport(self, html_report_path, name, summary):
220 """Adds a link to another html report in this report.
221
222 The link to be added is assumed to be an entry in this directory.
223 """
Yuke Liaodd1ec0592018-02-02 01:26:37224 # Use relative paths instead of absolute paths to make the generated reports
225 # portable.
226 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
227 html_report_path, self._output_path)
228
Yuke Liaod54030e2018-01-08 17:34:12229 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37230 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12231 os.path.basename(html_report_path) ==
232 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
233 self._table_entries.append(table_entry)
234
235 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35236 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12237 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
238
239 def _CreateTableEntryFromCoverageSummary(self,
240 summary,
241 href=None,
242 name=None,
243 is_dir=None):
244 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37245 assert (href is None and name is None and is_dir is None) or (
246 href is not None and name is not None and is_dir is not None), (
247 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35248 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37249 'attributes must be None.')
250
Yuke Liaod54030e2018-01-08 17:34:12251 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37252 if href is not None:
253 entry['href'] = href
254 if name is not None:
255 entry['name'] = name
256 if is_dir is not None:
257 entry['is_dir'] = is_dir
258
Yuke Liaoea228d02018-01-05 19:10:33259 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12260 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37261 if summary_dict[feature]['total'] == 0:
262 percentage = 0.0
263 else:
Yuke Liao0e4c8682018-04-18 21:06:59264 percentage = float(summary_dict[feature]
265 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35266
Yuke Liaoea228d02018-01-05 19:10:33267 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12268 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33269 'total': summary_dict[feature]['total'],
270 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35271 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33272 'color_class': color_class
273 }
Yuke Liaod54030e2018-01-08 17:34:12274
Yuke Liaod54030e2018-01-08 17:34:12275 return entry
Yuke Liaoea228d02018-01-05 19:10:33276
277 def _GetColorClass(self, percentage):
278 """Returns the css color class based on coverage percentage."""
279 if percentage >= 0 and percentage < 80:
280 return 'red'
281 if percentage >= 80 and percentage < 100:
282 return 'yellow'
283 if percentage == 100:
284 return 'green'
285
Abhishek Aryafb70b532018-05-06 17:47:40286 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33287
Yuke Liaodd1ec0592018-02-02 01:26:37288 def WriteHtmlCoverageReport(self):
289 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33290
291 In the report, sub-directories are displayed before files and within each
292 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33293 """
294
295 def EntryCmp(left, right):
296 """Compare function for table entries."""
297 if left['is_dir'] != right['is_dir']:
298 return -1 if left['is_dir'] == True else 1
299
Yuke Liaodd1ec0592018-02-02 01:26:37300 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33301
302 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
303
304 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48305
306 directory_view_path = _GetDirectoryViewPath()
307 component_view_path = _GetComponentViewPath()
308 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37309
Yuke Liaoea228d02018-01-05 19:10:33310 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37311 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
312 directory_view_href=_GetRelativePathToDirectoryOfFile(
313 directory_view_path, self._output_path),
314 component_view_href=_GetRelativePathToDirectoryOfFile(
315 component_view_path, self._output_path),
316 file_view_href=_GetRelativePathToDirectoryOfFile(
Abhishek Arya865fffd2018-05-08 22:16:01317 file_view_path, self._output_path),
318 style_overrides=self._style_overrides)
Yuke Liaodd1ec0592018-02-02 01:26:37319
Yuke Liaod54030e2018-01-08 17:34:12320 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37321 entries=self._table_entries,
322 total_entry=self._total_entry,
323 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33324 html_footer = self._footer_template.render()
325
Yuke Liaodd1ec0592018-02-02 01:26:37326 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33327 html_file.write(html_header + html_table + html_footer)
328
Yuke Liao506e8822017-12-04 16:52:54329
Abhishek Arya64636af2018-05-04 14:42:13330def _ConfigureLogging(args):
331 """Configures logging settings for later use."""
332 log_level = logging.DEBUG if args.verbose else logging.INFO
333 log_format = '[%(asctime)s %(levelname)s] %(message)s'
334 log_file = args.log_file if args.log_file else None
335 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
336
337
Max Morozd73e45f2018-04-24 18:32:47338def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54339 """Returns list of shared libraries used by specified binaries."""
340 logging.info('Finding shared libraries for targets (if any).')
341 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47342 cmd = []
343 shared_library_re = None
344
345 if sys.platform.startswith('linux'):
346 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13347 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
348 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47349 elif sys.platform.startswith('darwin'):
350 cmd.extend(['otool', '-L'])
351 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
352 else:
Abhishek Aryafb70b532018-05-06 17:47:40353 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47354
355 assert shared_library_re is not None
356
357 cmd.extend(binary_paths)
358 output = subprocess.check_output(cmd)
359
360 for line in output.splitlines():
361 m = shared_library_re.match(line)
362 if not m:
363 continue
364
365 shared_library_path = m.group(1)
366 if sys.platform.startswith('darwin'):
367 # otool outputs "@rpath" macro instead of the dirname of the given binary.
368 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
369
Abhishek Arya78120bc2018-05-07 20:53:54370 if shared_library_path in shared_libraries:
371 continue
372
Max Morozd73e45f2018-04-24 18:32:47373 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
374 'the given target(s) does not '
375 'exist.' % shared_library_path)
376 with open(shared_library_path) as f:
377 data = f.read()
378
379 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
380 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54381 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47382
Abhishek Arya78120bc2018-05-07 20:53:54383 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
384 shared_libraries)
385 logging.info('Finished finding shared libraries for targets.')
386 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47387
388
Yuke Liaoc60b2d02018-03-02 21:40:43389def _GetHostPlatform():
390 """Returns the host platform.
391
392 This is separate from the target platform/os that coverage is running for.
393 """
Abhishek Arya1ec832c2017-12-05 18:06:59394 if sys.platform == 'win32' or sys.platform == 'cygwin':
395 return 'win'
396 if sys.platform.startswith('linux'):
397 return 'linux'
398 else:
399 assert sys.platform == 'darwin'
400 return 'mac'
401
402
Yuke Liaoc60b2d02018-03-02 21:40:43403def _GetTargetOS():
404 """Returns the target os specified in args.gn file.
405
406 Returns an empty string is target_os is not specified.
407 """
Yuke Liao80afff32018-03-07 01:26:20408 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43409 return build_args['target_os'] if 'target_os' in build_args else ''
410
411
Yuke Liaob2926832018-03-02 17:34:29412def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10413 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43414 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10415
416
Yuke Liao506e8822017-12-04 16:52:54417# TODO(crbug.com/759794): remove this function once tools get included to
418# Clang bundle:
419# https://chromium-review.googlesource.com/c/chromium/src/+/688221
420def DownloadCoverageToolsIfNeeded():
421 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59422
Yuke Liaoc60b2d02018-03-02 21:40:43423 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54424 """Returns a pair of revision number by reading the build stamp file.
425
426 Args:
427 stamp_file_path: A path the build stamp file created by
428 tools/clang/scripts/update.py.
429 Returns:
430 A pair of integers represeting the main and sub revision respectively.
431 """
432 if not os.path.exists(stamp_file_path):
433 return 0, 0
434
435 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43436 stamp_file_line = stamp_file.readline()
437 if ',' in stamp_file_line:
438 package_version = stamp_file_line.rstrip().split(',')[0]
439 else:
440 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54441
Yuke Liaoc60b2d02018-03-02 21:40:43442 clang_revision_str, clang_sub_revision_str = package_version.split('-')
443 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59444
Yuke Liaoc60b2d02018-03-02 21:40:43445 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54446 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43447 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54448
449 coverage_revision_stamp_file = os.path.join(
450 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
451 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43452 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54453
Yuke Liaoea228d02018-01-05 19:10:33454 has_coverage_tools = (
455 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32456
Yuke Liaoea228d02018-01-05 19:10:33457 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54458 coverage_sub_revision == clang_sub_revision):
459 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43460 return
Yuke Liao506e8822017-12-04 16:52:54461
462 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
463 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
464
465 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43466 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54467 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43468 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54469 coverage_tools_url = (
470 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43471 else:
472 assert host_platform == 'win'
473 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54474
475 try:
476 clang_update.DownloadAndUnpack(coverage_tools_url,
477 clang_update.LLVM_BUILD_DIR)
Abhishek Aryafb70b532018-05-06 17:47:40478 logging.info('Coverage tools %s unpacked.', package_version)
Yuke Liao506e8822017-12-04 16:52:54479 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43480 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54481 file_handle.write('\n')
482 except urllib2.URLError:
483 raise Exception(
484 'Failed to download coverage tools: %s.' % coverage_tools_url)
485
486
Yuke Liaodd1ec0592018-02-02 01:26:37487def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59488 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54489 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
490
491 For a file with absolute path /a/b/x.cc, a html report is generated as:
492 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
493 OUTPUT_DIR/index.html.
494
495 Args:
496 binary_paths: A list of paths to the instrumented binaries.
497 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42498 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54499 """
Yuke Liao506e8822017-12-04 16:52:54500 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
501 # [[-object BIN]] [SOURCES]
502 # NOTE: For object files, the first one is specified as a positional argument,
503 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10504 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40505 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59506 subprocess_cmd = [
507 LLVM_COV_PATH, 'show', '-format=html',
508 '-output-dir={}'.format(OUTPUT_DIR),
509 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
510 ]
511 subprocess_cmd.extend(
512 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29513 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42514 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59515 if ignore_filename_regex:
516 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
517
Yuke Liao506e8822017-12-04 16:52:54518 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34519
520 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
521 # the platform name instead, as it simplifies the report dir structure when
522 # the same report is generated for different platforms.
523 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48524 platform_report_subdir_path = _GetCoverageReportRootDirPath()
525 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34526
Abhishek Aryafb70b532018-05-06 17:47:40527 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54528
529
Yuke Liaodd1ec0592018-02-02 01:26:37530def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
531 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48532 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37533 logging.debug('Generating file view html index file as: "%s".',
534 file_view_index_file_path)
535 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
536 'Path')
537 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33538
Yuke Liaodd1ec0592018-02-02 01:26:37539 for file_path in per_file_coverage_summary:
540 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
541
542 html_generator.AddLinkToAnotherReport(
543 _GetCoverageHtmlReportPathForFile(file_path),
544 os.path.relpath(file_path, SRC_ROOT_PATH),
545 per_file_coverage_summary[file_path])
546
547 html_generator.CreateTotalsEntry(totals_coverage_summary)
548 html_generator.WriteHtmlCoverageReport()
549 logging.debug('Finished generating file view html index file.')
550
551
552def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
553 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40554 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37555 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
556
Yuke Liaoea228d02018-01-05 19:10:33557 for file_path in per_file_coverage_summary:
558 summary = per_file_coverage_summary[file_path]
559 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40560
Yuke Liaoea228d02018-01-05 19:10:33561 while True:
562 per_directory_coverage_summary[parent_dir].AddSummary(summary)
563
564 if parent_dir == SRC_ROOT_PATH:
565 break
566 parent_dir = os.path.dirname(parent_dir)
567
Abhishek Aryafb70b532018-05-06 17:47:40568 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37569 return per_directory_coverage_summary
570
571
572def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
573 per_file_coverage_summary):
574 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40575 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33576 for dir_path in per_directory_coverage_summary:
577 _GenerateCoverageInHtmlForDirectory(
578 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
579
Abhishek Aryafb70b532018-05-06 17:47:40580 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10581
Yuke Liaoea228d02018-01-05 19:10:33582
583def _GenerateCoverageInHtmlForDirectory(
584 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
585 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37586 html_generator = _CoverageReportHtmlGenerator(
587 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33588
589 for entry_name in os.listdir(dir_path):
590 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33591
Yuke Liaodd1ec0592018-02-02 01:26:37592 if entry_path in per_file_coverage_summary:
593 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
594 entry_coverage_summary = per_file_coverage_summary[entry_path]
595 elif entry_path in per_directory_coverage_summary:
596 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
597 entry_path)
598 entry_coverage_summary = per_directory_coverage_summary[entry_path]
599 else:
Yuke Liaoc7e607142018-02-05 20:26:14600 # Any file without executable lines shouldn't be included into the report.
601 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37602 continue
Yuke Liaoea228d02018-01-05 19:10:33603
Yuke Liaodd1ec0592018-02-02 01:26:37604 html_generator.AddLinkToAnotherReport(entry_html_report_path,
605 os.path.basename(entry_path),
606 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33607
Yuke Liaod54030e2018-01-08 17:34:12608 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37609 html_generator.WriteHtmlCoverageReport()
610
611
612def _GenerateDirectoryViewHtmlIndexFile():
613 """Generates the html index file for directory view.
614
615 Note that the index file is already generated under SRC_ROOT_PATH, so this
616 file simply redirects to it, and the reason of this extra layer is for
617 structural consistency with other views.
618 """
Max Moroz7c5354f2018-05-06 00:03:48619 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37620 logging.debug('Generating directory view html index file as: "%s".',
621 directory_view_index_file_path)
622 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
623 SRC_ROOT_PATH)
624 _WriteRedirectHtmlFile(directory_view_index_file_path,
625 src_root_html_report_path)
626 logging.debug('Finished generating directory view html index file.')
627
628
629def _CalculatePerComponentCoverageSummary(component_to_directories,
630 per_directory_coverage_summary):
631 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40632 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37633 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
634
635 for component in component_to_directories:
636 for directory in component_to_directories[component]:
637 absolute_directory_path = os.path.abspath(directory)
638 if absolute_directory_path in per_directory_coverage_summary:
639 per_component_coverage_summary[component].AddSummary(
640 per_directory_coverage_summary[absolute_directory_path])
641
Abhishek Aryafb70b532018-05-06 17:47:40642 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37643 return per_component_coverage_summary
644
645
646def _ExtractComponentToDirectoriesMapping():
647 """Returns a mapping from components to directories."""
648 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
649 directory_to_component = component_mappings['dir-to-component']
650
651 component_to_directories = defaultdict(list)
652 for directory in directory_to_component:
653 component = directory_to_component[directory]
654 component_to_directories[component].append(directory)
655
656 return component_to_directories
657
658
659def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
660 component_to_directories,
661 per_directory_coverage_summary):
662 """Generates per-component coverage reports in html."""
663 logging.debug('Writing per-component coverage html reports.')
664 for component in per_component_coverage_summary:
665 _GenerateCoverageInHtmlForComponent(
666 component, per_component_coverage_summary, component_to_directories,
667 per_directory_coverage_summary)
668
669 logging.debug('Finished writing per-component coverage html reports.')
670
671
672def _GenerateCoverageInHtmlForComponent(
673 component_name, per_component_coverage_summary, component_to_directories,
674 per_directory_coverage_summary):
675 """Generates coverage html report for a component."""
676 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
677 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14678 component_html_report_dir = os.path.dirname(component_html_report_path)
679 if not os.path.exists(component_html_report_dir):
680 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37681
682 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
683 'Path')
684
685 for dir_path in component_to_directories[component_name]:
686 dir_absolute_path = os.path.abspath(dir_path)
687 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14688 # Any directory without an excercised file shouldn't be included into the
689 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37690 continue
691
692 html_generator.AddLinkToAnotherReport(
693 _GetCoverageHtmlReportPathForDirectory(dir_path),
694 os.path.relpath(dir_path, SRC_ROOT_PATH),
695 per_directory_coverage_summary[dir_absolute_path])
696
697 html_generator.CreateTotalsEntry(
698 per_component_coverage_summary[component_name])
699 html_generator.WriteHtmlCoverageReport()
700
701
702def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
703 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48704 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37705 logging.debug('Generating component view html index file as: "%s".',
706 component_view_index_file_path)
707 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
708 'Component')
709 totals_coverage_summary = _CoverageSummary()
710
711 for component in per_component_coverage_summary:
712 totals_coverage_summary.AddSummary(
713 per_component_coverage_summary[component])
714
715 html_generator.AddLinkToAnotherReport(
716 _GetCoverageHtmlReportPathForComponent(component), component,
717 per_component_coverage_summary[component])
718
719 html_generator.CreateTotalsEntry(totals_coverage_summary)
720 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14721 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33722
723
Max Moroz7c5354f2018-05-06 00:03:48724def _MergeTwoDirectories(src_path, dst_path):
725 """Merge src_path directory into dst_path directory."""
726 for filename in os.listdir(src_path):
727 dst_path = os.path.join(dst_path, filename)
728 if os.path.exists(dst_path):
729 shutil.rmtree(dst_path)
730 os.rename(os.path.join(src_path, filename), dst_path)
731 shutil.rmtree(src_path)
732
733
Yuke Liaoea228d02018-01-05 19:10:33734def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37735 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48736 html_index_file_path = _GetHtmlIndexPath()
737 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37738 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
739
740
741def _WriteRedirectHtmlFile(from_html_path, to_html_path):
742 """Writes a html file that redirects to another html file."""
743 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
744 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33745 content = ("""
746 <!DOCTYPE html>
747 <html>
748 <head>
749 <!-- HTML meta refresh URL redirection -->
750 <meta http-equiv="refresh" content="0; url=%s">
751 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37752 </html>""" % to_html_relative_path)
753 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33754 f.write(content)
755
756
Max Moroz7c5354f2018-05-06 00:03:48757def _CleanUpOutputDir():
758 """Perform a cleanup of the output dir."""
759 # Remove the default index.html file produced by llvm-cov.
760 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
761 if os.path.exists(index_path):
762 os.remove(index_path)
763
764
Yuke Liaodd1ec0592018-02-02 01:26:37765def _GetCoverageHtmlReportPathForFile(file_path):
766 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40767 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37768 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
769
770 # '+' is used instead of os.path.join because both of them are absolute paths
771 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14772 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37773 return _GetCoverageReportRootDirPath() + html_report_path
774
775
776def _GetCoverageHtmlReportPathForDirectory(dir_path):
777 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40778 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37779 html_report_path = os.path.join(
780 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
781
782 # '+' is used instead of os.path.join because both of them are absolute paths
783 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14784 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37785 return _GetCoverageReportRootDirPath() + html_report_path
786
787
788def _GetCoverageHtmlReportPathForComponent(component_name):
789 """Given a component, returns the corresponding html report path."""
790 component_file_name = component_name.lower().replace('>', '-')
791 html_report_name = os.extsep.join([component_file_name, 'html'])
792 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
793 html_report_name)
794
795
796def _GetCoverageReportRootDirPath():
797 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48798 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
799
800
801def _GetComponentViewPath():
802 """Path to the HTML file for the component view."""
803 return os.path.join(_GetCoverageReportRootDirPath(),
804 COMPONENT_VIEW_INDEX_FILE)
805
806
807def _GetDirectoryViewPath():
808 """Path to the HTML file for the directory view."""
809 return os.path.join(_GetCoverageReportRootDirPath(),
810 DIRECTORY_VIEW_INDEX_FILE)
811
812
813def _GetFileViewPath():
814 """Path to the HTML file for the file view."""
815 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
816
817
818def _GetLogsDirectoryPath():
819 """Path to the logs directory."""
820 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
821
822
823def _GetHtmlIndexPath():
824 """Path to the main HTML index file."""
825 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
826
827
828def _GetProfdataFilePath():
829 """Path to the resulting .profdata file."""
830 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
831
832
833def _GetSummaryFilePath():
834 """The JSON file that contains coverage summary written by llvm-cov export."""
835 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33836
837
Yuke Liao506e8822017-12-04 16:52:54838def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
839 """Builds and runs target to generate the coverage profile data.
840
841 Args:
842 targets: A list of targets to build with coverage instrumentation.
843 commands: A list of commands used to run the targets.
844 jobs_count: Number of jobs to run in parallel for building. If None, a
845 default value is derived based on CPUs availability.
846
847 Returns:
848 A relative path to the generated profdata file.
849 """
850 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02851 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59852 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02853 coverage_profdata_file_path = (
854 _CreateCoverageProfileDataFromTargetProfDataFiles(
855 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54856
Abhishek Aryac19bc5ef2018-05-04 22:10:02857 for target_profdata_file_path in target_profdata_file_paths:
858 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52859
Abhishek Aryac19bc5ef2018-05-04 22:10:02860 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54861
862
863def _BuildTargets(targets, jobs_count):
864 """Builds target with Clang coverage instrumentation.
865
866 This function requires current working directory to be the root of checkout.
867
868 Args:
869 targets: A list of targets to build with coverage instrumentation.
870 jobs_count: Number of jobs to run in parallel for compilation. If None, a
871 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54872 """
Abhishek Arya1ec832c2017-12-05 18:06:59873
Yuke Liao506e8822017-12-04 16:52:54874 def _IsGomaConfigured():
875 """Returns True if goma is enabled in the gn build args.
876
877 Returns:
878 A boolean indicates whether goma is configured for building or not.
879 """
Yuke Liao80afff32018-03-07 01:26:20880 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54881 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
882
Abhishek Aryafb70b532018-05-06 17:47:40883 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54884 if jobs_count is None and _IsGomaConfigured():
885 jobs_count = DEFAULT_GOMA_JOBS
886
887 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
888 if jobs_count is not None:
889 subprocess_cmd.append('-j' + str(jobs_count))
890
891 subprocess_cmd.extend(targets)
892 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40893 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54894
895
Abhishek Aryac19bc5ef2018-05-04 22:10:02896def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54897 """Runs commands and returns the relative paths to the profraw data files.
898
899 Args:
900 targets: A list of targets built with coverage instrumentation.
901 commands: A list of commands used to run the targets.
902
903 Returns:
904 A list of relative paths to the generated profraw data files.
905 """
Abhishek Aryafb70b532018-05-06 17:47:40906 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10907
Yuke Liao506e8822017-12-04 16:52:54908 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48909 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54910 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48911 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
912
913 # Ensure that logs directory exists.
914 if not os.path.exists(_GetLogsDirectoryPath()):
915 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54916
Abhishek Aryac19bc5ef2018-05-04 22:10:02917 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10918
Yuke Liaod4a9865202018-01-12 23:17:52919 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54920 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48921 output_file_name = os.extsep.join([target + '_output', 'log'])
922 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10923
Abhishek Aryac19bc5ef2018-05-04 22:10:02924 profdata_file_path = None
925 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40926 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02927 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10928
Abhishek Aryac19bc5ef2018-05-04 22:10:02929 if _IsIOSCommand(command):
930 # On iOS platform, due to lack of write permissions, profraw files are
931 # generated outside of the OUTPUT_DIR, and the exact paths are contained
932 # in the output of the command execution.
933 output = _ExecuteIOSCommand(target, command)
934 else:
935 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
936 output = _ExecuteCommand(target, command)
937
938 with open(output_file_path, 'w') as output_file:
939 output_file.write(output)
940
941 profraw_file_paths = []
942 if _IsIOS():
943 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
944 else:
Max Moroz7c5354f2018-05-06 00:03:48945 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02946 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48947 profraw_file_paths.append(
948 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02949
950 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40951 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02952 'please make sure the binary exists and is properly '
953 'instrumented.' % target)
954
955 try:
956 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
957 target, profraw_file_paths)
958 break
959 except Exception:
960 print('Retrying...')
961 finally:
962 # Remove profraw files now so that they are not used in next iteration.
963 for profraw_file_path in profraw_file_paths:
964 os.remove(profraw_file_path)
965
966 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40967 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02968 'Please file a bug with command you used, commit position and args.gn '
969 'config here: '
970 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40971 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02972 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54973
Abhishek Aryafb70b532018-05-06 17:47:40974 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10975
Abhishek Aryac19bc5ef2018-05-04 22:10:02976 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54977
978
979def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10980 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52981 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01982 #
Max Morozd73e45f2018-04-24 18:32:47983 # "%p" expands out to the process ID. It's not used by this scripts due to:
984 # 1) If a target program spawns too many processess, it may exhaust all disk
985 # space available. For example, unit_tests writes thousands of .profraw
986 # files each of size 1GB+.
987 # 2) If a target binary uses shared libraries, coverage profile data for them
988 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01989 #
Yuke Liaod4a9865202018-01-12 23:17:52990 # "%Nm" expands out to the instrumented binary's signature. When this pattern
991 # is specified, the runtime creates a pool of N raw profiles which are used
992 # for on-line profile merging. The runtime takes care of selecting a raw
993 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52994 # N must be between 1 and 9. The merge pool specifier can only occur once per
995 # filename pattern.
996 #
Max Morozd73e45f2018-04-24 18:32:47997 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01998 #
Max Morozd73e45f2018-04-24 18:32:47999 # For other cases, "%4m" is chosen as it creates some level of parallelism,
1000 # but it's not too big to consume too much computing resource or disk space.
1001 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:591002 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011003 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481004 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541005 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541006
Yuke Liaoa0c8c2f2018-02-28 20:14:101007 try:
Max Moroz7c5354f2018-05-06 00:03:481008 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101009 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291010 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481011 stderr=subprocess.STDOUT,
Yuke Liaob2926832018-03-02 17:34:291012 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101013 except subprocess.CalledProcessError as e:
1014 output = e.output
Abhishek Aryafb70b532018-05-06 17:47:401015 logging.warning('Command: "%s" exited with non-zero return code.', command)
Yuke Liaoa0c8c2f2018-02-28 20:14:101016
1017 return output
1018
1019
Yuke Liao27349c92018-03-22 21:10:011020def _IsFuzzerTarget(target):
1021 """Returns true if the target is a fuzzer target."""
1022 build_args = _GetBuildArgs()
1023 use_libfuzzer = ('use_libfuzzer' in build_args and
1024 build_args['use_libfuzzer'] == 'true')
1025 return use_libfuzzer and target.endswith('_fuzzer')
1026
1027
Yuke Liaob2926832018-03-02 17:34:291028def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101029 """Runs a single iOS command and generates a profraw data file.
1030
1031 iOS application doesn't have write access to folders outside of the app, so
1032 it's impossible to instruct the app to flush the profraw data file to the
1033 desired location. The profraw data file will be generated somewhere within the
1034 application's Documents folder, and the full path can be obtained by parsing
1035 the output.
1036 """
Yuke Liaob2926832018-03-02 17:34:291037 assert _IsIOSCommand(command)
1038
1039 # After running tests, iossim generates a profraw data file, it won't be
1040 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1041 # checkout.
1042 iossim_profraw_file_path = os.path.join(
1043 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101044
1045 try:
Yuke Liaob2926832018-03-02 17:34:291046 output = subprocess.check_output(
1047 shlex.split(command),
1048 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101049 except subprocess.CalledProcessError as e:
1050 # iossim emits non-zero return code even if tests run successfully, so
1051 # ignore the return code.
1052 output = e.output
1053
1054 return output
1055
1056
1057def _GetProfrawDataFileByParsingOutput(output):
1058 """Returns the path to the profraw data file obtained by parsing the output.
1059
1060 The output of running the test target has no format, but it is guaranteed to
1061 have a single line containing the path to the generated profraw data file.
1062 NOTE: This should only be called when target os is iOS.
1063 """
Yuke Liaob2926832018-03-02 17:34:291064 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101065
Yuke Liaob2926832018-03-02 17:34:291066 output_by_lines = ''.join(output).splitlines()
1067 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101068
1069 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291070 result = profraw_file_pattern.match(line)
1071 if result:
1072 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101073
1074 assert False, ('No profraw data file was generated, did you call '
1075 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1076 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541077
1078
Abhishek Aryac19bc5ef2018-05-04 22:10:021079def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1080 """Returns a relative path to coverage profdata file by merging target
1081 profdata files.
Yuke Liao506e8822017-12-04 16:52:541082
1083 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021084 profdata_file_paths: A list of relative paths to the profdata data files
1085 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541086
1087 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021088 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541089
1090 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021091 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541092 """
Abhishek Aryafb70b532018-05-06 17:47:401093 logging.info('Creating the coverage profile data file.')
1094 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481095 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541096 try:
Abhishek Arya1ec832c2017-12-05 18:06:591097 subprocess_cmd = [
1098 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1099 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021100 subprocess_cmd.extend(profdata_file_paths)
1101 subprocess.check_call(subprocess_cmd)
1102 except subprocess.CalledProcessError as error:
1103 print('Failed to merge target profdata files to create coverage profdata. '
1104 'Try again.')
1105 raise error
1106
Abhishek Aryafb70b532018-05-06 17:47:401107 logging.debug('Finished merging target profdata files.')
1108 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021109 profdata_file_path)
1110 return profdata_file_path
1111
1112
1113def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1114 """Returns a relative path to target profdata file by merging target
1115 profraw files.
1116
1117 Args:
1118 profraw_file_paths: A list of relative paths to the profdata data files
1119 that are to be merged.
1120
1121 Returns:
1122 A relative path to the merged coverage profdata file.
1123
1124 Raises:
1125 CalledProcessError: An error occurred merging profdata files.
1126 """
Abhishek Aryafb70b532018-05-06 17:47:401127 logging.info('Creating target profile data file.')
1128 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021129 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1130
1131 try:
1132 subprocess_cmd = [
1133 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1134 ]
Yuke Liao506e8822017-12-04 16:52:541135 subprocess_cmd.extend(profraw_file_paths)
1136 subprocess.check_call(subprocess_cmd)
1137 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021138 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541139 raise error
1140
Abhishek Aryafb70b532018-05-06 17:47:401141 logging.debug('Finished merging target profraw files.')
1142 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101143 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541144 return profdata_file_path
1145
1146
Yuke Liao0e4c8682018-04-18 21:06:591147def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1148 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331149 """Generates per file coverage summary using "llvm-cov export" command."""
1150 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1151 # [[-object BIN]] [SOURCES].
1152 # NOTE: For object files, the first one is specified as a positional argument,
1153 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101154 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401155 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331156 subprocess_cmd = [
1157 LLVM_COV_PATH, 'export', '-summary-only',
1158 '-instr-profile=' + profdata_file_path, binary_paths[0]
1159 ]
1160 subprocess_cmd.extend(
1161 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291162 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331163 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591164 if ignore_filename_regex:
1165 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331166
Max Moroz7c5354f2018-05-06 00:03:481167 export_output = subprocess.check_output(subprocess_cmd)
1168
1169 # Write output on the disk to be used by code coverage bot.
1170 with open(_GetSummaryFilePath(), 'w') as f:
1171 f.write(export_output)
1172
1173 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331174 assert len(json_output['data']) == 1
1175 files_coverage_data = json_output['data'][0]['files']
1176
1177 per_file_coverage_summary = {}
1178 for file_coverage_data in files_coverage_data:
1179 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401180 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1181 'File path "%s" in coverage summary is outside source checkout.' %
1182 file_path)
Yuke Liaoea228d02018-01-05 19:10:331183
Abhishek Aryafb70b532018-05-06 17:47:401184 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331185 if summary['lines']['count'] == 0:
1186 continue
1187
1188 per_file_coverage_summary[file_path] = _CoverageSummary(
1189 regions_total=summary['regions']['count'],
1190 regions_covered=summary['regions']['covered'],
1191 functions_total=summary['functions']['count'],
1192 functions_covered=summary['functions']['covered'],
1193 lines_total=summary['lines']['count'],
1194 lines_covered=summary['lines']['covered'])
1195
Abhishek Aryafb70b532018-05-06 17:47:401196 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331197 return per_file_coverage_summary
1198
1199
Yuke Liaob2926832018-03-02 17:34:291200def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1201 """Appends -arch arguments to the command list if it's ios platform.
1202
1203 iOS binaries are universal binaries, and require specifying the architecture
1204 to use, and one architecture needs to be specified for each binary.
1205 """
1206 if _IsIOS():
1207 cmd_list.extend(['-arch=x86_64'] * num_archs)
1208
1209
Yuke Liao506e8822017-12-04 16:52:541210def _GetBinaryPath(command):
1211 """Returns a relative path to the binary to be run by the command.
1212
Yuke Liao545db322018-02-15 17:12:011213 Currently, following types of commands are supported (e.g. url_unittests):
1214 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1215 2. Use xvfb.
1216 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1217 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371218 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1219 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101220 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371221 <iossim_arguments> -c <app_arguments>
1222 out/Coverage-iphonesimulator/url_unittests.app"
1223
Yuke Liao545db322018-02-15 17:12:011224
Yuke Liao506e8822017-12-04 16:52:541225 Args:
1226 command: A command used to run a target.
1227
1228 Returns:
1229 A relative path to the binary.
1230 """
Yuke Liao545db322018-02-15 17:12:011231 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1232
Yuke Liaob2926832018-03-02 17:34:291233 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011234 if os.path.basename(command_parts[0]) == 'python':
1235 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401236 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011237 return command_parts[2]
1238
1239 if os.path.basename(command_parts[0]) == xvfb_script_name:
1240 return command_parts[1]
1241
Yuke Liaob2926832018-03-02 17:34:291242 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101243 # For a given application bundle, the binary resides in the bundle and has
1244 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371245 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101246 app_name = os.path.splitext(os.path.basename(app_path))[0]
1247 return os.path.join(app_path, app_name)
1248
Yuke Liaob2926832018-03-02 17:34:291249 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541250
1251
Yuke Liaob2926832018-03-02 17:34:291252def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101253 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291254 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101255
1256
Yuke Liao95d13d72017-12-07 18:18:501257def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1258 """Verifies that the target executables specified in the commands are inside
1259 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541260 for command in commands:
1261 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501262 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481263 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501264 'Target executable "%s" in command: "%s" is outside of '
1265 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541266
1267
1268def _ValidateBuildingWithClangCoverage():
1269 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201270 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541271
1272 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1273 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591274 assert False, ('\'{} = true\' is required in args.gn.'
1275 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541276
1277
Yuke Liaoc60b2d02018-03-02 21:40:431278def _ValidateCurrentPlatformIsSupported():
1279 """Asserts that this script suports running on the current platform"""
1280 target_os = _GetTargetOS()
1281 if target_os:
1282 current_platform = target_os
1283 else:
1284 current_platform = _GetHostPlatform()
1285
1286 assert current_platform in [
1287 'linux', 'mac', 'chromeos', 'ios'
1288 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1289
1290
Yuke Liao80afff32018-03-07 01:26:201291def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541292 """Parses args.gn file and returns results as a dictionary.
1293
1294 Returns:
1295 A dictionary representing the build args.
1296 """
Yuke Liao80afff32018-03-07 01:26:201297 global _BUILD_ARGS
1298 if _BUILD_ARGS is not None:
1299 return _BUILD_ARGS
1300
1301 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541302 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1303 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1304 'missing args.gn file.' % BUILD_DIR)
1305 with open(build_args_path) as build_args_file:
1306 build_args_lines = build_args_file.readlines()
1307
Yuke Liao506e8822017-12-04 16:52:541308 for build_arg_line in build_args_lines:
1309 build_arg_without_comments = build_arg_line.split('#')[0]
1310 key_value_pair = build_arg_without_comments.split('=')
1311 if len(key_value_pair) != 2:
1312 continue
1313
1314 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431315
1316 # Values are wrapped within a pair of double-quotes, so remove the leading
1317 # and trailing double-quotes.
1318 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201319 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541320
Yuke Liao80afff32018-03-07 01:26:201321 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541322
1323
Abhishek Arya16f059a2017-12-07 17:47:321324def _VerifyPathsAndReturnAbsolutes(paths):
1325 """Verifies that the paths specified in |paths| exist and returns absolute
1326 versions.
Yuke Liao66da1732017-12-05 22:19:421327
1328 Args:
1329 paths: A list of files or directories.
1330 """
Abhishek Arya16f059a2017-12-07 17:47:321331 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421332 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321333 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1334 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1335
1336 absolute_paths.append(absolute_path)
1337
1338 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421339
1340
Yuke Liaodd1ec0592018-02-02 01:26:371341def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1342 """Returns a target path relative to the directory of base_path.
1343
1344 This method requires base_path to be a file, otherwise, one should call
1345 os.path.relpath directly.
1346 """
1347 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141348 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371349 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141350 base_dir = os.path.dirname(base_path)
1351 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371352
1353
Abhishek Arya64636af2018-05-04 14:42:131354def _GetBinaryPathsFromTargets(targets, build_dir):
1355 """Return binary paths from target names."""
1356 # FIXME: Derive output binary from target build definitions rather than
1357 # assuming that it is always the same name.
1358 binary_paths = []
1359 for target in targets:
1360 binary_path = os.path.join(build_dir, target)
1361 if _GetHostPlatform() == 'win':
1362 binary_path += '.exe'
1363
1364 if os.path.exists(binary_path):
1365 binary_paths.append(binary_path)
1366 else:
1367 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401368 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131369 os.path.basename(binary_path))
1370
1371 return binary_paths
1372
1373
Yuke Liao506e8822017-12-04 16:52:541374def _ParseCommandArguments():
1375 """Adds and parses relevant arguments for tool comands.
1376
1377 Returns:
1378 A dictionary representing the arguments.
1379 """
1380 arg_parser = argparse.ArgumentParser()
1381 arg_parser.usage = __doc__
1382
Abhishek Arya1ec832c2017-12-05 18:06:591383 arg_parser.add_argument(
1384 '-b',
1385 '--build-dir',
1386 type=str,
1387 required=True,
1388 help='The build directory, the path needs to be relative to the root of '
1389 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541390
Abhishek Arya1ec832c2017-12-05 18:06:591391 arg_parser.add_argument(
1392 '-o',
1393 '--output-dir',
1394 type=str,
1395 required=True,
1396 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541397
Abhishek Arya1ec832c2017-12-05 18:06:591398 arg_parser.add_argument(
1399 '-c',
1400 '--command',
1401 action='append',
Abhishek Arya64636af2018-05-04 14:42:131402 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591403 help='Commands used to run test targets, one test target needs one and '
1404 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131405 'current working directory is the root of the checkout. This option is '
1406 'incompatible with -p/--profdata-file option.')
1407
1408 arg_parser.add_argument(
1409 '-p',
1410 '--profdata-file',
1411 type=str,
1412 required=False,
1413 help='Path to profdata file to use for generating code coverage reports. '
1414 'This can be useful if you generated the profdata file seperately in '
1415 'your own test harness. This option is ignored if run command(s) are '
1416 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541417
Abhishek Arya1ec832c2017-12-05 18:06:591418 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421419 '-f',
1420 '--filters',
1421 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321422 required=False,
Yuke Liao66da1732017-12-05 22:19:421423 help='Directories or files to get code coverage for, and all files under '
1424 'the directories are included recursively.')
1425
1426 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591427 '-i',
1428 '--ignore-filename-regex',
1429 type=str,
1430 help='Skip source code files with file paths that match the given '
1431 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1432 'to exclude files in third_party/ and out/ folders from the report.')
1433
1434 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591435 '-j',
1436 '--jobs',
1437 type=int,
1438 default=None,
1439 help='Run N jobs to build in parallel. If not specified, a default value '
1440 'will be derived based on CPUs availability. Please refer to '
1441 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541442
Abhishek Arya1ec832c2017-12-05 18:06:591443 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101444 '-v',
1445 '--verbose',
1446 action='store_true',
1447 help='Prints additional output for diagnostics.')
1448
1449 arg_parser.add_argument(
1450 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1451
1452 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021453 'targets',
1454 nargs='+',
1455 help='The names of the test targets to run. If multiple run commands are '
1456 'specified using the -c/--command option, then the order of targets and '
1457 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541458
1459 args = arg_parser.parse_args()
1460 return args
1461
1462
1463def Main():
1464 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131465 # Change directory to source root to aid in relative paths calculations.
1466 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111467
Abhishek Arya64636af2018-05-04 14:42:131468 # Setup coverage binaries even when script is called with empty params. This
1469 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111470 DownloadCoverageToolsIfNeeded()
1471
Yuke Liao506e8822017-12-04 16:52:541472 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131473 _ConfigureLogging(args)
1474
Yuke Liao506e8822017-12-04 16:52:541475 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481476 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541477 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481478 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541479
Abhishek Arya64636af2018-05-04 14:42:131480 assert args.command or args.profdata_file, (
1481 'Need to either provide commands to run using -c/--command option OR '
1482 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431483
Abhishek Arya64636af2018-05-04 14:42:131484 assert not args.command or (len(args.targets) == len(args.command)), (
1485 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431486
Abhishek Arya1ec832c2017-12-05 18:06:591487 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401488 'Build directory: "%s" doesn\'t exist. '
1489 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131490
Yuke Liaoc60b2d02018-03-02 21:40:431491 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541492 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321493
1494 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421495 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321496 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421497
Max Moroz7c5354f2018-05-06 00:03:481498 if not os.path.exists(_GetCoverageReportRootDirPath()):
1499 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541500
Abhishek Arya64636af2018-05-04 14:42:131501 # Get profdate file and list of binary paths.
1502 if args.command:
1503 # A list of commands are provided. Run them to generate profdata file, and
1504 # create a list of binary paths from parsing commands.
1505 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1506 profdata_file_path = _CreateCoverageProfileDataForTargets(
1507 args.targets, args.command, args.jobs)
1508 binary_paths = [_GetBinaryPath(command) for command in args.command]
1509 else:
1510 # An input prof-data file is already provided. Just calculate binary paths.
1511 profdata_file_path = args.profdata_file
1512 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331513
Abhishek Arya78120bc2018-05-07 20:53:541514 binary_paths.extend(_GetSharedLibraries(binary_paths))
1515
Yuke Liao481d3482018-01-29 19:17:101516 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401517 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371518 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591519 binary_paths, profdata_file_path, absolute_filter_paths,
1520 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371521 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591522 absolute_filter_paths,
1523 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371524 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1525
1526 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1527 per_file_coverage_summary)
1528 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1529 per_file_coverage_summary)
1530 _GenerateDirectoryViewHtmlIndexFile()
1531
1532 component_to_directories = _ExtractComponentToDirectoriesMapping()
1533 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1534 component_to_directories, per_directory_coverage_summary)
1535 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1536 component_to_directories,
1537 per_directory_coverage_summary)
1538 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331539
1540 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371541 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331542 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481543 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331544
Max Moroz7c5354f2018-05-06 00:03:481545 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401546 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101547 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541548
Abhishek Arya1ec832c2017-12-05 18:06:591549
Yuke Liao506e8822017-12-04 16:52:541550if __name__ == '__main__':
1551 sys.exit(Main())