blob: a6a1c65da672489db8c76135940e0d1ee2f1884d [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
Yuke Liao506e8822017-12-04 16:52:5471import subprocess
Yuke Liao506e8822017-12-04 16:52:5472import urllib2
73
Abhishek Arya1ec832c2017-12-05 18:06:5974sys.path.append(
75 os.path.join(
76 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
77 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5478import update as clang_update
79
Yuke Liaoea228d02018-01-05 19:10:3380sys.path.append(
81 os.path.join(
82 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
83 'third_party'))
84import jinja2
85from collections import defaultdict
86
Yuke Liao506e8822017-12-04 16:52:5487# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5988SRC_ROOT_PATH = os.path.abspath(
89 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5490
91# Absolute path to the code coverage tools binary.
92LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
93LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
94LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
95
96# Build directory, the value is parsed from command line arguments.
97BUILD_DIR = None
98
99# Output directory for generated artifacts, the value is parsed from command
100# line arguemnts.
101OUTPUT_DIR = None
102
103# Default number of jobs used to build when goma is configured and enabled.
104DEFAULT_GOMA_JOBS = 100
105
106# Name of the file extension for profraw data files.
107PROFRAW_FILE_EXTENSION = 'profraw'
108
109# Name of the final profdata file, and this file needs to be passed to
110# "llvm-cov" command in order to call "llvm-cov show" to inspect the
111# line-by-line coverage of specific files.
112PROFDATA_FILE_NAME = 'coverage.profdata'
113
114# Build arg required for generating code coverage data.
115CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
116
Yuke Liaoea228d02018-01-05 19:10:33117# The default name of the html coverage report for a directory.
118DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
119
Yuke Liaodd1ec0592018-02-02 01:26:37120# Name of the html index files for different views.
121DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
122COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
123FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
124
125# Used to extract a mapping between directories and components.
126COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
127
Yuke Liao80afff32018-03-07 01:26:20128# Caches the results returned by _GetBuildArgs, don't use this variable
129# directly, call _GetBuildArgs instead.
130_BUILD_ARGS = None
131
Yuke Liaoea228d02018-01-05 19:10:33132
133class _CoverageSummary(object):
134 """Encapsulates coverage summary representation."""
135
Yuke Liaodd1ec0592018-02-02 01:26:37136 def __init__(self,
137 regions_total=0,
138 regions_covered=0,
139 functions_total=0,
140 functions_covered=0,
141 lines_total=0,
142 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33143 """Initializes _CoverageSummary object."""
144 self._summary = {
145 'regions': {
146 'total': regions_total,
147 'covered': regions_covered
148 },
149 'functions': {
150 'total': functions_total,
151 'covered': functions_covered
152 },
153 'lines': {
154 'total': lines_total,
155 'covered': lines_covered
156 }
157 }
158
159 def Get(self):
160 """Returns summary as a dictionary."""
161 return self._summary
162
163 def AddSummary(self, other_summary):
164 """Adds another summary to this one element-wise."""
165 for feature in self._summary:
166 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
167 self._summary[feature]['covered'] += other_summary.Get()[feature][
168 'covered']
169
170
Yuke Liaodd1ec0592018-02-02 01:26:37171class _CoverageReportHtmlGenerator(object):
172 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33173
Yuke Liaodd1ec0592018-02-02 01:26:37174 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33175 """
176
Yuke Liaodd1ec0592018-02-02 01:26:37177 def __init__(self, output_path, table_entry_type):
178 """Initializes _CoverageReportHtmlGenerator object.
179
180 Args:
181 output_path: Path to the html report that will be generated.
182 table_entry_type: Type of the table entries to be displayed in the table
183 header. For example: 'Path', 'Component'.
184 """
Yuke Liaoea228d02018-01-05 19:10:33185 css_file_name = os.extsep.join(['style', 'css'])
186 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
187 assert os.path.exists(css_absolute_path), (
188 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
189 'is called first, and the css file is generated at: "%s"' %
190 css_absolute_path)
191
192 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37193 self._output_path = output_path
194 self._table_entry_type = table_entry_type
195
Yuke Liaoea228d02018-01-05 19:10:33196 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12197 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33198 template_dir = os.path.join(
199 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
200
201 jinja_env = jinja2.Environment(
202 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
203 self._header_template = jinja_env.get_template('header.html')
204 self._table_template = jinja_env.get_template('table.html')
205 self._footer_template = jinja_env.get_template('footer.html')
206
207 def AddLinkToAnotherReport(self, html_report_path, name, summary):
208 """Adds a link to another html report in this report.
209
210 The link to be added is assumed to be an entry in this directory.
211 """
Yuke Liaodd1ec0592018-02-02 01:26:37212 # Use relative paths instead of absolute paths to make the generated reports
213 # portable.
214 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
215 html_report_path, self._output_path)
216
Yuke Liaod54030e2018-01-08 17:34:12217 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37218 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12219 os.path.basename(html_report_path) ==
220 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
221 self._table_entries.append(table_entry)
222
223 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35224 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12225 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
226
227 def _CreateTableEntryFromCoverageSummary(self,
228 summary,
229 href=None,
230 name=None,
231 is_dir=None):
232 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37233 assert (href is None and name is None and is_dir is None) or (
234 href is not None and name is not None and is_dir is not None), (
235 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35236 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37237 'attributes must be None.')
238
Yuke Liaod54030e2018-01-08 17:34:12239 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37240 if href is not None:
241 entry['href'] = href
242 if name is not None:
243 entry['name'] = name
244 if is_dir is not None:
245 entry['is_dir'] = is_dir
246
Yuke Liaoea228d02018-01-05 19:10:33247 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12248 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37249 if summary_dict[feature]['total'] == 0:
250 percentage = 0.0
251 else:
Yuke Liao0e4c8682018-04-18 21:06:59252 percentage = float(summary_dict[feature]
253 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35254
Yuke Liaoea228d02018-01-05 19:10:33255 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12256 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33257 'total': summary_dict[feature]['total'],
258 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35259 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33260 'color_class': color_class
261 }
Yuke Liaod54030e2018-01-08 17:34:12262
Yuke Liaod54030e2018-01-08 17:34:12263 return entry
Yuke Liaoea228d02018-01-05 19:10:33264
265 def _GetColorClass(self, percentage):
266 """Returns the css color class based on coverage percentage."""
267 if percentage >= 0 and percentage < 80:
268 return 'red'
269 if percentage >= 80 and percentage < 100:
270 return 'yellow'
271 if percentage == 100:
272 return 'green'
273
274 assert False, 'Invalid coverage percentage: "%d"' % percentage
275
Yuke Liaodd1ec0592018-02-02 01:26:37276 def WriteHtmlCoverageReport(self):
277 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33278
279 In the report, sub-directories are displayed before files and within each
280 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33281 """
282
283 def EntryCmp(left, right):
284 """Compare function for table entries."""
285 if left['is_dir'] != right['is_dir']:
286 return -1 if left['is_dir'] == True else 1
287
Yuke Liaodd1ec0592018-02-02 01:26:37288 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33289
290 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
291
292 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37293 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
294 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
295 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
296
Yuke Liaoea228d02018-01-05 19:10:33297 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37298 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
299 directory_view_href=_GetRelativePathToDirectoryOfFile(
300 directory_view_path, self._output_path),
301 component_view_href=_GetRelativePathToDirectoryOfFile(
302 component_view_path, self._output_path),
303 file_view_href=_GetRelativePathToDirectoryOfFile(
304 file_view_path, self._output_path))
305
Yuke Liaod54030e2018-01-08 17:34:12306 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37307 entries=self._table_entries,
308 total_entry=self._total_entry,
309 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33310 html_footer = self._footer_template.render()
311
Yuke Liaodd1ec0592018-02-02 01:26:37312 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33313 html_file.write(html_header + html_table + html_footer)
314
Yuke Liao506e8822017-12-04 16:52:54315
Yuke Liaoc60b2d02018-03-02 21:40:43316def _GetHostPlatform():
317 """Returns the host platform.
318
319 This is separate from the target platform/os that coverage is running for.
320 """
Abhishek Arya1ec832c2017-12-05 18:06:59321 if sys.platform == 'win32' or sys.platform == 'cygwin':
322 return 'win'
323 if sys.platform.startswith('linux'):
324 return 'linux'
325 else:
326 assert sys.platform == 'darwin'
327 return 'mac'
328
329
Yuke Liaoc60b2d02018-03-02 21:40:43330def _GetTargetOS():
331 """Returns the target os specified in args.gn file.
332
333 Returns an empty string is target_os is not specified.
334 """
Yuke Liao80afff32018-03-07 01:26:20335 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43336 return build_args['target_os'] if 'target_os' in build_args else ''
337
338
Yuke Liaob2926832018-03-02 17:34:29339def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10340 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43341 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10342
343
Yuke Liao506e8822017-12-04 16:52:54344# TODO(crbug.com/759794): remove this function once tools get included to
345# Clang bundle:
346# https://chromium-review.googlesource.com/c/chromium/src/+/688221
347def DownloadCoverageToolsIfNeeded():
348 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59349
Yuke Liaoc60b2d02018-03-02 21:40:43350 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54351 """Returns a pair of revision number by reading the build stamp file.
352
353 Args:
354 stamp_file_path: A path the build stamp file created by
355 tools/clang/scripts/update.py.
356 Returns:
357 A pair of integers represeting the main and sub revision respectively.
358 """
359 if not os.path.exists(stamp_file_path):
360 return 0, 0
361
362 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43363 stamp_file_line = stamp_file.readline()
364 if ',' in stamp_file_line:
365 package_version = stamp_file_line.rstrip().split(',')[0]
366 else:
367 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54368
Yuke Liaoc60b2d02018-03-02 21:40:43369 clang_revision_str, clang_sub_revision_str = package_version.split('-')
370 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59371
Yuke Liaoc60b2d02018-03-02 21:40:43372 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54373 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43374 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54375
376 coverage_revision_stamp_file = os.path.join(
377 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
378 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43379 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54380
Yuke Liaoea228d02018-01-05 19:10:33381 has_coverage_tools = (
382 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32383
Yuke Liaoea228d02018-01-05 19:10:33384 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54385 coverage_sub_revision == clang_sub_revision):
386 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43387 return
Yuke Liao506e8822017-12-04 16:52:54388
389 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
390 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
391
392 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43393 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54394 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43395 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54396 coverage_tools_url = (
397 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43398 else:
399 assert host_platform == 'win'
400 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54401
402 try:
403 clang_update.DownloadAndUnpack(coverage_tools_url,
404 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10405 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54406 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43407 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54408 file_handle.write('\n')
409 except urllib2.URLError:
410 raise Exception(
411 'Failed to download coverage tools: %s.' % coverage_tools_url)
412
413
Yuke Liaodd1ec0592018-02-02 01:26:37414def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59415 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54416 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
417
418 For a file with absolute path /a/b/x.cc, a html report is generated as:
419 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
420 OUTPUT_DIR/index.html.
421
422 Args:
423 binary_paths: A list of paths to the instrumented binaries.
424 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42425 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54426 """
Yuke Liao506e8822017-12-04 16:52:54427 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
428 # [[-object BIN]] [SOURCES]
429 # NOTE: For object files, the first one is specified as a positional argument,
430 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10431 logging.debug('Generating per file line by line coverage reports using '
432 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59433 subprocess_cmd = [
434 LLVM_COV_PATH, 'show', '-format=html',
435 '-output-dir={}'.format(OUTPUT_DIR),
436 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
437 ]
438 subprocess_cmd.extend(
439 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29440 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42441 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59442 if ignore_filename_regex:
443 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
444
Yuke Liao506e8822017-12-04 16:52:54445 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10446 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54447
448
Yuke Liaodd1ec0592018-02-02 01:26:37449def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
450 """Generates html index file for file view."""
451 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
452 logging.debug('Generating file view html index file as: "%s".',
453 file_view_index_file_path)
454 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
455 'Path')
456 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33457
Yuke Liaodd1ec0592018-02-02 01:26:37458 for file_path in per_file_coverage_summary:
459 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
460
461 html_generator.AddLinkToAnotherReport(
462 _GetCoverageHtmlReportPathForFile(file_path),
463 os.path.relpath(file_path, SRC_ROOT_PATH),
464 per_file_coverage_summary[file_path])
465
466 html_generator.CreateTotalsEntry(totals_coverage_summary)
467 html_generator.WriteHtmlCoverageReport()
468 logging.debug('Finished generating file view html index file.')
469
470
471def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
472 """Calculates per directory coverage summary."""
473 logging.debug('Calculating per-directory coverage summary')
474 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
475
Yuke Liaoea228d02018-01-05 19:10:33476 for file_path in per_file_coverage_summary:
477 summary = per_file_coverage_summary[file_path]
478 parent_dir = os.path.dirname(file_path)
479 while True:
480 per_directory_coverage_summary[parent_dir].AddSummary(summary)
481
482 if parent_dir == SRC_ROOT_PATH:
483 break
484 parent_dir = os.path.dirname(parent_dir)
485
Yuke Liaodd1ec0592018-02-02 01:26:37486 logging.debug('Finished calculating per-directory coverage summary')
487 return per_directory_coverage_summary
488
489
490def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
491 per_file_coverage_summary):
492 """Generates per directory coverage breakdown in html."""
493 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33494 for dir_path in per_directory_coverage_summary:
495 _GenerateCoverageInHtmlForDirectory(
496 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
497
Yuke Liaodd1ec0592018-02-02 01:26:37498 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10499
Yuke Liaoea228d02018-01-05 19:10:33500
501def _GenerateCoverageInHtmlForDirectory(
502 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
503 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37504 html_generator = _CoverageReportHtmlGenerator(
505 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33506
507 for entry_name in os.listdir(dir_path):
508 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33509
Yuke Liaodd1ec0592018-02-02 01:26:37510 if entry_path in per_file_coverage_summary:
511 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
512 entry_coverage_summary = per_file_coverage_summary[entry_path]
513 elif entry_path in per_directory_coverage_summary:
514 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
515 entry_path)
516 entry_coverage_summary = per_directory_coverage_summary[entry_path]
517 else:
Yuke Liaoc7e607142018-02-05 20:26:14518 # Any file without executable lines shouldn't be included into the report.
519 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37520 continue
Yuke Liaoea228d02018-01-05 19:10:33521
Yuke Liaodd1ec0592018-02-02 01:26:37522 html_generator.AddLinkToAnotherReport(entry_html_report_path,
523 os.path.basename(entry_path),
524 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33525
Yuke Liaod54030e2018-01-08 17:34:12526 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37527 html_generator.WriteHtmlCoverageReport()
528
529
530def _GenerateDirectoryViewHtmlIndexFile():
531 """Generates the html index file for directory view.
532
533 Note that the index file is already generated under SRC_ROOT_PATH, so this
534 file simply redirects to it, and the reason of this extra layer is for
535 structural consistency with other views.
536 """
537 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
538 DIRECTORY_VIEW_INDEX_FILE)
539 logging.debug('Generating directory view html index file as: "%s".',
540 directory_view_index_file_path)
541 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
542 SRC_ROOT_PATH)
543 _WriteRedirectHtmlFile(directory_view_index_file_path,
544 src_root_html_report_path)
545 logging.debug('Finished generating directory view html index file.')
546
547
548def _CalculatePerComponentCoverageSummary(component_to_directories,
549 per_directory_coverage_summary):
550 """Calculates per component coverage summary."""
551 logging.debug('Calculating per-component coverage summary')
552 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
553
554 for component in component_to_directories:
555 for directory in component_to_directories[component]:
556 absolute_directory_path = os.path.abspath(directory)
557 if absolute_directory_path in per_directory_coverage_summary:
558 per_component_coverage_summary[component].AddSummary(
559 per_directory_coverage_summary[absolute_directory_path])
560
561 logging.debug('Finished calculating per-component coverage summary')
562 return per_component_coverage_summary
563
564
565def _ExtractComponentToDirectoriesMapping():
566 """Returns a mapping from components to directories."""
567 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
568 directory_to_component = component_mappings['dir-to-component']
569
570 component_to_directories = defaultdict(list)
571 for directory in directory_to_component:
572 component = directory_to_component[directory]
573 component_to_directories[component].append(directory)
574
575 return component_to_directories
576
577
578def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
579 component_to_directories,
580 per_directory_coverage_summary):
581 """Generates per-component coverage reports in html."""
582 logging.debug('Writing per-component coverage html reports.')
583 for component in per_component_coverage_summary:
584 _GenerateCoverageInHtmlForComponent(
585 component, per_component_coverage_summary, component_to_directories,
586 per_directory_coverage_summary)
587
588 logging.debug('Finished writing per-component coverage html reports.')
589
590
591def _GenerateCoverageInHtmlForComponent(
592 component_name, per_component_coverage_summary, component_to_directories,
593 per_directory_coverage_summary):
594 """Generates coverage html report for a component."""
595 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
596 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14597 component_html_report_dir = os.path.dirname(component_html_report_path)
598 if not os.path.exists(component_html_report_dir):
599 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37600
601 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
602 'Path')
603
604 for dir_path in component_to_directories[component_name]:
605 dir_absolute_path = os.path.abspath(dir_path)
606 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14607 # Any directory without an excercised file shouldn't be included into the
608 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37609 continue
610
611 html_generator.AddLinkToAnotherReport(
612 _GetCoverageHtmlReportPathForDirectory(dir_path),
613 os.path.relpath(dir_path, SRC_ROOT_PATH),
614 per_directory_coverage_summary[dir_absolute_path])
615
616 html_generator.CreateTotalsEntry(
617 per_component_coverage_summary[component_name])
618 html_generator.WriteHtmlCoverageReport()
619
620
621def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
622 """Generates the html index file for component view."""
623 component_view_index_file_path = os.path.join(OUTPUT_DIR,
624 COMPONENT_VIEW_INDEX_FILE)
625 logging.debug('Generating component view html index file as: "%s".',
626 component_view_index_file_path)
627 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
628 'Component')
629 totals_coverage_summary = _CoverageSummary()
630
631 for component in per_component_coverage_summary:
632 totals_coverage_summary.AddSummary(
633 per_component_coverage_summary[component])
634
635 html_generator.AddLinkToAnotherReport(
636 _GetCoverageHtmlReportPathForComponent(component), component,
637 per_component_coverage_summary[component])
638
639 html_generator.CreateTotalsEntry(totals_coverage_summary)
640 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14641 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33642
643
644def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37645 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33646 html_index_file_path = os.path.join(OUTPUT_DIR,
647 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37648 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
649 DIRECTORY_VIEW_INDEX_FILE)
650 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
651
652
653def _WriteRedirectHtmlFile(from_html_path, to_html_path):
654 """Writes a html file that redirects to another html file."""
655 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
656 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33657 content = ("""
658 <!DOCTYPE html>
659 <html>
660 <head>
661 <!-- HTML meta refresh URL redirection -->
662 <meta http-equiv="refresh" content="0; url=%s">
663 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37664 </html>""" % to_html_relative_path)
665 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33666 f.write(content)
667
668
Yuke Liaodd1ec0592018-02-02 01:26:37669def _GetCoverageHtmlReportPathForFile(file_path):
670 """Given a file path, returns the corresponding html report path."""
671 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
672 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
673
674 # '+' is used instead of os.path.join because both of them are absolute paths
675 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14676 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37677 return _GetCoverageReportRootDirPath() + html_report_path
678
679
680def _GetCoverageHtmlReportPathForDirectory(dir_path):
681 """Given a directory path, returns the corresponding html report path."""
682 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
683 html_report_path = os.path.join(
684 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
685
686 # '+' is used instead of os.path.join because both of them are absolute paths
687 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14688 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37689 return _GetCoverageReportRootDirPath() + html_report_path
690
691
692def _GetCoverageHtmlReportPathForComponent(component_name):
693 """Given a component, returns the corresponding html report path."""
694 component_file_name = component_name.lower().replace('>', '-')
695 html_report_name = os.extsep.join([component_file_name, 'html'])
696 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
697 html_report_name)
698
699
700def _GetCoverageReportRootDirPath():
701 """The root directory that contains all generated coverage html reports."""
702 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33703
704
Yuke Liao506e8822017-12-04 16:52:54705def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
706 """Builds and runs target to generate the coverage profile data.
707
708 Args:
709 targets: A list of targets to build with coverage instrumentation.
710 commands: A list of commands used to run the targets.
711 jobs_count: Number of jobs to run in parallel for building. If None, a
712 default value is derived based on CPUs availability.
713
714 Returns:
715 A relative path to the generated profdata file.
716 """
717 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59718 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
719 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54720 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
721 profraw_file_paths)
722
Yuke Liaod4a9865202018-01-12 23:17:52723 for profraw_file_path in profraw_file_paths:
724 os.remove(profraw_file_path)
725
Yuke Liao506e8822017-12-04 16:52:54726 return profdata_file_path
727
728
729def _BuildTargets(targets, jobs_count):
730 """Builds target with Clang coverage instrumentation.
731
732 This function requires current working directory to be the root of checkout.
733
734 Args:
735 targets: A list of targets to build with coverage instrumentation.
736 jobs_count: Number of jobs to run in parallel for compilation. If None, a
737 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54738 """
Abhishek Arya1ec832c2017-12-05 18:06:59739
Yuke Liao506e8822017-12-04 16:52:54740 def _IsGomaConfigured():
741 """Returns True if goma is enabled in the gn build args.
742
743 Returns:
744 A boolean indicates whether goma is configured for building or not.
745 """
Yuke Liao80afff32018-03-07 01:26:20746 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54747 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
748
Yuke Liao481d3482018-01-29 19:17:10749 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54750 if jobs_count is None and _IsGomaConfigured():
751 jobs_count = DEFAULT_GOMA_JOBS
752
753 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
754 if jobs_count is not None:
755 subprocess_cmd.append('-j' + str(jobs_count))
756
757 subprocess_cmd.extend(targets)
758 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10759 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54760
761
762def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
763 """Runs commands and returns the relative paths to the profraw data files.
764
765 Args:
766 targets: A list of targets built with coverage instrumentation.
767 commands: A list of commands used to run the targets.
768
769 Returns:
770 A list of relative paths to the generated profraw data files.
771 """
Yuke Liao481d3482018-01-29 19:17:10772 logging.debug('Executing the test commands')
773
Yuke Liao506e8822017-12-04 16:52:54774 # Remove existing profraw data files.
775 for file_or_dir in os.listdir(OUTPUT_DIR):
776 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
777 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
778
Yuke Liaoa0c8c2f2018-02-28 20:14:10779 profraw_file_paths = []
780
Yuke Liaod4a9865202018-01-12 23:17:52781 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54782 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10783 output_file_name = os.extsep.join([target + '_output', 'txt'])
784 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
785 logging.info('Running command: "%s", the output is redirected to "%s"',
786 command, output_file_path)
787
Yuke Liaob2926832018-03-02 17:34:29788 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10789 # On iOS platform, due to lack of write permissions, profraw files are
790 # generated outside of the OUTPUT_DIR, and the exact paths are contained
791 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29792 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10793 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
794 else:
795 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
796 output = _ExecuteCommand(target, command)
797
798 with open(output_file_path, 'w') as output_file:
799 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54800
Yuke Liao481d3482018-01-29 19:17:10801 logging.debug('Finished executing the test commands')
802
Yuke Liaob2926832018-03-02 17:34:29803 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10804 return profraw_file_paths
805
Yuke Liao506e8822017-12-04 16:52:54806 for file_or_dir in os.listdir(OUTPUT_DIR):
807 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
808 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
809
810 # Assert one target/command generates at least one profraw data file.
811 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59812 assert any(
813 os.path.basename(profraw_file).startswith(target)
814 for profraw_file in profraw_file_paths), (
815 'Running target: %s failed to generate any profraw data file, '
816 'please make sure the binary exists and is properly instrumented.' %
817 target)
Yuke Liao506e8822017-12-04 16:52:54818
819 return profraw_file_paths
820
821
822def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10823 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52824 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01825 #
826 # "%p" expands out to the process ID.
827 #
Yuke Liaod4a9865202018-01-12 23:17:52828 # "%Nm" expands out to the instrumented binary's signature. When this pattern
829 # is specified, the runtime creates a pool of N raw profiles which are used
830 # for on-line profile merging. The runtime takes care of selecting a raw
831 # profile from the pool, locking it, and updating it before the program exits.
832 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
833 # N must be between 1 and 9. The merge pool specifier can only occur once per
834 # filename pattern.
835 #
Yuke Liao27349c92018-03-22 21:10:01836 # "%p" is used when tests run in single process, however, it can't be used for
837 # multi-process because each process produces an intermediate dump, which may
838 # consume hundreds of gigabytes of disk space.
839 #
840 # For "%Nm", 4 is chosen because it creates some level of parallelism, but
841 # it's not too big to consume too much computing resource or disk space.
842 profile_pattern_string = '%p' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59843 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01844 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54845 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
846 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54847
Yuke Liaoa0c8c2f2018-02-28 20:14:10848 try:
849 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29850 shlex.split(command),
851 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10852 except subprocess.CalledProcessError as e:
853 output = e.output
854 logging.warning('Command: "%s" exited with non-zero return code', command)
855
856 return output
857
858
Yuke Liao27349c92018-03-22 21:10:01859def _IsFuzzerTarget(target):
860 """Returns true if the target is a fuzzer target."""
861 build_args = _GetBuildArgs()
862 use_libfuzzer = ('use_libfuzzer' in build_args and
863 build_args['use_libfuzzer'] == 'true')
864 return use_libfuzzer and target.endswith('_fuzzer')
865
866
Yuke Liaob2926832018-03-02 17:34:29867def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10868 """Runs a single iOS command and generates a profraw data file.
869
870 iOS application doesn't have write access to folders outside of the app, so
871 it's impossible to instruct the app to flush the profraw data file to the
872 desired location. The profraw data file will be generated somewhere within the
873 application's Documents folder, and the full path can be obtained by parsing
874 the output.
875 """
Yuke Liaob2926832018-03-02 17:34:29876 assert _IsIOSCommand(command)
877
878 # After running tests, iossim generates a profraw data file, it won't be
879 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
880 # checkout.
881 iossim_profraw_file_path = os.path.join(
882 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10883
884 try:
Yuke Liaob2926832018-03-02 17:34:29885 output = subprocess.check_output(
886 shlex.split(command),
887 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10888 except subprocess.CalledProcessError as e:
889 # iossim emits non-zero return code even if tests run successfully, so
890 # ignore the return code.
891 output = e.output
892
893 return output
894
895
896def _GetProfrawDataFileByParsingOutput(output):
897 """Returns the path to the profraw data file obtained by parsing the output.
898
899 The output of running the test target has no format, but it is guaranteed to
900 have a single line containing the path to the generated profraw data file.
901 NOTE: This should only be called when target os is iOS.
902 """
Yuke Liaob2926832018-03-02 17:34:29903 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10904
Yuke Liaob2926832018-03-02 17:34:29905 output_by_lines = ''.join(output).splitlines()
906 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10907
908 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29909 result = profraw_file_pattern.match(line)
910 if result:
911 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10912
913 assert False, ('No profraw data file was generated, did you call '
914 'coverage_util::ConfigureCoverageReportPath() in test setup? '
915 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54916
917
918def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
919 """Returns a relative path to the profdata file by merging profraw data files.
920
921 Args:
922 profraw_file_paths: A list of relative paths to the profraw data files that
923 are to be merged.
924
925 Returns:
926 A relative path to the generated profdata file.
927
928 Raises:
929 CalledProcessError: An error occurred merging profraw data files.
930 """
Yuke Liao481d3482018-01-29 19:17:10931 logging.info('Creating the coverage profile data file')
932 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54933 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
934 try:
Abhishek Arya1ec832c2017-12-05 18:06:59935 subprocess_cmd = [
936 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
937 ]
Yuke Liao506e8822017-12-04 16:52:54938 subprocess_cmd.extend(profraw_file_paths)
939 subprocess.check_call(subprocess_cmd)
940 except subprocess.CalledProcessError as error:
941 print('Failed to merge profraw files to create profdata file')
942 raise error
943
Yuke Liao481d3482018-01-29 19:17:10944 logging.debug('Finished merging profraw files')
945 logging.info('Code coverage profile data is created as: %s',
946 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54947 return profdata_file_path
948
949
Yuke Liao0e4c8682018-04-18 21:06:59950def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
951 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:33952 """Generates per file coverage summary using "llvm-cov export" command."""
953 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
954 # [[-object BIN]] [SOURCES].
955 # NOTE: For object files, the first one is specified as a positional argument,
956 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10957 logging.debug('Generating per-file code coverage summary using "llvm-cov '
958 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33959 subprocess_cmd = [
960 LLVM_COV_PATH, 'export', '-summary-only',
961 '-instr-profile=' + profdata_file_path, binary_paths[0]
962 ]
963 subprocess_cmd.extend(
964 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29965 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33966 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59967 if ignore_filename_regex:
968 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:33969
970 json_output = json.loads(subprocess.check_output(subprocess_cmd))
971 assert len(json_output['data']) == 1
972 files_coverage_data = json_output['data'][0]['files']
973
974 per_file_coverage_summary = {}
975 for file_coverage_data in files_coverage_data:
976 file_path = file_coverage_data['filename']
977 summary = file_coverage_data['summary']
978
Yuke Liaoea228d02018-01-05 19:10:33979 if summary['lines']['count'] == 0:
980 continue
981
982 per_file_coverage_summary[file_path] = _CoverageSummary(
983 regions_total=summary['regions']['count'],
984 regions_covered=summary['regions']['covered'],
985 functions_total=summary['functions']['count'],
986 functions_covered=summary['functions']['covered'],
987 lines_total=summary['lines']['count'],
988 lines_covered=summary['lines']['covered'])
989
Yuke Liao481d3482018-01-29 19:17:10990 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33991 return per_file_coverage_summary
992
993
Yuke Liaob2926832018-03-02 17:34:29994def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
995 """Appends -arch arguments to the command list if it's ios platform.
996
997 iOS binaries are universal binaries, and require specifying the architecture
998 to use, and one architecture needs to be specified for each binary.
999 """
1000 if _IsIOS():
1001 cmd_list.extend(['-arch=x86_64'] * num_archs)
1002
1003
Yuke Liao506e8822017-12-04 16:52:541004def _GetBinaryPath(command):
1005 """Returns a relative path to the binary to be run by the command.
1006
Yuke Liao545db322018-02-15 17:12:011007 Currently, following types of commands are supported (e.g. url_unittests):
1008 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1009 2. Use xvfb.
1010 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1011 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371012 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1013 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101014 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371015 <iossim_arguments> -c <app_arguments>
1016 out/Coverage-iphonesimulator/url_unittests.app"
1017
Yuke Liao545db322018-02-15 17:12:011018
Yuke Liao506e8822017-12-04 16:52:541019 Args:
1020 command: A command used to run a target.
1021
1022 Returns:
1023 A relative path to the binary.
1024 """
Yuke Liao545db322018-02-15 17:12:011025 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1026
Yuke Liaob2926832018-03-02 17:34:291027 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011028 if os.path.basename(command_parts[0]) == 'python':
1029 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1030 'This tool doesn\'t understand the command: "%s"' % command)
1031 return command_parts[2]
1032
1033 if os.path.basename(command_parts[0]) == xvfb_script_name:
1034 return command_parts[1]
1035
Yuke Liaob2926832018-03-02 17:34:291036 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101037 # For a given application bundle, the binary resides in the bundle and has
1038 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371039 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101040 app_name = os.path.splitext(os.path.basename(app_path))[0]
1041 return os.path.join(app_path, app_name)
1042
Yuke Liaob2926832018-03-02 17:34:291043 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541044
1045
Yuke Liaob2926832018-03-02 17:34:291046def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101047 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291048 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101049
1050
Yuke Liao95d13d72017-12-07 18:18:501051def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1052 """Verifies that the target executables specified in the commands are inside
1053 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541054 for command in commands:
1055 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501056 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1057 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1058 'Target executable "%s" in command: "%s" is outside of '
1059 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541060
1061
1062def _ValidateBuildingWithClangCoverage():
1063 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201064 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541065
1066 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1067 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591068 assert False, ('\'{} = true\' is required in args.gn.'
1069 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541070
1071
Yuke Liaoc60b2d02018-03-02 21:40:431072def _ValidateCurrentPlatformIsSupported():
1073 """Asserts that this script suports running on the current platform"""
1074 target_os = _GetTargetOS()
1075 if target_os:
1076 current_platform = target_os
1077 else:
1078 current_platform = _GetHostPlatform()
1079
1080 assert current_platform in [
1081 'linux', 'mac', 'chromeos', 'ios'
1082 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1083
1084
Yuke Liao80afff32018-03-07 01:26:201085def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541086 """Parses args.gn file and returns results as a dictionary.
1087
1088 Returns:
1089 A dictionary representing the build args.
1090 """
Yuke Liao80afff32018-03-07 01:26:201091 global _BUILD_ARGS
1092 if _BUILD_ARGS is not None:
1093 return _BUILD_ARGS
1094
1095 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541096 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1097 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1098 'missing args.gn file.' % BUILD_DIR)
1099 with open(build_args_path) as build_args_file:
1100 build_args_lines = build_args_file.readlines()
1101
Yuke Liao506e8822017-12-04 16:52:541102 for build_arg_line in build_args_lines:
1103 build_arg_without_comments = build_arg_line.split('#')[0]
1104 key_value_pair = build_arg_without_comments.split('=')
1105 if len(key_value_pair) != 2:
1106 continue
1107
1108 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431109
1110 # Values are wrapped within a pair of double-quotes, so remove the leading
1111 # and trailing double-quotes.
1112 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201113 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541114
Yuke Liao80afff32018-03-07 01:26:201115 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541116
1117
Abhishek Arya16f059a2017-12-07 17:47:321118def _VerifyPathsAndReturnAbsolutes(paths):
1119 """Verifies that the paths specified in |paths| exist and returns absolute
1120 versions.
Yuke Liao66da1732017-12-05 22:19:421121
1122 Args:
1123 paths: A list of files or directories.
1124 """
Abhishek Arya16f059a2017-12-07 17:47:321125 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421126 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321127 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1128 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1129
1130 absolute_paths.append(absolute_path)
1131
1132 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421133
1134
Yuke Liaodd1ec0592018-02-02 01:26:371135def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1136 """Returns a target path relative to the directory of base_path.
1137
1138 This method requires base_path to be a file, otherwise, one should call
1139 os.path.relpath directly.
1140 """
1141 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141142 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371143 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141144 base_dir = os.path.dirname(base_path)
1145 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371146
1147
Yuke Liao506e8822017-12-04 16:52:541148def _ParseCommandArguments():
1149 """Adds and parses relevant arguments for tool comands.
1150
1151 Returns:
1152 A dictionary representing the arguments.
1153 """
1154 arg_parser = argparse.ArgumentParser()
1155 arg_parser.usage = __doc__
1156
Abhishek Arya1ec832c2017-12-05 18:06:591157 arg_parser.add_argument(
1158 '-b',
1159 '--build-dir',
1160 type=str,
1161 required=True,
1162 help='The build directory, the path needs to be relative to the root of '
1163 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541164
Abhishek Arya1ec832c2017-12-05 18:06:591165 arg_parser.add_argument(
1166 '-o',
1167 '--output-dir',
1168 type=str,
1169 required=True,
1170 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541171
Abhishek Arya1ec832c2017-12-05 18:06:591172 arg_parser.add_argument(
1173 '-c',
1174 '--command',
1175 action='append',
1176 required=True,
1177 help='Commands used to run test targets, one test target needs one and '
1178 'only one command, when specifying commands, one should assume the '
1179 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541180
Abhishek Arya1ec832c2017-12-05 18:06:591181 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421182 '-f',
1183 '--filters',
1184 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321185 required=False,
Yuke Liao66da1732017-12-05 22:19:421186 help='Directories or files to get code coverage for, and all files under '
1187 'the directories are included recursively.')
1188
1189 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591190 '-i',
1191 '--ignore-filename-regex',
1192 type=str,
1193 help='Skip source code files with file paths that match the given '
1194 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1195 'to exclude files in third_party/ and out/ folders from the report.')
1196
1197 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591198 '-j',
1199 '--jobs',
1200 type=int,
1201 default=None,
1202 help='Run N jobs to build in parallel. If not specified, a default value '
1203 'will be derived based on CPUs availability. Please refer to '
1204 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541205
Abhishek Arya1ec832c2017-12-05 18:06:591206 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101207 '-v',
1208 '--verbose',
1209 action='store_true',
1210 help='Prints additional output for diagnostics.')
1211
1212 arg_parser.add_argument(
1213 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1214
1215 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591216 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541217
1218 args = arg_parser.parse_args()
1219 return args
1220
1221
1222def Main():
1223 """Execute tool commands."""
1224 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1225 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591226 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541227 args = _ParseCommandArguments()
1228 global BUILD_DIR
1229 BUILD_DIR = args.build_dir
1230 global OUTPUT_DIR
1231 OUTPUT_DIR = args.output_dir
1232
1233 assert len(args.targets) == len(args.command), ('Number of targets must be '
1234 'equal to the number of test '
1235 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431236
1237 # logging should be configured before it is used.
1238 log_level = logging.DEBUG if args.verbose else logging.INFO
1239 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1240 log_file = args.log_file if args.log_file else None
1241 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1242
Abhishek Arya1ec832c2017-12-05 18:06:591243 assert os.path.exists(BUILD_DIR), (
1244 'Build directory: {} doesn\'t exist. '
1245 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431246 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541247 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501248 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321249
Yuke Liaoc60b2d02018-03-02 21:40:431250 DownloadCoverageToolsIfNeeded()
1251
Abhishek Arya16f059a2017-12-07 17:47:321252 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421253 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321254 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421255
Yuke Liao506e8822017-12-04 16:52:541256 if not os.path.exists(OUTPUT_DIR):
1257 os.makedirs(OUTPUT_DIR)
1258
Abhishek Arya1ec832c2017-12-05 18:06:591259 profdata_file_path = _CreateCoverageProfileDataForTargets(
1260 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541261 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331262
Yuke Liao481d3482018-01-29 19:17:101263 logging.info('Generating code coverage report in html (this can take a while '
1264 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371265 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591266 binary_paths, profdata_file_path, absolute_filter_paths,
1267 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371268 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591269 absolute_filter_paths,
1270 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371271 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1272
1273 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1274 per_file_coverage_summary)
1275 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1276 per_file_coverage_summary)
1277 _GenerateDirectoryViewHtmlIndexFile()
1278
1279 component_to_directories = _ExtractComponentToDirectoriesMapping()
1280 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1281 component_to_directories, per_directory_coverage_summary)
1282 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1283 component_to_directories,
1284 per_directory_coverage_summary)
1285 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331286
1287 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371288 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331289 _OverwriteHtmlReportsIndexFile()
1290
Yuke Liao506e8822017-12-04 16:52:541291 html_index_file_path = 'file://' + os.path.abspath(
1292 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101293 logging.info('Index file for html report is generated as: %s',
1294 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541295
Abhishek Arya1ec832c2017-12-05 18:06:591296
Yuke Liao506e8822017-12-04 16:52:541297if __name__ == '__main__':
1298 sys.exit(Main())