blob: 117d64e3687da27529c6ce96547a96eb5d17fa18 [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 Liaoa785f4d32018-02-13 21:41:35252 percentage = float(summary_dict[feature]['covered']) / summary_dict[
253 feature]['total'] * 100
254
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,
415 filters):
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 Liao506e8822017-12-04 16:52:54442 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10443 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54444
445
Yuke Liaodd1ec0592018-02-02 01:26:37446def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
447 """Generates html index file for file view."""
448 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
449 logging.debug('Generating file view html index file as: "%s".',
450 file_view_index_file_path)
451 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
452 'Path')
453 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33454
Yuke Liaodd1ec0592018-02-02 01:26:37455 for file_path in per_file_coverage_summary:
456 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
457
458 html_generator.AddLinkToAnotherReport(
459 _GetCoverageHtmlReportPathForFile(file_path),
460 os.path.relpath(file_path, SRC_ROOT_PATH),
461 per_file_coverage_summary[file_path])
462
463 html_generator.CreateTotalsEntry(totals_coverage_summary)
464 html_generator.WriteHtmlCoverageReport()
465 logging.debug('Finished generating file view html index file.')
466
467
468def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
469 """Calculates per directory coverage summary."""
470 logging.debug('Calculating per-directory coverage summary')
471 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
472
Yuke Liaoea228d02018-01-05 19:10:33473 for file_path in per_file_coverage_summary:
474 summary = per_file_coverage_summary[file_path]
475 parent_dir = os.path.dirname(file_path)
476 while True:
477 per_directory_coverage_summary[parent_dir].AddSummary(summary)
478
479 if parent_dir == SRC_ROOT_PATH:
480 break
481 parent_dir = os.path.dirname(parent_dir)
482
Yuke Liaodd1ec0592018-02-02 01:26:37483 logging.debug('Finished calculating per-directory coverage summary')
484 return per_directory_coverage_summary
485
486
487def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
488 per_file_coverage_summary):
489 """Generates per directory coverage breakdown in html."""
490 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33491 for dir_path in per_directory_coverage_summary:
492 _GenerateCoverageInHtmlForDirectory(
493 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
494
Yuke Liaodd1ec0592018-02-02 01:26:37495 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10496
Yuke Liaoea228d02018-01-05 19:10:33497
498def _GenerateCoverageInHtmlForDirectory(
499 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
500 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37501 html_generator = _CoverageReportHtmlGenerator(
502 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33503
504 for entry_name in os.listdir(dir_path):
505 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33506
Yuke Liaodd1ec0592018-02-02 01:26:37507 if entry_path in per_file_coverage_summary:
508 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
509 entry_coverage_summary = per_file_coverage_summary[entry_path]
510 elif entry_path in per_directory_coverage_summary:
511 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
512 entry_path)
513 entry_coverage_summary = per_directory_coverage_summary[entry_path]
514 else:
Yuke Liaoc7e607142018-02-05 20:26:14515 # Any file without executable lines shouldn't be included into the report.
516 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37517 continue
Yuke Liaoea228d02018-01-05 19:10:33518
Yuke Liaodd1ec0592018-02-02 01:26:37519 html_generator.AddLinkToAnotherReport(entry_html_report_path,
520 os.path.basename(entry_path),
521 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33522
Yuke Liaod54030e2018-01-08 17:34:12523 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37524 html_generator.WriteHtmlCoverageReport()
525
526
527def _GenerateDirectoryViewHtmlIndexFile():
528 """Generates the html index file for directory view.
529
530 Note that the index file is already generated under SRC_ROOT_PATH, so this
531 file simply redirects to it, and the reason of this extra layer is for
532 structural consistency with other views.
533 """
534 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
535 DIRECTORY_VIEW_INDEX_FILE)
536 logging.debug('Generating directory view html index file as: "%s".',
537 directory_view_index_file_path)
538 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
539 SRC_ROOT_PATH)
540 _WriteRedirectHtmlFile(directory_view_index_file_path,
541 src_root_html_report_path)
542 logging.debug('Finished generating directory view html index file.')
543
544
545def _CalculatePerComponentCoverageSummary(component_to_directories,
546 per_directory_coverage_summary):
547 """Calculates per component coverage summary."""
548 logging.debug('Calculating per-component coverage summary')
549 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
550
551 for component in component_to_directories:
552 for directory in component_to_directories[component]:
553 absolute_directory_path = os.path.abspath(directory)
554 if absolute_directory_path in per_directory_coverage_summary:
555 per_component_coverage_summary[component].AddSummary(
556 per_directory_coverage_summary[absolute_directory_path])
557
558 logging.debug('Finished calculating per-component coverage summary')
559 return per_component_coverage_summary
560
561
562def _ExtractComponentToDirectoriesMapping():
563 """Returns a mapping from components to directories."""
564 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
565 directory_to_component = component_mappings['dir-to-component']
566
567 component_to_directories = defaultdict(list)
568 for directory in directory_to_component:
569 component = directory_to_component[directory]
570 component_to_directories[component].append(directory)
571
572 return component_to_directories
573
574
575def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
576 component_to_directories,
577 per_directory_coverage_summary):
578 """Generates per-component coverage reports in html."""
579 logging.debug('Writing per-component coverage html reports.')
580 for component in per_component_coverage_summary:
581 _GenerateCoverageInHtmlForComponent(
582 component, per_component_coverage_summary, component_to_directories,
583 per_directory_coverage_summary)
584
585 logging.debug('Finished writing per-component coverage html reports.')
586
587
588def _GenerateCoverageInHtmlForComponent(
589 component_name, per_component_coverage_summary, component_to_directories,
590 per_directory_coverage_summary):
591 """Generates coverage html report for a component."""
592 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
593 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14594 component_html_report_dir = os.path.dirname(component_html_report_path)
595 if not os.path.exists(component_html_report_dir):
596 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37597
598 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
599 'Path')
600
601 for dir_path in component_to_directories[component_name]:
602 dir_absolute_path = os.path.abspath(dir_path)
603 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14604 # Any directory without an excercised file shouldn't be included into the
605 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37606 continue
607
608 html_generator.AddLinkToAnotherReport(
609 _GetCoverageHtmlReportPathForDirectory(dir_path),
610 os.path.relpath(dir_path, SRC_ROOT_PATH),
611 per_directory_coverage_summary[dir_absolute_path])
612
613 html_generator.CreateTotalsEntry(
614 per_component_coverage_summary[component_name])
615 html_generator.WriteHtmlCoverageReport()
616
617
618def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
619 """Generates the html index file for component view."""
620 component_view_index_file_path = os.path.join(OUTPUT_DIR,
621 COMPONENT_VIEW_INDEX_FILE)
622 logging.debug('Generating component view html index file as: "%s".',
623 component_view_index_file_path)
624 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
625 'Component')
626 totals_coverage_summary = _CoverageSummary()
627
628 for component in per_component_coverage_summary:
629 totals_coverage_summary.AddSummary(
630 per_component_coverage_summary[component])
631
632 html_generator.AddLinkToAnotherReport(
633 _GetCoverageHtmlReportPathForComponent(component), component,
634 per_component_coverage_summary[component])
635
636 html_generator.CreateTotalsEntry(totals_coverage_summary)
637 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14638 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33639
640
641def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37642 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33643 html_index_file_path = os.path.join(OUTPUT_DIR,
644 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37645 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
646 DIRECTORY_VIEW_INDEX_FILE)
647 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
648
649
650def _WriteRedirectHtmlFile(from_html_path, to_html_path):
651 """Writes a html file that redirects to another html file."""
652 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
653 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33654 content = ("""
655 <!DOCTYPE html>
656 <html>
657 <head>
658 <!-- HTML meta refresh URL redirection -->
659 <meta http-equiv="refresh" content="0; url=%s">
660 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37661 </html>""" % to_html_relative_path)
662 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33663 f.write(content)
664
665
Yuke Liaodd1ec0592018-02-02 01:26:37666def _GetCoverageHtmlReportPathForFile(file_path):
667 """Given a file path, returns the corresponding html report path."""
668 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
669 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
670
671 # '+' is used instead of os.path.join because both of them are absolute paths
672 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14673 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37674 return _GetCoverageReportRootDirPath() + html_report_path
675
676
677def _GetCoverageHtmlReportPathForDirectory(dir_path):
678 """Given a directory path, returns the corresponding html report path."""
679 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
680 html_report_path = os.path.join(
681 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
682
683 # '+' is used instead of os.path.join because both of them are absolute paths
684 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14685 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37686 return _GetCoverageReportRootDirPath() + html_report_path
687
688
689def _GetCoverageHtmlReportPathForComponent(component_name):
690 """Given a component, returns the corresponding html report path."""
691 component_file_name = component_name.lower().replace('>', '-')
692 html_report_name = os.extsep.join([component_file_name, 'html'])
693 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
694 html_report_name)
695
696
697def _GetCoverageReportRootDirPath():
698 """The root directory that contains all generated coverage html reports."""
699 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33700
701
Yuke Liao506e8822017-12-04 16:52:54702def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
703 """Builds and runs target to generate the coverage profile data.
704
705 Args:
706 targets: A list of targets to build with coverage instrumentation.
707 commands: A list of commands used to run the targets.
708 jobs_count: Number of jobs to run in parallel for building. If None, a
709 default value is derived based on CPUs availability.
710
711 Returns:
712 A relative path to the generated profdata file.
713 """
714 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59715 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
716 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54717 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
718 profraw_file_paths)
719
Yuke Liaod4a9865202018-01-12 23:17:52720 for profraw_file_path in profraw_file_paths:
721 os.remove(profraw_file_path)
722
Yuke Liao506e8822017-12-04 16:52:54723 return profdata_file_path
724
725
726def _BuildTargets(targets, jobs_count):
727 """Builds target with Clang coverage instrumentation.
728
729 This function requires current working directory to be the root of checkout.
730
731 Args:
732 targets: A list of targets to build with coverage instrumentation.
733 jobs_count: Number of jobs to run in parallel for compilation. If None, a
734 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54735 """
Abhishek Arya1ec832c2017-12-05 18:06:59736
Yuke Liao506e8822017-12-04 16:52:54737 def _IsGomaConfigured():
738 """Returns True if goma is enabled in the gn build args.
739
740 Returns:
741 A boolean indicates whether goma is configured for building or not.
742 """
Yuke Liao80afff32018-03-07 01:26:20743 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54744 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
745
Yuke Liao481d3482018-01-29 19:17:10746 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54747 if jobs_count is None and _IsGomaConfigured():
748 jobs_count = DEFAULT_GOMA_JOBS
749
750 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
751 if jobs_count is not None:
752 subprocess_cmd.append('-j' + str(jobs_count))
753
754 subprocess_cmd.extend(targets)
755 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10756 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54757
758
759def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
760 """Runs commands and returns the relative paths to the profraw data files.
761
762 Args:
763 targets: A list of targets built with coverage instrumentation.
764 commands: A list of commands used to run the targets.
765
766 Returns:
767 A list of relative paths to the generated profraw data files.
768 """
Yuke Liao481d3482018-01-29 19:17:10769 logging.debug('Executing the test commands')
770
Yuke Liao506e8822017-12-04 16:52:54771 # Remove existing profraw data files.
772 for file_or_dir in os.listdir(OUTPUT_DIR):
773 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
774 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
775
Yuke Liaoa0c8c2f2018-02-28 20:14:10776 profraw_file_paths = []
777
Yuke Liaod4a9865202018-01-12 23:17:52778 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54779 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10780 output_file_name = os.extsep.join([target + '_output', 'txt'])
781 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
782 logging.info('Running command: "%s", the output is redirected to "%s"',
783 command, output_file_path)
784
Yuke Liaob2926832018-03-02 17:34:29785 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10786 # On iOS platform, due to lack of write permissions, profraw files are
787 # generated outside of the OUTPUT_DIR, and the exact paths are contained
788 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29789 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10790 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
791 else:
792 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
793 output = _ExecuteCommand(target, command)
794
795 with open(output_file_path, 'w') as output_file:
796 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54797
Yuke Liao481d3482018-01-29 19:17:10798 logging.debug('Finished executing the test commands')
799
Yuke Liaob2926832018-03-02 17:34:29800 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10801 return profraw_file_paths
802
Yuke Liao506e8822017-12-04 16:52:54803 for file_or_dir in os.listdir(OUTPUT_DIR):
804 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
805 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
806
807 # Assert one target/command generates at least one profraw data file.
808 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59809 assert any(
810 os.path.basename(profraw_file).startswith(target)
811 for profraw_file in profraw_file_paths), (
812 'Running target: %s failed to generate any profraw data file, '
813 'please make sure the binary exists and is properly instrumented.' %
814 target)
Yuke Liao506e8822017-12-04 16:52:54815
816 return profraw_file_paths
817
818
819def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10820 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52821 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01822 #
823 # "%p" expands out to the process ID.
824 #
Yuke Liaod4a9865202018-01-12 23:17:52825 # "%Nm" expands out to the instrumented binary's signature. When this pattern
826 # is specified, the runtime creates a pool of N raw profiles which are used
827 # for on-line profile merging. The runtime takes care of selecting a raw
828 # profile from the pool, locking it, and updating it before the program exits.
829 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
830 # N must be between 1 and 9. The merge pool specifier can only occur once per
831 # filename pattern.
832 #
Yuke Liao27349c92018-03-22 21:10:01833 # "%p" is used when tests run in single process, however, it can't be used for
834 # multi-process because each process produces an intermediate dump, which may
835 # consume hundreds of gigabytes of disk space.
836 #
837 # For "%Nm", 4 is chosen because it creates some level of parallelism, but
838 # it's not too big to consume too much computing resource or disk space.
839 profile_pattern_string = '%p' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59840 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01841 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54842 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
843 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54844
Yuke Liaoa0c8c2f2018-02-28 20:14:10845 try:
846 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29847 shlex.split(command),
848 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10849 except subprocess.CalledProcessError as e:
850 output = e.output
851 logging.warning('Command: "%s" exited with non-zero return code', command)
852
853 return output
854
855
Yuke Liao27349c92018-03-22 21:10:01856def _IsFuzzerTarget(target):
857 """Returns true if the target is a fuzzer target."""
858 build_args = _GetBuildArgs()
859 use_libfuzzer = ('use_libfuzzer' in build_args and
860 build_args['use_libfuzzer'] == 'true')
861 return use_libfuzzer and target.endswith('_fuzzer')
862
863
Yuke Liaob2926832018-03-02 17:34:29864def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10865 """Runs a single iOS command and generates a profraw data file.
866
867 iOS application doesn't have write access to folders outside of the app, so
868 it's impossible to instruct the app to flush the profraw data file to the
869 desired location. The profraw data file will be generated somewhere within the
870 application's Documents folder, and the full path can be obtained by parsing
871 the output.
872 """
Yuke Liaob2926832018-03-02 17:34:29873 assert _IsIOSCommand(command)
874
875 # After running tests, iossim generates a profraw data file, it won't be
876 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
877 # checkout.
878 iossim_profraw_file_path = os.path.join(
879 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10880
881 try:
Yuke Liaob2926832018-03-02 17:34:29882 output = subprocess.check_output(
883 shlex.split(command),
884 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10885 except subprocess.CalledProcessError as e:
886 # iossim emits non-zero return code even if tests run successfully, so
887 # ignore the return code.
888 output = e.output
889
890 return output
891
892
893def _GetProfrawDataFileByParsingOutput(output):
894 """Returns the path to the profraw data file obtained by parsing the output.
895
896 The output of running the test target has no format, but it is guaranteed to
897 have a single line containing the path to the generated profraw data file.
898 NOTE: This should only be called when target os is iOS.
899 """
Yuke Liaob2926832018-03-02 17:34:29900 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10901
Yuke Liaob2926832018-03-02 17:34:29902 output_by_lines = ''.join(output).splitlines()
903 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10904
905 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29906 result = profraw_file_pattern.match(line)
907 if result:
908 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10909
910 assert False, ('No profraw data file was generated, did you call '
911 'coverage_util::ConfigureCoverageReportPath() in test setup? '
912 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54913
914
915def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
916 """Returns a relative path to the profdata file by merging profraw data files.
917
918 Args:
919 profraw_file_paths: A list of relative paths to the profraw data files that
920 are to be merged.
921
922 Returns:
923 A relative path to the generated profdata file.
924
925 Raises:
926 CalledProcessError: An error occurred merging profraw data files.
927 """
Yuke Liao481d3482018-01-29 19:17:10928 logging.info('Creating the coverage profile data file')
929 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54930 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
931 try:
Abhishek Arya1ec832c2017-12-05 18:06:59932 subprocess_cmd = [
933 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
934 ]
Yuke Liao506e8822017-12-04 16:52:54935 subprocess_cmd.extend(profraw_file_paths)
936 subprocess.check_call(subprocess_cmd)
937 except subprocess.CalledProcessError as error:
938 print('Failed to merge profraw files to create profdata file')
939 raise error
940
Yuke Liao481d3482018-01-29 19:17:10941 logging.debug('Finished merging profraw files')
942 logging.info('Code coverage profile data is created as: %s',
943 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54944 return profdata_file_path
945
946
Yuke Liaoea228d02018-01-05 19:10:33947def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
948 """Generates per file coverage summary using "llvm-cov export" command."""
949 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
950 # [[-object BIN]] [SOURCES].
951 # NOTE: For object files, the first one is specified as a positional argument,
952 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10953 logging.debug('Generating per-file code coverage summary using "llvm-cov '
954 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33955 subprocess_cmd = [
956 LLVM_COV_PATH, 'export', '-summary-only',
957 '-instr-profile=' + profdata_file_path, binary_paths[0]
958 ]
959 subprocess_cmd.extend(
960 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29961 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33962 subprocess_cmd.extend(filters)
963
964 json_output = json.loads(subprocess.check_output(subprocess_cmd))
965 assert len(json_output['data']) == 1
966 files_coverage_data = json_output['data'][0]['files']
967
968 per_file_coverage_summary = {}
969 for file_coverage_data in files_coverage_data:
970 file_path = file_coverage_data['filename']
971 summary = file_coverage_data['summary']
972
Yuke Liaoea228d02018-01-05 19:10:33973 if summary['lines']['count'] == 0:
974 continue
975
976 per_file_coverage_summary[file_path] = _CoverageSummary(
977 regions_total=summary['regions']['count'],
978 regions_covered=summary['regions']['covered'],
979 functions_total=summary['functions']['count'],
980 functions_covered=summary['functions']['covered'],
981 lines_total=summary['lines']['count'],
982 lines_covered=summary['lines']['covered'])
983
Yuke Liao481d3482018-01-29 19:17:10984 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33985 return per_file_coverage_summary
986
987
Yuke Liaob2926832018-03-02 17:34:29988def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
989 """Appends -arch arguments to the command list if it's ios platform.
990
991 iOS binaries are universal binaries, and require specifying the architecture
992 to use, and one architecture needs to be specified for each binary.
993 """
994 if _IsIOS():
995 cmd_list.extend(['-arch=x86_64'] * num_archs)
996
997
Yuke Liao506e8822017-12-04 16:52:54998def _GetBinaryPath(command):
999 """Returns a relative path to the binary to be run by the command.
1000
Yuke Liao545db322018-02-15 17:12:011001 Currently, following types of commands are supported (e.g. url_unittests):
1002 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1003 2. Use xvfb.
1004 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1005 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371006 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1007 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101008 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371009 <iossim_arguments> -c <app_arguments>
1010 out/Coverage-iphonesimulator/url_unittests.app"
1011
Yuke Liao545db322018-02-15 17:12:011012
Yuke Liao506e8822017-12-04 16:52:541013 Args:
1014 command: A command used to run a target.
1015
1016 Returns:
1017 A relative path to the binary.
1018 """
Yuke Liao545db322018-02-15 17:12:011019 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1020
Yuke Liaob2926832018-03-02 17:34:291021 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011022 if os.path.basename(command_parts[0]) == 'python':
1023 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1024 'This tool doesn\'t understand the command: "%s"' % command)
1025 return command_parts[2]
1026
1027 if os.path.basename(command_parts[0]) == xvfb_script_name:
1028 return command_parts[1]
1029
Yuke Liaob2926832018-03-02 17:34:291030 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101031 # For a given application bundle, the binary resides in the bundle and has
1032 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371033 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101034 app_name = os.path.splitext(os.path.basename(app_path))[0]
1035 return os.path.join(app_path, app_name)
1036
Yuke Liaob2926832018-03-02 17:34:291037 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541038
1039
Yuke Liaob2926832018-03-02 17:34:291040def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101041 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291042 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101043
1044
Yuke Liao95d13d72017-12-07 18:18:501045def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1046 """Verifies that the target executables specified in the commands are inside
1047 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541048 for command in commands:
1049 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501050 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1051 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1052 'Target executable "%s" in command: "%s" is outside of '
1053 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541054
1055
1056def _ValidateBuildingWithClangCoverage():
1057 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201058 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541059
1060 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1061 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591062 assert False, ('\'{} = true\' is required in args.gn.'
1063 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541064
1065
Yuke Liaoc60b2d02018-03-02 21:40:431066def _ValidateCurrentPlatformIsSupported():
1067 """Asserts that this script suports running on the current platform"""
1068 target_os = _GetTargetOS()
1069 if target_os:
1070 current_platform = target_os
1071 else:
1072 current_platform = _GetHostPlatform()
1073
1074 assert current_platform in [
1075 'linux', 'mac', 'chromeos', 'ios'
1076 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1077
1078
Yuke Liao80afff32018-03-07 01:26:201079def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541080 """Parses args.gn file and returns results as a dictionary.
1081
1082 Returns:
1083 A dictionary representing the build args.
1084 """
Yuke Liao80afff32018-03-07 01:26:201085 global _BUILD_ARGS
1086 if _BUILD_ARGS is not None:
1087 return _BUILD_ARGS
1088
1089 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541090 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1091 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1092 'missing args.gn file.' % BUILD_DIR)
1093 with open(build_args_path) as build_args_file:
1094 build_args_lines = build_args_file.readlines()
1095
Yuke Liao506e8822017-12-04 16:52:541096 for build_arg_line in build_args_lines:
1097 build_arg_without_comments = build_arg_line.split('#')[0]
1098 key_value_pair = build_arg_without_comments.split('=')
1099 if len(key_value_pair) != 2:
1100 continue
1101
1102 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431103
1104 # Values are wrapped within a pair of double-quotes, so remove the leading
1105 # and trailing double-quotes.
1106 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201107 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541108
Yuke Liao80afff32018-03-07 01:26:201109 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541110
1111
Abhishek Arya16f059a2017-12-07 17:47:321112def _VerifyPathsAndReturnAbsolutes(paths):
1113 """Verifies that the paths specified in |paths| exist and returns absolute
1114 versions.
Yuke Liao66da1732017-12-05 22:19:421115
1116 Args:
1117 paths: A list of files or directories.
1118 """
Abhishek Arya16f059a2017-12-07 17:47:321119 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421120 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321121 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1122 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1123
1124 absolute_paths.append(absolute_path)
1125
1126 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421127
1128
Yuke Liaodd1ec0592018-02-02 01:26:371129def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1130 """Returns a target path relative to the directory of base_path.
1131
1132 This method requires base_path to be a file, otherwise, one should call
1133 os.path.relpath directly.
1134 """
1135 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141136 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371137 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141138 base_dir = os.path.dirname(base_path)
1139 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371140
1141
Yuke Liao506e8822017-12-04 16:52:541142def _ParseCommandArguments():
1143 """Adds and parses relevant arguments for tool comands.
1144
1145 Returns:
1146 A dictionary representing the arguments.
1147 """
1148 arg_parser = argparse.ArgumentParser()
1149 arg_parser.usage = __doc__
1150
Abhishek Arya1ec832c2017-12-05 18:06:591151 arg_parser.add_argument(
1152 '-b',
1153 '--build-dir',
1154 type=str,
1155 required=True,
1156 help='The build directory, the path needs to be relative to the root of '
1157 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541158
Abhishek Arya1ec832c2017-12-05 18:06:591159 arg_parser.add_argument(
1160 '-o',
1161 '--output-dir',
1162 type=str,
1163 required=True,
1164 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541165
Abhishek Arya1ec832c2017-12-05 18:06:591166 arg_parser.add_argument(
1167 '-c',
1168 '--command',
1169 action='append',
1170 required=True,
1171 help='Commands used to run test targets, one test target needs one and '
1172 'only one command, when specifying commands, one should assume the '
1173 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541174
Abhishek Arya1ec832c2017-12-05 18:06:591175 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421176 '-f',
1177 '--filters',
1178 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321179 required=False,
Yuke Liao66da1732017-12-05 22:19:421180 help='Directories or files to get code coverage for, and all files under '
1181 'the directories are included recursively.')
1182
1183 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591184 '-j',
1185 '--jobs',
1186 type=int,
1187 default=None,
1188 help='Run N jobs to build in parallel. If not specified, a default value '
1189 'will be derived based on CPUs availability. Please refer to '
1190 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541191
Abhishek Arya1ec832c2017-12-05 18:06:591192 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101193 '-v',
1194 '--verbose',
1195 action='store_true',
1196 help='Prints additional output for diagnostics.')
1197
1198 arg_parser.add_argument(
1199 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1200
1201 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591202 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541203
1204 args = arg_parser.parse_args()
1205 return args
1206
1207
1208def Main():
1209 """Execute tool commands."""
1210 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1211 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591212 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541213 args = _ParseCommandArguments()
1214 global BUILD_DIR
1215 BUILD_DIR = args.build_dir
1216 global OUTPUT_DIR
1217 OUTPUT_DIR = args.output_dir
1218
1219 assert len(args.targets) == len(args.command), ('Number of targets must be '
1220 'equal to the number of test '
1221 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431222
1223 # logging should be configured before it is used.
1224 log_level = logging.DEBUG if args.verbose else logging.INFO
1225 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1226 log_file = args.log_file if args.log_file else None
1227 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1228
Abhishek Arya1ec832c2017-12-05 18:06:591229 assert os.path.exists(BUILD_DIR), (
1230 'Build directory: {} doesn\'t exist. '
1231 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431232 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541233 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501234 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321235
Yuke Liaoc60b2d02018-03-02 21:40:431236 DownloadCoverageToolsIfNeeded()
1237
Abhishek Arya16f059a2017-12-07 17:47:321238 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421239 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321240 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421241
Yuke Liao506e8822017-12-04 16:52:541242 if not os.path.exists(OUTPUT_DIR):
1243 os.makedirs(OUTPUT_DIR)
1244
Abhishek Arya1ec832c2017-12-05 18:06:591245 profdata_file_path = _CreateCoverageProfileDataForTargets(
1246 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541247 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331248
Yuke Liao481d3482018-01-29 19:17:101249 logging.info('Generating code coverage report in html (this can take a while '
1250 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371251 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1252 binary_paths, profdata_file_path, absolute_filter_paths)
1253 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1254 absolute_filter_paths)
1255 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1256
1257 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1258 per_file_coverage_summary)
1259 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1260 per_file_coverage_summary)
1261 _GenerateDirectoryViewHtmlIndexFile()
1262
1263 component_to_directories = _ExtractComponentToDirectoriesMapping()
1264 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1265 component_to_directories, per_directory_coverage_summary)
1266 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1267 component_to_directories,
1268 per_directory_coverage_summary)
1269 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331270
1271 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371272 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331273 _OverwriteHtmlReportsIndexFile()
1274
Yuke Liao506e8822017-12-04 16:52:541275 html_index_file_path = 'file://' + os.path.abspath(
1276 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101277 logging.info('Index file for html report is generated as: %s',
1278 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541279
Abhishek Arya1ec832c2017-12-05 18:06:591280
Yuke Liao506e8822017-12-04 16:52:541281if __name__ == '__main__':
1282 sys.exit(Main())