blob: 5a707cf6974b6637c95f604efb72b562f6d61be0 [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')
216
217 def AddLinkToAnotherReport(self, html_report_path, name, summary):
218 """Adds a link to another html report in this report.
219
220 The link to be added is assumed to be an entry in this directory.
221 """
Yuke Liaodd1ec0592018-02-02 01:26:37222 # Use relative paths instead of absolute paths to make the generated reports
223 # portable.
224 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
225 html_report_path, self._output_path)
226
Yuke Liaod54030e2018-01-08 17:34:12227 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37228 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12229 os.path.basename(html_report_path) ==
230 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
231 self._table_entries.append(table_entry)
232
233 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35234 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12235 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
236
237 def _CreateTableEntryFromCoverageSummary(self,
238 summary,
239 href=None,
240 name=None,
241 is_dir=None):
242 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37243 assert (href is None and name is None and is_dir is None) or (
244 href is not None and name is not None and is_dir is not None), (
245 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35246 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37247 'attributes must be None.')
248
Yuke Liaod54030e2018-01-08 17:34:12249 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37250 if href is not None:
251 entry['href'] = href
252 if name is not None:
253 entry['name'] = name
254 if is_dir is not None:
255 entry['is_dir'] = is_dir
256
Yuke Liaoea228d02018-01-05 19:10:33257 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12258 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37259 if summary_dict[feature]['total'] == 0:
260 percentage = 0.0
261 else:
Yuke Liao0e4c8682018-04-18 21:06:59262 percentage = float(summary_dict[feature]
263 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35264
Yuke Liaoea228d02018-01-05 19:10:33265 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12266 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33267 'total': summary_dict[feature]['total'],
268 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35269 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33270 'color_class': color_class
271 }
Yuke Liaod54030e2018-01-08 17:34:12272
Yuke Liaod54030e2018-01-08 17:34:12273 return entry
Yuke Liaoea228d02018-01-05 19:10:33274
275 def _GetColorClass(self, percentage):
276 """Returns the css color class based on coverage percentage."""
277 if percentage >= 0 and percentage < 80:
278 return 'red'
279 if percentage >= 80 and percentage < 100:
280 return 'yellow'
281 if percentage == 100:
282 return 'green'
283
Abhishek Aryafb70b532018-05-06 17:47:40284 assert False, 'Invalid coverage percentage: "%d".' % percentage
Yuke Liaoea228d02018-01-05 19:10:33285
Yuke Liaodd1ec0592018-02-02 01:26:37286 def WriteHtmlCoverageReport(self):
287 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33288
289 In the report, sub-directories are displayed before files and within each
290 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33291 """
292
293 def EntryCmp(left, right):
294 """Compare function for table entries."""
295 if left['is_dir'] != right['is_dir']:
296 return -1 if left['is_dir'] == True else 1
297
Yuke Liaodd1ec0592018-02-02 01:26:37298 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33299
300 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
301
302 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Max Moroz7c5354f2018-05-06 00:03:48303
304 directory_view_path = _GetDirectoryViewPath()
305 component_view_path = _GetComponentViewPath()
306 file_view_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37307
Yuke Liaoea228d02018-01-05 19:10:33308 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37309 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
310 directory_view_href=_GetRelativePathToDirectoryOfFile(
311 directory_view_path, self._output_path),
312 component_view_href=_GetRelativePathToDirectoryOfFile(
313 component_view_path, self._output_path),
314 file_view_href=_GetRelativePathToDirectoryOfFile(
315 file_view_path, self._output_path))
316
Yuke Liaod54030e2018-01-08 17:34:12317 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37318 entries=self._table_entries,
319 total_entry=self._total_entry,
320 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33321 html_footer = self._footer_template.render()
322
Yuke Liaodd1ec0592018-02-02 01:26:37323 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33324 html_file.write(html_header + html_table + html_footer)
325
Yuke Liao506e8822017-12-04 16:52:54326
Abhishek Arya64636af2018-05-04 14:42:13327def _ConfigureLogging(args):
328 """Configures logging settings for later use."""
329 log_level = logging.DEBUG if args.verbose else logging.INFO
330 log_format = '[%(asctime)s %(levelname)s] %(message)s'
331 log_file = args.log_file if args.log_file else None
332 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
333
334
Max Morozd73e45f2018-04-24 18:32:47335def _GetSharedLibraries(binary_paths):
Abhishek Arya78120bc2018-05-07 20:53:54336 """Returns list of shared libraries used by specified binaries."""
337 logging.info('Finding shared libraries for targets (if any).')
338 shared_libraries = []
Max Morozd73e45f2018-04-24 18:32:47339 cmd = []
340 shared_library_re = None
341
342 if sys.platform.startswith('linux'):
343 cmd.extend(['ldd'])
Abhishek Arya64636af2018-05-04 14:42:13344 shared_library_re = re.compile(r'.*\.so\s=>\s(.*' + BUILD_DIR +
345 r'.*\.so)\s.*')
Max Morozd73e45f2018-04-24 18:32:47346 elif sys.platform.startswith('darwin'):
347 cmd.extend(['otool', '-L'])
348 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
349 else:
Abhishek Aryafb70b532018-05-06 17:47:40350 assert False, 'Cannot detect shared libraries used by the given targets.'
Max Morozd73e45f2018-04-24 18:32:47351
352 assert shared_library_re is not None
353
354 cmd.extend(binary_paths)
355 output = subprocess.check_output(cmd)
356
357 for line in output.splitlines():
358 m = shared_library_re.match(line)
359 if not m:
360 continue
361
362 shared_library_path = m.group(1)
363 if sys.platform.startswith('darwin'):
364 # otool outputs "@rpath" macro instead of the dirname of the given binary.
365 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
366
Abhishek Arya78120bc2018-05-07 20:53:54367 if shared_library_path in shared_libraries:
368 continue
369
Max Morozd73e45f2018-04-24 18:32:47370 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
371 'the given target(s) does not '
372 'exist.' % shared_library_path)
373 with open(shared_library_path) as f:
374 data = f.read()
375
376 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
377 if '__llvm_cov' in data:
Abhishek Arya78120bc2018-05-07 20:53:54378 shared_libraries.append(shared_library_path)
Max Morozd73e45f2018-04-24 18:32:47379
Abhishek Arya78120bc2018-05-07 20:53:54380 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
381 shared_libraries)
382 logging.info('Finished finding shared libraries for targets.')
383 return shared_libraries
Max Morozd73e45f2018-04-24 18:32:47384
385
Yuke Liaoc60b2d02018-03-02 21:40:43386def _GetHostPlatform():
387 """Returns the host platform.
388
389 This is separate from the target platform/os that coverage is running for.
390 """
Abhishek Arya1ec832c2017-12-05 18:06:59391 if sys.platform == 'win32' or sys.platform == 'cygwin':
392 return 'win'
393 if sys.platform.startswith('linux'):
394 return 'linux'
395 else:
396 assert sys.platform == 'darwin'
397 return 'mac'
398
399
Yuke Liaoc60b2d02018-03-02 21:40:43400def _GetTargetOS():
401 """Returns the target os specified in args.gn file.
402
403 Returns an empty string is target_os is not specified.
404 """
Yuke Liao80afff32018-03-07 01:26:20405 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43406 return build_args['target_os'] if 'target_os' in build_args else ''
407
408
Yuke Liaob2926832018-03-02 17:34:29409def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10410 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43411 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10412
413
Yuke Liao506e8822017-12-04 16:52:54414# TODO(crbug.com/759794): remove this function once tools get included to
415# Clang bundle:
416# https://chromium-review.googlesource.com/c/chromium/src/+/688221
417def DownloadCoverageToolsIfNeeded():
418 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59419
Yuke Liaoc60b2d02018-03-02 21:40:43420 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54421 """Returns a pair of revision number by reading the build stamp file.
422
423 Args:
424 stamp_file_path: A path the build stamp file created by
425 tools/clang/scripts/update.py.
426 Returns:
427 A pair of integers represeting the main and sub revision respectively.
428 """
429 if not os.path.exists(stamp_file_path):
430 return 0, 0
431
432 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43433 stamp_file_line = stamp_file.readline()
434 if ',' in stamp_file_line:
435 package_version = stamp_file_line.rstrip().split(',')[0]
436 else:
437 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54438
Yuke Liaoc60b2d02018-03-02 21:40:43439 clang_revision_str, clang_sub_revision_str = package_version.split('-')
440 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59441
Yuke Liaoc60b2d02018-03-02 21:40:43442 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54443 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43444 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54445
446 coverage_revision_stamp_file = os.path.join(
447 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
448 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43449 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54450
Yuke Liaoea228d02018-01-05 19:10:33451 has_coverage_tools = (
452 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32453
Yuke Liaoea228d02018-01-05 19:10:33454 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54455 coverage_sub_revision == clang_sub_revision):
456 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43457 return
Yuke Liao506e8822017-12-04 16:52:54458
459 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
460 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
461
462 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43463 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54464 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43465 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54466 coverage_tools_url = (
467 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43468 else:
469 assert host_platform == 'win'
470 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54471
472 try:
473 clang_update.DownloadAndUnpack(coverage_tools_url,
474 clang_update.LLVM_BUILD_DIR)
Abhishek Aryafb70b532018-05-06 17:47:40475 logging.info('Coverage tools %s unpacked.', package_version)
Yuke Liao506e8822017-12-04 16:52:54476 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43477 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54478 file_handle.write('\n')
479 except urllib2.URLError:
480 raise Exception(
481 'Failed to download coverage tools: %s.' % coverage_tools_url)
482
483
Yuke Liaodd1ec0592018-02-02 01:26:37484def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59485 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54486 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
487
488 For a file with absolute path /a/b/x.cc, a html report is generated as:
489 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
490 OUTPUT_DIR/index.html.
491
492 Args:
493 binary_paths: A list of paths to the instrumented binaries.
494 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42495 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54496 """
Yuke Liao506e8822017-12-04 16:52:54497 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
498 # [[-object BIN]] [SOURCES]
499 # NOTE: For object files, the first one is specified as a positional argument,
500 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10501 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40502 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59503 subprocess_cmd = [
504 LLVM_COV_PATH, 'show', '-format=html',
505 '-output-dir={}'.format(OUTPUT_DIR),
506 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
507 ]
508 subprocess_cmd.extend(
509 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29510 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42511 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59512 if ignore_filename_regex:
513 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
514
Yuke Liao506e8822017-12-04 16:52:54515 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34516
517 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
518 # the platform name instead, as it simplifies the report dir structure when
519 # the same report is generated for different platforms.
520 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
Max Moroz7c5354f2018-05-06 00:03:48521 platform_report_subdir_path = _GetCoverageReportRootDirPath()
522 _MergeTwoDirectories(default_report_subdir_path, platform_report_subdir_path)
Max Moroz025d8952018-05-03 16:33:34523
Abhishek Aryafb70b532018-05-06 17:47:40524 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54525
526
Yuke Liaodd1ec0592018-02-02 01:26:37527def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
528 """Generates html index file for file view."""
Max Moroz7c5354f2018-05-06 00:03:48529 file_view_index_file_path = _GetFileViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37530 logging.debug('Generating file view html index file as: "%s".',
531 file_view_index_file_path)
532 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
533 'Path')
534 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33535
Yuke Liaodd1ec0592018-02-02 01:26:37536 for file_path in per_file_coverage_summary:
537 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
538
539 html_generator.AddLinkToAnotherReport(
540 _GetCoverageHtmlReportPathForFile(file_path),
541 os.path.relpath(file_path, SRC_ROOT_PATH),
542 per_file_coverage_summary[file_path])
543
544 html_generator.CreateTotalsEntry(totals_coverage_summary)
545 html_generator.WriteHtmlCoverageReport()
546 logging.debug('Finished generating file view html index file.')
547
548
549def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
550 """Calculates per directory coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40551 logging.debug('Calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37552 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
553
Yuke Liaoea228d02018-01-05 19:10:33554 for file_path in per_file_coverage_summary:
555 summary = per_file_coverage_summary[file_path]
556 parent_dir = os.path.dirname(file_path)
Abhishek Aryafb70b532018-05-06 17:47:40557
Yuke Liaoea228d02018-01-05 19:10:33558 while True:
559 per_directory_coverage_summary[parent_dir].AddSummary(summary)
560
561 if parent_dir == SRC_ROOT_PATH:
562 break
563 parent_dir = os.path.dirname(parent_dir)
564
Abhishek Aryafb70b532018-05-06 17:47:40565 logging.debug('Finished calculating per-directory coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37566 return per_directory_coverage_summary
567
568
569def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
570 per_file_coverage_summary):
571 """Generates per directory coverage breakdown in html."""
Abhishek Aryafb70b532018-05-06 17:47:40572 logging.debug('Writing per-directory coverage html reports.')
Yuke Liaoea228d02018-01-05 19:10:33573 for dir_path in per_directory_coverage_summary:
574 _GenerateCoverageInHtmlForDirectory(
575 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
576
Abhishek Aryafb70b532018-05-06 17:47:40577 logging.debug('Finished writing per-directory coverage html reports.')
Yuke Liao481d3482018-01-29 19:17:10578
Yuke Liaoea228d02018-01-05 19:10:33579
580def _GenerateCoverageInHtmlForDirectory(
581 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
582 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37583 html_generator = _CoverageReportHtmlGenerator(
584 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33585
586 for entry_name in os.listdir(dir_path):
587 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33588
Yuke Liaodd1ec0592018-02-02 01:26:37589 if entry_path in per_file_coverage_summary:
590 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
591 entry_coverage_summary = per_file_coverage_summary[entry_path]
592 elif entry_path in per_directory_coverage_summary:
593 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
594 entry_path)
595 entry_coverage_summary = per_directory_coverage_summary[entry_path]
596 else:
Yuke Liaoc7e607142018-02-05 20:26:14597 # Any file without executable lines shouldn't be included into the report.
598 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37599 continue
Yuke Liaoea228d02018-01-05 19:10:33600
Yuke Liaodd1ec0592018-02-02 01:26:37601 html_generator.AddLinkToAnotherReport(entry_html_report_path,
602 os.path.basename(entry_path),
603 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33604
Yuke Liaod54030e2018-01-08 17:34:12605 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37606 html_generator.WriteHtmlCoverageReport()
607
608
609def _GenerateDirectoryViewHtmlIndexFile():
610 """Generates the html index file for directory view.
611
612 Note that the index file is already generated under SRC_ROOT_PATH, so this
613 file simply redirects to it, and the reason of this extra layer is for
614 structural consistency with other views.
615 """
Max Moroz7c5354f2018-05-06 00:03:48616 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37617 logging.debug('Generating directory view html index file as: "%s".',
618 directory_view_index_file_path)
619 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
620 SRC_ROOT_PATH)
621 _WriteRedirectHtmlFile(directory_view_index_file_path,
622 src_root_html_report_path)
623 logging.debug('Finished generating directory view html index file.')
624
625
626def _CalculatePerComponentCoverageSummary(component_to_directories,
627 per_directory_coverage_summary):
628 """Calculates per component coverage summary."""
Abhishek Aryafb70b532018-05-06 17:47:40629 logging.debug('Calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37630 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
631
632 for component in component_to_directories:
633 for directory in component_to_directories[component]:
634 absolute_directory_path = os.path.abspath(directory)
635 if absolute_directory_path in per_directory_coverage_summary:
636 per_component_coverage_summary[component].AddSummary(
637 per_directory_coverage_summary[absolute_directory_path])
638
Abhishek Aryafb70b532018-05-06 17:47:40639 logging.debug('Finished calculating per-component coverage summary.')
Yuke Liaodd1ec0592018-02-02 01:26:37640 return per_component_coverage_summary
641
642
643def _ExtractComponentToDirectoriesMapping():
644 """Returns a mapping from components to directories."""
645 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
646 directory_to_component = component_mappings['dir-to-component']
647
648 component_to_directories = defaultdict(list)
649 for directory in directory_to_component:
650 component = directory_to_component[directory]
651 component_to_directories[component].append(directory)
652
653 return component_to_directories
654
655
656def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
657 component_to_directories,
658 per_directory_coverage_summary):
659 """Generates per-component coverage reports in html."""
660 logging.debug('Writing per-component coverage html reports.')
661 for component in per_component_coverage_summary:
662 _GenerateCoverageInHtmlForComponent(
663 component, per_component_coverage_summary, component_to_directories,
664 per_directory_coverage_summary)
665
666 logging.debug('Finished writing per-component coverage html reports.')
667
668
669def _GenerateCoverageInHtmlForComponent(
670 component_name, per_component_coverage_summary, component_to_directories,
671 per_directory_coverage_summary):
672 """Generates coverage html report for a component."""
673 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
674 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14675 component_html_report_dir = os.path.dirname(component_html_report_path)
676 if not os.path.exists(component_html_report_dir):
677 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37678
679 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
680 'Path')
681
682 for dir_path in component_to_directories[component_name]:
683 dir_absolute_path = os.path.abspath(dir_path)
684 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14685 # Any directory without an excercised file shouldn't be included into the
686 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37687 continue
688
689 html_generator.AddLinkToAnotherReport(
690 _GetCoverageHtmlReportPathForDirectory(dir_path),
691 os.path.relpath(dir_path, SRC_ROOT_PATH),
692 per_directory_coverage_summary[dir_absolute_path])
693
694 html_generator.CreateTotalsEntry(
695 per_component_coverage_summary[component_name])
696 html_generator.WriteHtmlCoverageReport()
697
698
699def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
700 """Generates the html index file for component view."""
Max Moroz7c5354f2018-05-06 00:03:48701 component_view_index_file_path = _GetComponentViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37702 logging.debug('Generating component view html index file as: "%s".',
703 component_view_index_file_path)
704 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
705 'Component')
706 totals_coverage_summary = _CoverageSummary()
707
708 for component in per_component_coverage_summary:
709 totals_coverage_summary.AddSummary(
710 per_component_coverage_summary[component])
711
712 html_generator.AddLinkToAnotherReport(
713 _GetCoverageHtmlReportPathForComponent(component), component,
714 per_component_coverage_summary[component])
715
716 html_generator.CreateTotalsEntry(totals_coverage_summary)
717 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14718 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33719
720
Max Moroz7c5354f2018-05-06 00:03:48721def _MergeTwoDirectories(src_path, dst_path):
722 """Merge src_path directory into dst_path directory."""
723 for filename in os.listdir(src_path):
724 dst_path = os.path.join(dst_path, filename)
725 if os.path.exists(dst_path):
726 shutil.rmtree(dst_path)
727 os.rename(os.path.join(src_path, filename), dst_path)
728 shutil.rmtree(src_path)
729
730
Yuke Liaoea228d02018-01-05 19:10:33731def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37732 """Overwrites the root index file to redirect to the default view."""
Max Moroz7c5354f2018-05-06 00:03:48733 html_index_file_path = _GetHtmlIndexPath()
734 directory_view_index_file_path = _GetDirectoryViewPath()
Yuke Liaodd1ec0592018-02-02 01:26:37735 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
736
737
738def _WriteRedirectHtmlFile(from_html_path, to_html_path):
739 """Writes a html file that redirects to another html file."""
740 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
741 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33742 content = ("""
743 <!DOCTYPE html>
744 <html>
745 <head>
746 <!-- HTML meta refresh URL redirection -->
747 <meta http-equiv="refresh" content="0; url=%s">
748 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37749 </html>""" % to_html_relative_path)
750 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33751 f.write(content)
752
753
Max Moroz7c5354f2018-05-06 00:03:48754def _CleanUpOutputDir():
755 """Perform a cleanup of the output dir."""
756 # Remove the default index.html file produced by llvm-cov.
757 index_path = os.path.join(OUTPUT_DIR, INDEX_HTML_FILE)
758 if os.path.exists(index_path):
759 os.remove(index_path)
760
761
Yuke Liaodd1ec0592018-02-02 01:26:37762def _GetCoverageHtmlReportPathForFile(file_path):
763 """Given a file path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40764 assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
Yuke Liaodd1ec0592018-02-02 01:26:37765 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
766
767 # '+' is used instead of os.path.join because both of them are absolute paths
768 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14769 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37770 return _GetCoverageReportRootDirPath() + html_report_path
771
772
773def _GetCoverageHtmlReportPathForDirectory(dir_path):
774 """Given a directory path, returns the corresponding html report path."""
Abhishek Aryafb70b532018-05-06 17:47:40775 assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
Yuke Liaodd1ec0592018-02-02 01:26:37776 html_report_path = os.path.join(
777 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
778
779 # '+' is used instead of os.path.join because both of them are absolute paths
780 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14781 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37782 return _GetCoverageReportRootDirPath() + html_report_path
783
784
785def _GetCoverageHtmlReportPathForComponent(component_name):
786 """Given a component, returns the corresponding html report path."""
787 component_file_name = component_name.lower().replace('>', '-')
788 html_report_name = os.extsep.join([component_file_name, 'html'])
789 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
790 html_report_name)
791
792
793def _GetCoverageReportRootDirPath():
794 """The root directory that contains all generated coverage html reports."""
Max Moroz7c5354f2018-05-06 00:03:48795 return os.path.join(OUTPUT_DIR, _GetHostPlatform())
796
797
798def _GetComponentViewPath():
799 """Path to the HTML file for the component view."""
800 return os.path.join(_GetCoverageReportRootDirPath(),
801 COMPONENT_VIEW_INDEX_FILE)
802
803
804def _GetDirectoryViewPath():
805 """Path to the HTML file for the directory view."""
806 return os.path.join(_GetCoverageReportRootDirPath(),
807 DIRECTORY_VIEW_INDEX_FILE)
808
809
810def _GetFileViewPath():
811 """Path to the HTML file for the file view."""
812 return os.path.join(_GetCoverageReportRootDirPath(), FILE_VIEW_INDEX_FILE)
813
814
815def _GetLogsDirectoryPath():
816 """Path to the logs directory."""
817 return os.path.join(_GetCoverageReportRootDirPath(), LOGS_DIR_NAME)
818
819
820def _GetHtmlIndexPath():
821 """Path to the main HTML index file."""
822 return os.path.join(_GetCoverageReportRootDirPath(), INDEX_HTML_FILE)
823
824
825def _GetProfdataFilePath():
826 """Path to the resulting .profdata file."""
827 return os.path.join(_GetCoverageReportRootDirPath(), PROFDATA_FILE_NAME)
828
829
830def _GetSummaryFilePath():
831 """The JSON file that contains coverage summary written by llvm-cov export."""
832 return os.path.join(_GetCoverageReportRootDirPath(), SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33833
834
Yuke Liao506e8822017-12-04 16:52:54835def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
836 """Builds and runs target to generate the coverage profile data.
837
838 Args:
839 targets: A list of targets to build with coverage instrumentation.
840 commands: A list of commands used to run the targets.
841 jobs_count: Number of jobs to run in parallel for building. If None, a
842 default value is derived based on CPUs availability.
843
844 Returns:
845 A relative path to the generated profdata file.
846 """
847 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02848 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59849 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02850 coverage_profdata_file_path = (
851 _CreateCoverageProfileDataFromTargetProfDataFiles(
852 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54853
Abhishek Aryac19bc5ef2018-05-04 22:10:02854 for target_profdata_file_path in target_profdata_file_paths:
855 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52856
Abhishek Aryac19bc5ef2018-05-04 22:10:02857 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54858
859
860def _BuildTargets(targets, jobs_count):
861 """Builds target with Clang coverage instrumentation.
862
863 This function requires current working directory to be the root of checkout.
864
865 Args:
866 targets: A list of targets to build with coverage instrumentation.
867 jobs_count: Number of jobs to run in parallel for compilation. If None, a
868 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54869 """
Abhishek Arya1ec832c2017-12-05 18:06:59870
Yuke Liao506e8822017-12-04 16:52:54871 def _IsGomaConfigured():
872 """Returns True if goma is enabled in the gn build args.
873
874 Returns:
875 A boolean indicates whether goma is configured for building or not.
876 """
Yuke Liao80afff32018-03-07 01:26:20877 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54878 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
879
Abhishek Aryafb70b532018-05-06 17:47:40880 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54881 if jobs_count is None and _IsGomaConfigured():
882 jobs_count = DEFAULT_GOMA_JOBS
883
884 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
885 if jobs_count is not None:
886 subprocess_cmd.append('-j' + str(jobs_count))
887
888 subprocess_cmd.extend(targets)
889 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40890 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54891
892
Abhishek Aryac19bc5ef2018-05-04 22:10:02893def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54894 """Runs commands and returns the relative paths to the profraw data files.
895
896 Args:
897 targets: A list of targets built with coverage instrumentation.
898 commands: A list of commands used to run the targets.
899
900 Returns:
901 A list of relative paths to the generated profraw data files.
902 """
Abhishek Aryafb70b532018-05-06 17:47:40903 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10904
Yuke Liao506e8822017-12-04 16:52:54905 # Remove existing profraw data files.
Max Moroz7c5354f2018-05-06 00:03:48906 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Yuke Liao506e8822017-12-04 16:52:54907 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48908 os.remove(os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
909
910 # Ensure that logs directory exists.
911 if not os.path.exists(_GetLogsDirectoryPath()):
912 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54913
Abhishek Aryac19bc5ef2018-05-04 22:10:02914 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10915
Yuke Liaod4a9865202018-01-12 23:17:52916 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54917 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48918 output_file_name = os.extsep.join([target + '_output', 'log'])
919 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10920
Abhishek Aryac19bc5ef2018-05-04 22:10:02921 profdata_file_path = None
922 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40923 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02924 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10925
Abhishek Aryac19bc5ef2018-05-04 22:10:02926 if _IsIOSCommand(command):
927 # On iOS platform, due to lack of write permissions, profraw files are
928 # generated outside of the OUTPUT_DIR, and the exact paths are contained
929 # in the output of the command execution.
930 output = _ExecuteIOSCommand(target, command)
931 else:
932 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
933 output = _ExecuteCommand(target, command)
934
935 with open(output_file_path, 'w') as output_file:
936 output_file.write(output)
937
938 profraw_file_paths = []
939 if _IsIOS():
940 profraw_file_paths = _GetProfrawDataFileByParsingOutput(output)
941 else:
Max Moroz7c5354f2018-05-06 00:03:48942 for file_or_dir in os.listdir(_GetCoverageReportRootDirPath()):
Abhishek Aryac19bc5ef2018-05-04 22:10:02943 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48944 profraw_file_paths.append(
945 os.path.join(_GetCoverageReportRootDirPath(), file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02946
947 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40948 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryac19bc5ef2018-05-04 22:10:02949 'please make sure the binary exists and is properly '
950 'instrumented.' % target)
951
952 try:
953 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
954 target, profraw_file_paths)
955 break
956 except Exception:
957 print('Retrying...')
958 finally:
959 # Remove profraw files now so that they are not used in next iteration.
960 for profraw_file_path in profraw_file_paths:
961 os.remove(profraw_file_path)
962
963 assert profdata_file_path, (
Abhishek Aryafb70b532018-05-06 17:47:40964 'Failed to merge target "%s" profraw files after %d retries. '
Abhishek Aryac19bc5ef2018-05-04 22:10:02965 'Please file a bug with command you used, commit position and args.gn '
966 'config here: '
967 'https://bugs.chromium.org/p/chromium/issues/entry?'
Abhishek Aryafb70b532018-05-06 17:47:40968 'components=Tools%%3ECodeCoverage' % (target, MERGE_RETRIES))
Abhishek Aryac19bc5ef2018-05-04 22:10:02969 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54970
Abhishek Aryafb70b532018-05-06 17:47:40971 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10972
Abhishek Aryac19bc5ef2018-05-04 22:10:02973 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54974
975
976def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10977 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52978 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01979 #
Max Morozd73e45f2018-04-24 18:32:47980 # "%p" expands out to the process ID. It's not used by this scripts due to:
981 # 1) If a target program spawns too many processess, it may exhaust all disk
982 # space available. For example, unit_tests writes thousands of .profraw
983 # files each of size 1GB+.
984 # 2) If a target binary uses shared libraries, coverage profile data for them
985 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01986 #
Yuke Liaod4a9865202018-01-12 23:17:52987 # "%Nm" expands out to the instrumented binary's signature. When this pattern
988 # is specified, the runtime creates a pool of N raw profiles which are used
989 # for on-line profile merging. The runtime takes care of selecting a raw
990 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52991 # N must be between 1 and 9. The merge pool specifier can only occur once per
992 # filename pattern.
993 #
Max Morozd73e45f2018-04-24 18:32:47994 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01995 #
Max Morozd73e45f2018-04-24 18:32:47996 # For other cases, "%4m" is chosen as it creates some level of parallelism,
997 # but it's not too big to consume too much computing resource or disk space.
998 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59999 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:011000 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz7c5354f2018-05-06 00:03:481001 expected_profraw_file_path = os.path.join(_GetCoverageReportRootDirPath(),
Yuke Liao506e8822017-12-04 16:52:541002 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:541003
Yuke Liaoa0c8c2f2018-02-28 20:14:101004 try:
Max Moroz7c5354f2018-05-06 00:03:481005 # Some fuzz targets or tests may write into stderr, redirect it as well.
Yuke Liaoa0c8c2f2018-02-28 20:14:101006 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:291007 shlex.split(command),
Max Moroz7c5354f2018-05-06 00:03:481008 stderr=subprocess.STDOUT,
Yuke Liaob2926832018-03-02 17:34:291009 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101010 except subprocess.CalledProcessError as e:
1011 output = e.output
Abhishek Aryafb70b532018-05-06 17:47:401012 logging.warning('Command: "%s" exited with non-zero return code.', command)
Yuke Liaoa0c8c2f2018-02-28 20:14:101013
1014 return output
1015
1016
Yuke Liao27349c92018-03-22 21:10:011017def _IsFuzzerTarget(target):
1018 """Returns true if the target is a fuzzer target."""
1019 build_args = _GetBuildArgs()
1020 use_libfuzzer = ('use_libfuzzer' in build_args and
1021 build_args['use_libfuzzer'] == 'true')
1022 return use_libfuzzer and target.endswith('_fuzzer')
1023
1024
Yuke Liaob2926832018-03-02 17:34:291025def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101026 """Runs a single iOS command and generates a profraw data file.
1027
1028 iOS application doesn't have write access to folders outside of the app, so
1029 it's impossible to instruct the app to flush the profraw data file to the
1030 desired location. The profraw data file will be generated somewhere within the
1031 application's Documents folder, and the full path can be obtained by parsing
1032 the output.
1033 """
Yuke Liaob2926832018-03-02 17:34:291034 assert _IsIOSCommand(command)
1035
1036 # After running tests, iossim generates a profraw data file, it won't be
1037 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
1038 # checkout.
1039 iossim_profraw_file_path = os.path.join(
1040 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:101041
1042 try:
Yuke Liaob2926832018-03-02 17:34:291043 output = subprocess.check_output(
1044 shlex.split(command),
1045 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:101046 except subprocess.CalledProcessError as e:
1047 # iossim emits non-zero return code even if tests run successfully, so
1048 # ignore the return code.
1049 output = e.output
1050
1051 return output
1052
1053
1054def _GetProfrawDataFileByParsingOutput(output):
1055 """Returns the path to the profraw data file obtained by parsing the output.
1056
1057 The output of running the test target has no format, but it is guaranteed to
1058 have a single line containing the path to the generated profraw data file.
1059 NOTE: This should only be called when target os is iOS.
1060 """
Yuke Liaob2926832018-03-02 17:34:291061 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:101062
Yuke Liaob2926832018-03-02 17:34:291063 output_by_lines = ''.join(output).splitlines()
1064 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:101065
1066 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:291067 result = profraw_file_pattern.match(line)
1068 if result:
1069 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:101070
1071 assert False, ('No profraw data file was generated, did you call '
1072 'coverage_util::ConfigureCoverageReportPath() in test setup? '
1073 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:541074
1075
Abhishek Aryac19bc5ef2018-05-04 22:10:021076def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
1077 """Returns a relative path to coverage profdata file by merging target
1078 profdata files.
Yuke Liao506e8822017-12-04 16:52:541079
1080 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:021081 profdata_file_paths: A list of relative paths to the profdata data files
1082 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:541083
1084 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:021085 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:541086
1087 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:021088 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:541089 """
Abhishek Aryafb70b532018-05-06 17:47:401090 logging.info('Creating the coverage profile data file.')
1091 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:481092 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:541093 try:
Abhishek Arya1ec832c2017-12-05 18:06:591094 subprocess_cmd = [
1095 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1096 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:021097 subprocess_cmd.extend(profdata_file_paths)
1098 subprocess.check_call(subprocess_cmd)
1099 except subprocess.CalledProcessError as error:
1100 print('Failed to merge target profdata files to create coverage profdata. '
1101 'Try again.')
1102 raise error
1103
Abhishek Aryafb70b532018-05-06 17:47:401104 logging.debug('Finished merging target profdata files.')
1105 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:021106 profdata_file_path)
1107 return profdata_file_path
1108
1109
1110def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
1111 """Returns a relative path to target profdata file by merging target
1112 profraw files.
1113
1114 Args:
1115 profraw_file_paths: A list of relative paths to the profdata data files
1116 that are to be merged.
1117
1118 Returns:
1119 A relative path to the merged coverage profdata file.
1120
1121 Raises:
1122 CalledProcessError: An error occurred merging profdata files.
1123 """
Abhishek Aryafb70b532018-05-06 17:47:401124 logging.info('Creating target profile data file.')
1125 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:021126 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
1127
1128 try:
1129 subprocess_cmd = [
1130 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
1131 ]
Yuke Liao506e8822017-12-04 16:52:541132 subprocess_cmd.extend(profraw_file_paths)
1133 subprocess.check_call(subprocess_cmd)
1134 except subprocess.CalledProcessError as error:
Abhishek Aryac19bc5ef2018-05-04 22:10:021135 print('Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:541136 raise error
1137
Abhishek Aryafb70b532018-05-06 17:47:401138 logging.debug('Finished merging target profraw files.')
1139 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:101140 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541141 return profdata_file_path
1142
1143
Yuke Liao0e4c8682018-04-18 21:06:591144def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1145 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331146 """Generates per file coverage summary using "llvm-cov export" command."""
1147 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1148 # [[-object BIN]] [SOURCES].
1149 # NOTE: For object files, the first one is specified as a positional argument,
1150 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101151 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:401152 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:331153 subprocess_cmd = [
1154 LLVM_COV_PATH, 'export', '-summary-only',
1155 '-instr-profile=' + profdata_file_path, binary_paths[0]
1156 ]
1157 subprocess_cmd.extend(
1158 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291159 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331160 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591161 if ignore_filename_regex:
1162 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331163
Max Moroz7c5354f2018-05-06 00:03:481164 export_output = subprocess.check_output(subprocess_cmd)
1165
1166 # Write output on the disk to be used by code coverage bot.
1167 with open(_GetSummaryFilePath(), 'w') as f:
1168 f.write(export_output)
1169
1170 json_output = json.loads(export_output)
Yuke Liaoea228d02018-01-05 19:10:331171 assert len(json_output['data']) == 1
1172 files_coverage_data = json_output['data'][0]['files']
1173
1174 per_file_coverage_summary = {}
1175 for file_coverage_data in files_coverage_data:
1176 file_path = file_coverage_data['filename']
Abhishek Aryafb70b532018-05-06 17:47:401177 assert file_path.startswith(SRC_ROOT_PATH + os.sep), (
1178 'File path "%s" in coverage summary is outside source checkout.' %
1179 file_path)
Yuke Liaoea228d02018-01-05 19:10:331180
Abhishek Aryafb70b532018-05-06 17:47:401181 summary = file_coverage_data['summary']
Yuke Liaoea228d02018-01-05 19:10:331182 if summary['lines']['count'] == 0:
1183 continue
1184
1185 per_file_coverage_summary[file_path] = _CoverageSummary(
1186 regions_total=summary['regions']['count'],
1187 regions_covered=summary['regions']['covered'],
1188 functions_total=summary['functions']['count'],
1189 functions_covered=summary['functions']['covered'],
1190 lines_total=summary['lines']['count'],
1191 lines_covered=summary['lines']['covered'])
1192
Abhishek Aryafb70b532018-05-06 17:47:401193 logging.debug('Finished generating per-file code coverage summary.')
Yuke Liaoea228d02018-01-05 19:10:331194 return per_file_coverage_summary
1195
1196
Yuke Liaob2926832018-03-02 17:34:291197def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1198 """Appends -arch arguments to the command list if it's ios platform.
1199
1200 iOS binaries are universal binaries, and require specifying the architecture
1201 to use, and one architecture needs to be specified for each binary.
1202 """
1203 if _IsIOS():
1204 cmd_list.extend(['-arch=x86_64'] * num_archs)
1205
1206
Yuke Liao506e8822017-12-04 16:52:541207def _GetBinaryPath(command):
1208 """Returns a relative path to the binary to be run by the command.
1209
Yuke Liao545db322018-02-15 17:12:011210 Currently, following types of commands are supported (e.g. url_unittests):
1211 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1212 2. Use xvfb.
1213 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1214 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371215 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1216 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101217 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371218 <iossim_arguments> -c <app_arguments>
1219 out/Coverage-iphonesimulator/url_unittests.app"
1220
Yuke Liao545db322018-02-15 17:12:011221
Yuke Liao506e8822017-12-04 16:52:541222 Args:
1223 command: A command used to run a target.
1224
1225 Returns:
1226 A relative path to the binary.
1227 """
Yuke Liao545db322018-02-15 17:12:011228 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1229
Yuke Liaob2926832018-03-02 17:34:291230 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011231 if os.path.basename(command_parts[0]) == 'python':
1232 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:401233 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:011234 return command_parts[2]
1235
1236 if os.path.basename(command_parts[0]) == xvfb_script_name:
1237 return command_parts[1]
1238
Yuke Liaob2926832018-03-02 17:34:291239 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101240 # For a given application bundle, the binary resides in the bundle and has
1241 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371242 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101243 app_name = os.path.splitext(os.path.basename(app_path))[0]
1244 return os.path.join(app_path, app_name)
1245
Yuke Liaob2926832018-03-02 17:34:291246 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541247
1248
Yuke Liaob2926832018-03-02 17:34:291249def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101250 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291251 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101252
1253
Yuke Liao95d13d72017-12-07 18:18:501254def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1255 """Verifies that the target executables specified in the commands are inside
1256 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541257 for command in commands:
1258 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501259 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
Max Moroz7c5354f2018-05-06 00:03:481260 assert binary_absolute_path.startswith(BUILD_DIR), (
Yuke Liao95d13d72017-12-07 18:18:501261 'Target executable "%s" in command: "%s" is outside of '
1262 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541263
1264
1265def _ValidateBuildingWithClangCoverage():
1266 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201267 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541268
1269 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1270 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591271 assert False, ('\'{} = true\' is required in args.gn.'
1272 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541273
1274
Yuke Liaoc60b2d02018-03-02 21:40:431275def _ValidateCurrentPlatformIsSupported():
1276 """Asserts that this script suports running on the current platform"""
1277 target_os = _GetTargetOS()
1278 if target_os:
1279 current_platform = target_os
1280 else:
1281 current_platform = _GetHostPlatform()
1282
1283 assert current_platform in [
1284 'linux', 'mac', 'chromeos', 'ios'
1285 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1286
1287
Yuke Liao80afff32018-03-07 01:26:201288def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541289 """Parses args.gn file and returns results as a dictionary.
1290
1291 Returns:
1292 A dictionary representing the build args.
1293 """
Yuke Liao80afff32018-03-07 01:26:201294 global _BUILD_ARGS
1295 if _BUILD_ARGS is not None:
1296 return _BUILD_ARGS
1297
1298 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541299 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1300 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1301 'missing args.gn file.' % BUILD_DIR)
1302 with open(build_args_path) as build_args_file:
1303 build_args_lines = build_args_file.readlines()
1304
Yuke Liao506e8822017-12-04 16:52:541305 for build_arg_line in build_args_lines:
1306 build_arg_without_comments = build_arg_line.split('#')[0]
1307 key_value_pair = build_arg_without_comments.split('=')
1308 if len(key_value_pair) != 2:
1309 continue
1310
1311 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431312
1313 # Values are wrapped within a pair of double-quotes, so remove the leading
1314 # and trailing double-quotes.
1315 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201316 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541317
Yuke Liao80afff32018-03-07 01:26:201318 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541319
1320
Abhishek Arya16f059a2017-12-07 17:47:321321def _VerifyPathsAndReturnAbsolutes(paths):
1322 """Verifies that the paths specified in |paths| exist and returns absolute
1323 versions.
Yuke Liao66da1732017-12-05 22:19:421324
1325 Args:
1326 paths: A list of files or directories.
1327 """
Abhishek Arya16f059a2017-12-07 17:47:321328 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421329 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321330 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1331 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1332
1333 absolute_paths.append(absolute_path)
1334
1335 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421336
1337
Yuke Liaodd1ec0592018-02-02 01:26:371338def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1339 """Returns a target path relative to the directory of base_path.
1340
1341 This method requires base_path to be a file, otherwise, one should call
1342 os.path.relpath directly.
1343 """
1344 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141345 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371346 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141347 base_dir = os.path.dirname(base_path)
1348 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371349
1350
Abhishek Arya64636af2018-05-04 14:42:131351def _GetBinaryPathsFromTargets(targets, build_dir):
1352 """Return binary paths from target names."""
1353 # FIXME: Derive output binary from target build definitions rather than
1354 # assuming that it is always the same name.
1355 binary_paths = []
1356 for target in targets:
1357 binary_path = os.path.join(build_dir, target)
1358 if _GetHostPlatform() == 'win':
1359 binary_path += '.exe'
1360
1361 if os.path.exists(binary_path):
1362 binary_paths.append(binary_path)
1363 else:
1364 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:401365 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:131366 os.path.basename(binary_path))
1367
1368 return binary_paths
1369
1370
Yuke Liao506e8822017-12-04 16:52:541371def _ParseCommandArguments():
1372 """Adds and parses relevant arguments for tool comands.
1373
1374 Returns:
1375 A dictionary representing the arguments.
1376 """
1377 arg_parser = argparse.ArgumentParser()
1378 arg_parser.usage = __doc__
1379
Abhishek Arya1ec832c2017-12-05 18:06:591380 arg_parser.add_argument(
1381 '-b',
1382 '--build-dir',
1383 type=str,
1384 required=True,
1385 help='The build directory, the path needs to be relative to the root of '
1386 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541387
Abhishek Arya1ec832c2017-12-05 18:06:591388 arg_parser.add_argument(
1389 '-o',
1390 '--output-dir',
1391 type=str,
1392 required=True,
1393 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541394
Abhishek Arya1ec832c2017-12-05 18:06:591395 arg_parser.add_argument(
1396 '-c',
1397 '--command',
1398 action='append',
Abhishek Arya64636af2018-05-04 14:42:131399 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:591400 help='Commands used to run test targets, one test target needs one and '
1401 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:131402 'current working directory is the root of the checkout. This option is '
1403 'incompatible with -p/--profdata-file option.')
1404
1405 arg_parser.add_argument(
1406 '-p',
1407 '--profdata-file',
1408 type=str,
1409 required=False,
1410 help='Path to profdata file to use for generating code coverage reports. '
1411 'This can be useful if you generated the profdata file seperately in '
1412 'your own test harness. This option is ignored if run command(s) are '
1413 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:541414
Abhishek Arya1ec832c2017-12-05 18:06:591415 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421416 '-f',
1417 '--filters',
1418 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321419 required=False,
Yuke Liao66da1732017-12-05 22:19:421420 help='Directories or files to get code coverage for, and all files under '
1421 'the directories are included recursively.')
1422
1423 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591424 '-i',
1425 '--ignore-filename-regex',
1426 type=str,
1427 help='Skip source code files with file paths that match the given '
1428 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1429 'to exclude files in third_party/ and out/ folders from the report.')
1430
1431 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591432 '-j',
1433 '--jobs',
1434 type=int,
1435 default=None,
1436 help='Run N jobs to build in parallel. If not specified, a default value '
1437 'will be derived based on CPUs availability. Please refer to '
1438 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541439
Abhishek Arya1ec832c2017-12-05 18:06:591440 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101441 '-v',
1442 '--verbose',
1443 action='store_true',
1444 help='Prints additional output for diagnostics.')
1445
1446 arg_parser.add_argument(
1447 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1448
1449 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:021450 'targets',
1451 nargs='+',
1452 help='The names of the test targets to run. If multiple run commands are '
1453 'specified using the -c/--command option, then the order of targets and '
1454 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541455
1456 args = arg_parser.parse_args()
1457 return args
1458
1459
1460def Main():
1461 """Execute tool commands."""
Abhishek Arya64636af2018-05-04 14:42:131462 # Change directory to source root to aid in relative paths calculations.
1463 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111464
Abhishek Arya64636af2018-05-04 14:42:131465 # Setup coverage binaries even when script is called with empty params. This
1466 # is used by coverage bot for initial setup.
Abhishek Arya8a0751a2018-05-03 18:53:111467 DownloadCoverageToolsIfNeeded()
1468
Yuke Liao506e8822017-12-04 16:52:541469 args = _ParseCommandArguments()
Abhishek Arya64636af2018-05-04 14:42:131470 _ConfigureLogging(args)
1471
Yuke Liao506e8822017-12-04 16:52:541472 global BUILD_DIR
Max Moroz7c5354f2018-05-06 00:03:481473 BUILD_DIR = os.path.abspath(args.build_dir)
Yuke Liao506e8822017-12-04 16:52:541474 global OUTPUT_DIR
Max Moroz7c5354f2018-05-06 00:03:481475 OUTPUT_DIR = os.path.abspath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541476
Abhishek Arya64636af2018-05-04 14:42:131477 assert args.command or args.profdata_file, (
1478 'Need to either provide commands to run using -c/--command option OR '
1479 'provide prof-data file as input using -p/--profdata-file option.')
Yuke Liaoc60b2d02018-03-02 21:40:431480
Abhishek Arya64636af2018-05-04 14:42:131481 assert not args.command or (len(args.targets) == len(args.command)), (
1482 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431483
Abhishek Arya1ec832c2017-12-05 18:06:591484 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401485 'Build directory: "%s" doesn\'t exist. '
1486 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131487
Yuke Liaoc60b2d02018-03-02 21:40:431488 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541489 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321490
1491 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421492 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321493 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421494
Max Moroz7c5354f2018-05-06 00:03:481495 if not os.path.exists(_GetCoverageReportRootDirPath()):
1496 os.makedirs(_GetCoverageReportRootDirPath())
Yuke Liao506e8822017-12-04 16:52:541497
Abhishek Arya64636af2018-05-04 14:42:131498 # Get profdate file and list of binary paths.
1499 if args.command:
1500 # A list of commands are provided. Run them to generate profdata file, and
1501 # create a list of binary paths from parsing commands.
1502 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1503 profdata_file_path = _CreateCoverageProfileDataForTargets(
1504 args.targets, args.command, args.jobs)
1505 binary_paths = [_GetBinaryPath(command) for command in args.command]
1506 else:
1507 # An input prof-data file is already provided. Just calculate binary paths.
1508 profdata_file_path = args.profdata_file
1509 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331510
Abhishek Arya78120bc2018-05-07 20:53:541511 binary_paths.extend(_GetSharedLibraries(binary_paths))
1512
Yuke Liao481d3482018-01-29 19:17:101513 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401514 'depending on size of target!).')
Yuke Liaodd1ec0592018-02-02 01:26:371515 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591516 binary_paths, profdata_file_path, absolute_filter_paths,
1517 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371518 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591519 absolute_filter_paths,
1520 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371521 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1522
1523 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1524 per_file_coverage_summary)
1525 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1526 per_file_coverage_summary)
1527 _GenerateDirectoryViewHtmlIndexFile()
1528
1529 component_to_directories = _ExtractComponentToDirectoriesMapping()
1530 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1531 component_to_directories, per_directory_coverage_summary)
1532 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1533 component_to_directories,
1534 per_directory_coverage_summary)
1535 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331536
1537 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371538 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331539 _OverwriteHtmlReportsIndexFile()
Max Moroz7c5354f2018-05-06 00:03:481540 _CleanUpOutputDir()
Yuke Liaoea228d02018-01-05 19:10:331541
Max Moroz7c5354f2018-05-06 00:03:481542 html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
Abhishek Aryafb70b532018-05-06 17:47:401543 logging.info('Index file for html report is generated as: "%s".',
Yuke Liao481d3482018-01-29 19:17:101544 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541545
Abhishek Arya1ec832c2017-12-05 18:06:591546
Yuke Liao506e8822017-12-04 16:52:541547if __name__ == '__main__':
1548 sys.exit(Main())