blob: f0f99d44c18a0524ca094b7df64598480c1882be [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3856
57 For an overview of how code coverage works in Chromium, please refer to
58 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5459"""
60
61from __future__ import print_function
62
63import sys
64
65import argparse
Yuke Liaoea228d02018-01-05 19:10:3366import json
Yuke Liao481d3482018-01-29 19:17:1067import logging
Yuke Liao506e8822017-12-04 16:52:5468import os
Yuke Liaob2926832018-03-02 17:34:2969import re
70import shlex
Max Moroz025d8952018-05-03 16:33:3471import shutil
Yuke Liao506e8822017-12-04 16:52:5472import subprocess
Yuke Liao506e8822017-12-04 16:52:5473import urllib2
74
Abhishek Arya1ec832c2017-12-05 18:06:5975sys.path.append(
76 os.path.join(
77 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
78 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5479import update as clang_update
80
Yuke Liaoea228d02018-01-05 19:10:3381sys.path.append(
82 os.path.join(
83 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
84 'third_party'))
85import jinja2
86from collections import defaultdict
87
Yuke Liao506e8822017-12-04 16:52:5488# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5989SRC_ROOT_PATH = os.path.abspath(
90 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5491
92# Absolute path to the code coverage tools binary.
93LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
94LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
95LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
96
97# Build directory, the value is parsed from command line arguments.
98BUILD_DIR = None
99
100# Output directory for generated artifacts, the value is parsed from command
101# line arguemnts.
102OUTPUT_DIR = None
103
104# Default number of jobs used to build when goma is configured and enabled.
105DEFAULT_GOMA_JOBS = 100
106
107# Name of the file extension for profraw data files.
108PROFRAW_FILE_EXTENSION = 'profraw'
109
110# Name of the final profdata file, and this file needs to be passed to
111# "llvm-cov" command in order to call "llvm-cov show" to inspect the
112# line-by-line coverage of specific files.
113PROFDATA_FILE_NAME = 'coverage.profdata'
114
115# Build arg required for generating code coverage data.
116CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
117
Yuke Liaoea228d02018-01-05 19:10:33118# The default name of the html coverage report for a directory.
119DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
120
Yuke Liaodd1ec0592018-02-02 01:26:37121# Name of the html index files for different views.
122DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
123COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
124FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
125
126# Used to extract a mapping between directories and components.
127COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
128
Yuke Liao80afff32018-03-07 01:26:20129# Caches the results returned by _GetBuildArgs, don't use this variable
130# directly, call _GetBuildArgs instead.
131_BUILD_ARGS = None
132
Yuke Liaoea228d02018-01-05 19:10:33133
134class _CoverageSummary(object):
135 """Encapsulates coverage summary representation."""
136
Yuke Liaodd1ec0592018-02-02 01:26:37137 def __init__(self,
138 regions_total=0,
139 regions_covered=0,
140 functions_total=0,
141 functions_covered=0,
142 lines_total=0,
143 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33144 """Initializes _CoverageSummary object."""
145 self._summary = {
146 'regions': {
147 'total': regions_total,
148 'covered': regions_covered
149 },
150 'functions': {
151 'total': functions_total,
152 'covered': functions_covered
153 },
154 'lines': {
155 'total': lines_total,
156 'covered': lines_covered
157 }
158 }
159
160 def Get(self):
161 """Returns summary as a dictionary."""
162 return self._summary
163
164 def AddSummary(self, other_summary):
165 """Adds another summary to this one element-wise."""
166 for feature in self._summary:
167 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
168 self._summary[feature]['covered'] += other_summary.Get()[feature][
169 'covered']
170
171
Yuke Liaodd1ec0592018-02-02 01:26:37172class _CoverageReportHtmlGenerator(object):
173 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33174
Yuke Liaodd1ec0592018-02-02 01:26:37175 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33176 """
177
Yuke Liaodd1ec0592018-02-02 01:26:37178 def __init__(self, output_path, table_entry_type):
179 """Initializes _CoverageReportHtmlGenerator object.
180
181 Args:
182 output_path: Path to the html report that will be generated.
183 table_entry_type: Type of the table entries to be displayed in the table
184 header. For example: 'Path', 'Component'.
185 """
Yuke Liaoea228d02018-01-05 19:10:33186 css_file_name = os.extsep.join(['style', 'css'])
187 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
188 assert os.path.exists(css_absolute_path), (
189 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
190 'is called first, and the css file is generated at: "%s"' %
191 css_absolute_path)
192
193 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37194 self._output_path = output_path
195 self._table_entry_type = table_entry_type
196
Yuke Liaoea228d02018-01-05 19:10:33197 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12198 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33199 template_dir = os.path.join(
200 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
201
202 jinja_env = jinja2.Environment(
203 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
204 self._header_template = jinja_env.get_template('header.html')
205 self._table_template = jinja_env.get_template('table.html')
206 self._footer_template = jinja_env.get_template('footer.html')
207
208 def AddLinkToAnotherReport(self, html_report_path, name, summary):
209 """Adds a link to another html report in this report.
210
211 The link to be added is assumed to be an entry in this directory.
212 """
Yuke Liaodd1ec0592018-02-02 01:26:37213 # Use relative paths instead of absolute paths to make the generated reports
214 # portable.
215 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
216 html_report_path, self._output_path)
217
Yuke Liaod54030e2018-01-08 17:34:12218 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37219 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12220 os.path.basename(html_report_path) ==
221 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
222 self._table_entries.append(table_entry)
223
224 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35225 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12226 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
227
228 def _CreateTableEntryFromCoverageSummary(self,
229 summary,
230 href=None,
231 name=None,
232 is_dir=None):
233 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37234 assert (href is None and name is None and is_dir is None) or (
235 href is not None and name is not None and is_dir is not None), (
236 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35237 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37238 'attributes must be None.')
239
Yuke Liaod54030e2018-01-08 17:34:12240 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37241 if href is not None:
242 entry['href'] = href
243 if name is not None:
244 entry['name'] = name
245 if is_dir is not None:
246 entry['is_dir'] = is_dir
247
Yuke Liaoea228d02018-01-05 19:10:33248 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12249 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37250 if summary_dict[feature]['total'] == 0:
251 percentage = 0.0
252 else:
Yuke Liao0e4c8682018-04-18 21:06:59253 percentage = float(summary_dict[feature]
254 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35255
Yuke Liaoea228d02018-01-05 19:10:33256 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12257 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33258 'total': summary_dict[feature]['total'],
259 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35260 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33261 'color_class': color_class
262 }
Yuke Liaod54030e2018-01-08 17:34:12263
Yuke Liaod54030e2018-01-08 17:34:12264 return entry
Yuke Liaoea228d02018-01-05 19:10:33265
266 def _GetColorClass(self, percentage):
267 """Returns the css color class based on coverage percentage."""
268 if percentage >= 0 and percentage < 80:
269 return 'red'
270 if percentage >= 80 and percentage < 100:
271 return 'yellow'
272 if percentage == 100:
273 return 'green'
274
275 assert False, 'Invalid coverage percentage: "%d"' % percentage
276
Yuke Liaodd1ec0592018-02-02 01:26:37277 def WriteHtmlCoverageReport(self):
278 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33279
280 In the report, sub-directories are displayed before files and within each
281 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33282 """
283
284 def EntryCmp(left, right):
285 """Compare function for table entries."""
286 if left['is_dir'] != right['is_dir']:
287 return -1 if left['is_dir'] == True else 1
288
Yuke Liaodd1ec0592018-02-02 01:26:37289 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33290
291 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
292
293 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37294 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
295 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
296 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
297
Yuke Liaoea228d02018-01-05 19:10:33298 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37299 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
300 directory_view_href=_GetRelativePathToDirectoryOfFile(
301 directory_view_path, self._output_path),
302 component_view_href=_GetRelativePathToDirectoryOfFile(
303 component_view_path, self._output_path),
304 file_view_href=_GetRelativePathToDirectoryOfFile(
305 file_view_path, self._output_path))
306
Yuke Liaod54030e2018-01-08 17:34:12307 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37308 entries=self._table_entries,
309 total_entry=self._total_entry,
310 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33311 html_footer = self._footer_template.render()
312
Yuke Liaodd1ec0592018-02-02 01:26:37313 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33314 html_file.write(html_header + html_table + html_footer)
315
Yuke Liao506e8822017-12-04 16:52:54316
Max Morozd73e45f2018-04-24 18:32:47317def _GetSharedLibraries(binary_paths):
318 """Returns set of shared libraries used by specified binaries."""
319 libraries = set()
320 cmd = []
321 shared_library_re = None
322
323 if sys.platform.startswith('linux'):
324 cmd.extend(['ldd'])
325 shared_library_re = re.compile(
326 r'.*\.so\s=>\s(.*' + BUILD_DIR + '.*\.so)\s.*')
327 elif sys.platform.startswith('darwin'):
328 cmd.extend(['otool', '-L'])
329 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
330 else:
331 assert False, ('Cannot detect shared libraries used by the given targets.')
332
333 assert shared_library_re is not None
334
335 cmd.extend(binary_paths)
336 output = subprocess.check_output(cmd)
337
338 for line in output.splitlines():
339 m = shared_library_re.match(line)
340 if not m:
341 continue
342
343 shared_library_path = m.group(1)
344 if sys.platform.startswith('darwin'):
345 # otool outputs "@rpath" macro instead of the dirname of the given binary.
346 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
347
348 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
349 'the given target(s) does not '
350 'exist.' % shared_library_path)
351 with open(shared_library_path) as f:
352 data = f.read()
353
354 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
355 if '__llvm_cov' in data:
356 libraries.add(shared_library_path)
357
358 return list(libraries)
359
360
Yuke Liaoc60b2d02018-03-02 21:40:43361def _GetHostPlatform():
362 """Returns the host platform.
363
364 This is separate from the target platform/os that coverage is running for.
365 """
Abhishek Arya1ec832c2017-12-05 18:06:59366 if sys.platform == 'win32' or sys.platform == 'cygwin':
367 return 'win'
368 if sys.platform.startswith('linux'):
369 return 'linux'
370 else:
371 assert sys.platform == 'darwin'
372 return 'mac'
373
374
Yuke Liaoc60b2d02018-03-02 21:40:43375def _GetTargetOS():
376 """Returns the target os specified in args.gn file.
377
378 Returns an empty string is target_os is not specified.
379 """
Yuke Liao80afff32018-03-07 01:26:20380 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43381 return build_args['target_os'] if 'target_os' in build_args else ''
382
383
Yuke Liaob2926832018-03-02 17:34:29384def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10385 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43386 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10387
388
Yuke Liao506e8822017-12-04 16:52:54389# TODO(crbug.com/759794): remove this function once tools get included to
390# Clang bundle:
391# https://chromium-review.googlesource.com/c/chromium/src/+/688221
392def DownloadCoverageToolsIfNeeded():
393 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59394
Yuke Liaoc60b2d02018-03-02 21:40:43395 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54396 """Returns a pair of revision number by reading the build stamp file.
397
398 Args:
399 stamp_file_path: A path the build stamp file created by
400 tools/clang/scripts/update.py.
401 Returns:
402 A pair of integers represeting the main and sub revision respectively.
403 """
404 if not os.path.exists(stamp_file_path):
405 return 0, 0
406
407 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43408 stamp_file_line = stamp_file.readline()
409 if ',' in stamp_file_line:
410 package_version = stamp_file_line.rstrip().split(',')[0]
411 else:
412 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54413
Yuke Liaoc60b2d02018-03-02 21:40:43414 clang_revision_str, clang_sub_revision_str = package_version.split('-')
415 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59416
Yuke Liaoc60b2d02018-03-02 21:40:43417 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54418 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43419 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54420
421 coverage_revision_stamp_file = os.path.join(
422 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
423 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43424 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54425
Yuke Liaoea228d02018-01-05 19:10:33426 has_coverage_tools = (
427 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32428
Yuke Liaoea228d02018-01-05 19:10:33429 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54430 coverage_sub_revision == clang_sub_revision):
431 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43432 return
Yuke Liao506e8822017-12-04 16:52:54433
434 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
435 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
436
437 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43438 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54439 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43440 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54441 coverage_tools_url = (
442 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43443 else:
444 assert host_platform == 'win'
445 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54446
447 try:
448 clang_update.DownloadAndUnpack(coverage_tools_url,
449 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10450 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54451 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43452 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54453 file_handle.write('\n')
454 except urllib2.URLError:
455 raise Exception(
456 'Failed to download coverage tools: %s.' % coverage_tools_url)
457
458
Yuke Liaodd1ec0592018-02-02 01:26:37459def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59460 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54461 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
462
463 For a file with absolute path /a/b/x.cc, a html report is generated as:
464 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
465 OUTPUT_DIR/index.html.
466
467 Args:
468 binary_paths: A list of paths to the instrumented binaries.
469 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42470 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54471 """
Yuke Liao506e8822017-12-04 16:52:54472 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
473 # [[-object BIN]] [SOURCES]
474 # NOTE: For object files, the first one is specified as a positional argument,
475 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10476 logging.debug('Generating per file line by line coverage reports using '
477 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59478 subprocess_cmd = [
479 LLVM_COV_PATH, 'show', '-format=html',
480 '-output-dir={}'.format(OUTPUT_DIR),
481 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
482 ]
483 subprocess_cmd.extend(
484 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29485 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42486 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59487 if ignore_filename_regex:
488 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
489
Yuke Liao506e8822017-12-04 16:52:54490 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34491
492 # llvm-cov creates "coverage" subdir in the output dir. We would like to use
493 # the platform name instead, as it simplifies the report dir structure when
494 # the same report is generated for different platforms.
495 default_report_subdir_path = os.path.join(OUTPUT_DIR, 'coverage')
496 platform_report_subdir_path = os.path.join(OUTPUT_DIR, _GetHostPlatform())
497 if os.path.exists(platform_report_subdir_path):
498 shutil.rmtree(platform_report_subdir_path)
499 os.rename(default_report_subdir_path, platform_report_subdir_path)
500
Yuke Liao481d3482018-01-29 19:17:10501 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54502
503
Yuke Liaodd1ec0592018-02-02 01:26:37504def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
505 """Generates html index file for file view."""
506 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
507 logging.debug('Generating file view html index file as: "%s".',
508 file_view_index_file_path)
509 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
510 'Path')
511 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33512
Yuke Liaodd1ec0592018-02-02 01:26:37513 for file_path in per_file_coverage_summary:
514 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
515
516 html_generator.AddLinkToAnotherReport(
517 _GetCoverageHtmlReportPathForFile(file_path),
518 os.path.relpath(file_path, SRC_ROOT_PATH),
519 per_file_coverage_summary[file_path])
520
521 html_generator.CreateTotalsEntry(totals_coverage_summary)
522 html_generator.WriteHtmlCoverageReport()
523 logging.debug('Finished generating file view html index file.')
524
525
526def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
527 """Calculates per directory coverage summary."""
528 logging.debug('Calculating per-directory coverage summary')
529 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
530
Yuke Liaoea228d02018-01-05 19:10:33531 for file_path in per_file_coverage_summary:
532 summary = per_file_coverage_summary[file_path]
533 parent_dir = os.path.dirname(file_path)
534 while True:
535 per_directory_coverage_summary[parent_dir].AddSummary(summary)
536
537 if parent_dir == SRC_ROOT_PATH:
538 break
539 parent_dir = os.path.dirname(parent_dir)
540
Yuke Liaodd1ec0592018-02-02 01:26:37541 logging.debug('Finished calculating per-directory coverage summary')
542 return per_directory_coverage_summary
543
544
545def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
546 per_file_coverage_summary):
547 """Generates per directory coverage breakdown in html."""
548 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33549 for dir_path in per_directory_coverage_summary:
550 _GenerateCoverageInHtmlForDirectory(
551 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
552
Yuke Liaodd1ec0592018-02-02 01:26:37553 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10554
Yuke Liaoea228d02018-01-05 19:10:33555
556def _GenerateCoverageInHtmlForDirectory(
557 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
558 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37559 html_generator = _CoverageReportHtmlGenerator(
560 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33561
562 for entry_name in os.listdir(dir_path):
563 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33564
Yuke Liaodd1ec0592018-02-02 01:26:37565 if entry_path in per_file_coverage_summary:
566 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
567 entry_coverage_summary = per_file_coverage_summary[entry_path]
568 elif entry_path in per_directory_coverage_summary:
569 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
570 entry_path)
571 entry_coverage_summary = per_directory_coverage_summary[entry_path]
572 else:
Yuke Liaoc7e607142018-02-05 20:26:14573 # Any file without executable lines shouldn't be included into the report.
574 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37575 continue
Yuke Liaoea228d02018-01-05 19:10:33576
Yuke Liaodd1ec0592018-02-02 01:26:37577 html_generator.AddLinkToAnotherReport(entry_html_report_path,
578 os.path.basename(entry_path),
579 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33580
Yuke Liaod54030e2018-01-08 17:34:12581 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37582 html_generator.WriteHtmlCoverageReport()
583
584
585def _GenerateDirectoryViewHtmlIndexFile():
586 """Generates the html index file for directory view.
587
588 Note that the index file is already generated under SRC_ROOT_PATH, so this
589 file simply redirects to it, and the reason of this extra layer is for
590 structural consistency with other views.
591 """
592 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
593 DIRECTORY_VIEW_INDEX_FILE)
594 logging.debug('Generating directory view html index file as: "%s".',
595 directory_view_index_file_path)
596 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
597 SRC_ROOT_PATH)
598 _WriteRedirectHtmlFile(directory_view_index_file_path,
599 src_root_html_report_path)
600 logging.debug('Finished generating directory view html index file.')
601
602
603def _CalculatePerComponentCoverageSummary(component_to_directories,
604 per_directory_coverage_summary):
605 """Calculates per component coverage summary."""
606 logging.debug('Calculating per-component coverage summary')
607 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
608
609 for component in component_to_directories:
610 for directory in component_to_directories[component]:
611 absolute_directory_path = os.path.abspath(directory)
612 if absolute_directory_path in per_directory_coverage_summary:
613 per_component_coverage_summary[component].AddSummary(
614 per_directory_coverage_summary[absolute_directory_path])
615
616 logging.debug('Finished calculating per-component coverage summary')
617 return per_component_coverage_summary
618
619
620def _ExtractComponentToDirectoriesMapping():
621 """Returns a mapping from components to directories."""
622 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
623 directory_to_component = component_mappings['dir-to-component']
624
625 component_to_directories = defaultdict(list)
626 for directory in directory_to_component:
627 component = directory_to_component[directory]
628 component_to_directories[component].append(directory)
629
630 return component_to_directories
631
632
633def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
634 component_to_directories,
635 per_directory_coverage_summary):
636 """Generates per-component coverage reports in html."""
637 logging.debug('Writing per-component coverage html reports.')
638 for component in per_component_coverage_summary:
639 _GenerateCoverageInHtmlForComponent(
640 component, per_component_coverage_summary, component_to_directories,
641 per_directory_coverage_summary)
642
643 logging.debug('Finished writing per-component coverage html reports.')
644
645
646def _GenerateCoverageInHtmlForComponent(
647 component_name, per_component_coverage_summary, component_to_directories,
648 per_directory_coverage_summary):
649 """Generates coverage html report for a component."""
650 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
651 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14652 component_html_report_dir = os.path.dirname(component_html_report_path)
653 if not os.path.exists(component_html_report_dir):
654 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37655
656 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
657 'Path')
658
659 for dir_path in component_to_directories[component_name]:
660 dir_absolute_path = os.path.abspath(dir_path)
661 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14662 # Any directory without an excercised file shouldn't be included into the
663 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37664 continue
665
666 html_generator.AddLinkToAnotherReport(
667 _GetCoverageHtmlReportPathForDirectory(dir_path),
668 os.path.relpath(dir_path, SRC_ROOT_PATH),
669 per_directory_coverage_summary[dir_absolute_path])
670
671 html_generator.CreateTotalsEntry(
672 per_component_coverage_summary[component_name])
673 html_generator.WriteHtmlCoverageReport()
674
675
676def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
677 """Generates the html index file for component view."""
678 component_view_index_file_path = os.path.join(OUTPUT_DIR,
679 COMPONENT_VIEW_INDEX_FILE)
680 logging.debug('Generating component view html index file as: "%s".',
681 component_view_index_file_path)
682 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
683 'Component')
684 totals_coverage_summary = _CoverageSummary()
685
686 for component in per_component_coverage_summary:
687 totals_coverage_summary.AddSummary(
688 per_component_coverage_summary[component])
689
690 html_generator.AddLinkToAnotherReport(
691 _GetCoverageHtmlReportPathForComponent(component), component,
692 per_component_coverage_summary[component])
693
694 html_generator.CreateTotalsEntry(totals_coverage_summary)
695 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14696 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33697
698
699def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37700 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33701 html_index_file_path = os.path.join(OUTPUT_DIR,
702 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37703 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
704 DIRECTORY_VIEW_INDEX_FILE)
705 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
706
707
708def _WriteRedirectHtmlFile(from_html_path, to_html_path):
709 """Writes a html file that redirects to another html file."""
710 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
711 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33712 content = ("""
713 <!DOCTYPE html>
714 <html>
715 <head>
716 <!-- HTML meta refresh URL redirection -->
717 <meta http-equiv="refresh" content="0; url=%s">
718 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37719 </html>""" % to_html_relative_path)
720 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33721 f.write(content)
722
723
Yuke Liaodd1ec0592018-02-02 01:26:37724def _GetCoverageHtmlReportPathForFile(file_path):
725 """Given a file path, returns the corresponding html report path."""
726 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
727 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
728
729 # '+' is used instead of os.path.join because both of them are absolute paths
730 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14731 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37732 return _GetCoverageReportRootDirPath() + html_report_path
733
734
735def _GetCoverageHtmlReportPathForDirectory(dir_path):
736 """Given a directory path, returns the corresponding html report path."""
737 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
738 html_report_path = os.path.join(
739 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
740
741 # '+' is used instead of os.path.join because both of them are absolute paths
742 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14743 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37744 return _GetCoverageReportRootDirPath() + html_report_path
745
746
747def _GetCoverageHtmlReportPathForComponent(component_name):
748 """Given a component, returns the corresponding html report path."""
749 component_file_name = component_name.lower().replace('>', '-')
750 html_report_name = os.extsep.join([component_file_name, 'html'])
751 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
752 html_report_name)
753
754
755def _GetCoverageReportRootDirPath():
756 """The root directory that contains all generated coverage html reports."""
Max Moroz025d8952018-05-03 16:33:34757 return os.path.join(os.path.abspath(OUTPUT_DIR), _GetHostPlatform())
Yuke Liaoea228d02018-01-05 19:10:33758
759
Yuke Liao506e8822017-12-04 16:52:54760def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
761 """Builds and runs target to generate the coverage profile data.
762
763 Args:
764 targets: A list of targets to build with coverage instrumentation.
765 commands: A list of commands used to run the targets.
766 jobs_count: Number of jobs to run in parallel for building. If None, a
767 default value is derived based on CPUs availability.
768
769 Returns:
770 A relative path to the generated profdata file.
771 """
772 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59773 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
774 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54775 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
776 profraw_file_paths)
777
Yuke Liaod4a9865202018-01-12 23:17:52778 for profraw_file_path in profraw_file_paths:
779 os.remove(profraw_file_path)
780
Yuke Liao506e8822017-12-04 16:52:54781 return profdata_file_path
782
783
784def _BuildTargets(targets, jobs_count):
785 """Builds target with Clang coverage instrumentation.
786
787 This function requires current working directory to be the root of checkout.
788
789 Args:
790 targets: A list of targets to build with coverage instrumentation.
791 jobs_count: Number of jobs to run in parallel for compilation. If None, a
792 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54793 """
Abhishek Arya1ec832c2017-12-05 18:06:59794
Yuke Liao506e8822017-12-04 16:52:54795 def _IsGomaConfigured():
796 """Returns True if goma is enabled in the gn build args.
797
798 Returns:
799 A boolean indicates whether goma is configured for building or not.
800 """
Yuke Liao80afff32018-03-07 01:26:20801 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54802 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
803
Yuke Liao481d3482018-01-29 19:17:10804 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54805 if jobs_count is None and _IsGomaConfigured():
806 jobs_count = DEFAULT_GOMA_JOBS
807
808 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
809 if jobs_count is not None:
810 subprocess_cmd.append('-j' + str(jobs_count))
811
812 subprocess_cmd.extend(targets)
813 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10814 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54815
816
817def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
818 """Runs commands and returns the relative paths to the profraw data files.
819
820 Args:
821 targets: A list of targets built with coverage instrumentation.
822 commands: A list of commands used to run the targets.
823
824 Returns:
825 A list of relative paths to the generated profraw data files.
826 """
Yuke Liao481d3482018-01-29 19:17:10827 logging.debug('Executing the test commands')
828
Yuke Liao506e8822017-12-04 16:52:54829 # Remove existing profraw data files.
830 for file_or_dir in os.listdir(OUTPUT_DIR):
831 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
832 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
833
Yuke Liaoa0c8c2f2018-02-28 20:14:10834 profraw_file_paths = []
835
Yuke Liaod4a9865202018-01-12 23:17:52836 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54837 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10838 output_file_name = os.extsep.join([target + '_output', 'txt'])
839 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
840 logging.info('Running command: "%s", the output is redirected to "%s"',
841 command, output_file_path)
842
Yuke Liaob2926832018-03-02 17:34:29843 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10844 # On iOS platform, due to lack of write permissions, profraw files are
845 # generated outside of the OUTPUT_DIR, and the exact paths are contained
846 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29847 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10848 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
849 else:
850 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
851 output = _ExecuteCommand(target, command)
852
853 with open(output_file_path, 'w') as output_file:
854 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54855
Yuke Liao481d3482018-01-29 19:17:10856 logging.debug('Finished executing the test commands')
857
Yuke Liaob2926832018-03-02 17:34:29858 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10859 return profraw_file_paths
860
Yuke Liao506e8822017-12-04 16:52:54861 for file_or_dir in os.listdir(OUTPUT_DIR):
862 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
863 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
864
865 # Assert one target/command generates at least one profraw data file.
866 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59867 assert any(
868 os.path.basename(profraw_file).startswith(target)
869 for profraw_file in profraw_file_paths), (
870 'Running target: %s failed to generate any profraw data file, '
871 'please make sure the binary exists and is properly instrumented.' %
872 target)
Yuke Liao506e8822017-12-04 16:52:54873
874 return profraw_file_paths
875
876
877def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10878 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52879 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01880 #
Max Morozd73e45f2018-04-24 18:32:47881 # "%p" expands out to the process ID. It's not used by this scripts due to:
882 # 1) If a target program spawns too many processess, it may exhaust all disk
883 # space available. For example, unit_tests writes thousands of .profraw
884 # files each of size 1GB+.
885 # 2) If a target binary uses shared libraries, coverage profile data for them
886 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01887 #
Yuke Liaod4a9865202018-01-12 23:17:52888 # "%Nm" expands out to the instrumented binary's signature. When this pattern
889 # is specified, the runtime creates a pool of N raw profiles which are used
890 # for on-line profile merging. The runtime takes care of selecting a raw
891 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52892 # N must be between 1 and 9. The merge pool specifier can only occur once per
893 # filename pattern.
894 #
Max Morozd73e45f2018-04-24 18:32:47895 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01896 #
Max Morozd73e45f2018-04-24 18:32:47897 # For other cases, "%4m" is chosen as it creates some level of parallelism,
898 # but it's not too big to consume too much computing resource or disk space.
899 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59900 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01901 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54902 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
903 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54904
Yuke Liaoa0c8c2f2018-02-28 20:14:10905 try:
906 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29907 shlex.split(command),
908 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10909 except subprocess.CalledProcessError as e:
910 output = e.output
911 logging.warning('Command: "%s" exited with non-zero return code', command)
912
913 return output
914
915
Yuke Liao27349c92018-03-22 21:10:01916def _IsFuzzerTarget(target):
917 """Returns true if the target is a fuzzer target."""
918 build_args = _GetBuildArgs()
919 use_libfuzzer = ('use_libfuzzer' in build_args and
920 build_args['use_libfuzzer'] == 'true')
921 return use_libfuzzer and target.endswith('_fuzzer')
922
923
Yuke Liaob2926832018-03-02 17:34:29924def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10925 """Runs a single iOS command and generates a profraw data file.
926
927 iOS application doesn't have write access to folders outside of the app, so
928 it's impossible to instruct the app to flush the profraw data file to the
929 desired location. The profraw data file will be generated somewhere within the
930 application's Documents folder, and the full path can be obtained by parsing
931 the output.
932 """
Yuke Liaob2926832018-03-02 17:34:29933 assert _IsIOSCommand(command)
934
935 # After running tests, iossim generates a profraw data file, it won't be
936 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
937 # checkout.
938 iossim_profraw_file_path = os.path.join(
939 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10940
941 try:
Yuke Liaob2926832018-03-02 17:34:29942 output = subprocess.check_output(
943 shlex.split(command),
944 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10945 except subprocess.CalledProcessError as e:
946 # iossim emits non-zero return code even if tests run successfully, so
947 # ignore the return code.
948 output = e.output
949
950 return output
951
952
953def _GetProfrawDataFileByParsingOutput(output):
954 """Returns the path to the profraw data file obtained by parsing the output.
955
956 The output of running the test target has no format, but it is guaranteed to
957 have a single line containing the path to the generated profraw data file.
958 NOTE: This should only be called when target os is iOS.
959 """
Yuke Liaob2926832018-03-02 17:34:29960 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10961
Yuke Liaob2926832018-03-02 17:34:29962 output_by_lines = ''.join(output).splitlines()
963 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10964
965 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29966 result = profraw_file_pattern.match(line)
967 if result:
968 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10969
970 assert False, ('No profraw data file was generated, did you call '
971 'coverage_util::ConfigureCoverageReportPath() in test setup? '
972 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54973
974
975def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
976 """Returns a relative path to the profdata file by merging profraw data files.
977
978 Args:
979 profraw_file_paths: A list of relative paths to the profraw data files that
980 are to be merged.
981
982 Returns:
983 A relative path to the generated profdata file.
984
985 Raises:
986 CalledProcessError: An error occurred merging profraw data files.
987 """
Yuke Liao481d3482018-01-29 19:17:10988 logging.info('Creating the coverage profile data file')
989 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54990 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
991 try:
Abhishek Arya1ec832c2017-12-05 18:06:59992 subprocess_cmd = [
993 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
994 ]
Yuke Liao506e8822017-12-04 16:52:54995 subprocess_cmd.extend(profraw_file_paths)
996 subprocess.check_call(subprocess_cmd)
997 except subprocess.CalledProcessError as error:
998 print('Failed to merge profraw files to create profdata file')
999 raise error
1000
Yuke Liao481d3482018-01-29 19:17:101001 logging.debug('Finished merging profraw files')
1002 logging.info('Code coverage profile data is created as: %s',
1003 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:541004 return profdata_file_path
1005
1006
Yuke Liao0e4c8682018-04-18 21:06:591007def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
1008 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:331009 """Generates per file coverage summary using "llvm-cov export" command."""
1010 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1011 # [[-object BIN]] [SOURCES].
1012 # NOTE: For object files, the first one is specified as a positional argument,
1013 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101014 logging.debug('Generating per-file code coverage summary using "llvm-cov '
1015 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:331016 subprocess_cmd = [
1017 LLVM_COV_PATH, 'export', '-summary-only',
1018 '-instr-profile=' + profdata_file_path, binary_paths[0]
1019 ]
1020 subprocess_cmd.extend(
1021 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291022 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331023 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591024 if ignore_filename_regex:
1025 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331026
1027 json_output = json.loads(subprocess.check_output(subprocess_cmd))
1028 assert len(json_output['data']) == 1
1029 files_coverage_data = json_output['data'][0]['files']
1030
1031 per_file_coverage_summary = {}
1032 for file_coverage_data in files_coverage_data:
1033 file_path = file_coverage_data['filename']
1034 summary = file_coverage_data['summary']
1035
Yuke Liaoea228d02018-01-05 19:10:331036 if summary['lines']['count'] == 0:
1037 continue
1038
1039 per_file_coverage_summary[file_path] = _CoverageSummary(
1040 regions_total=summary['regions']['count'],
1041 regions_covered=summary['regions']['covered'],
1042 functions_total=summary['functions']['count'],
1043 functions_covered=summary['functions']['covered'],
1044 lines_total=summary['lines']['count'],
1045 lines_covered=summary['lines']['covered'])
1046
Yuke Liao481d3482018-01-29 19:17:101047 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:331048 return per_file_coverage_summary
1049
1050
Yuke Liaob2926832018-03-02 17:34:291051def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1052 """Appends -arch arguments to the command list if it's ios platform.
1053
1054 iOS binaries are universal binaries, and require specifying the architecture
1055 to use, and one architecture needs to be specified for each binary.
1056 """
1057 if _IsIOS():
1058 cmd_list.extend(['-arch=x86_64'] * num_archs)
1059
1060
Yuke Liao506e8822017-12-04 16:52:541061def _GetBinaryPath(command):
1062 """Returns a relative path to the binary to be run by the command.
1063
Yuke Liao545db322018-02-15 17:12:011064 Currently, following types of commands are supported (e.g. url_unittests):
1065 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1066 2. Use xvfb.
1067 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1068 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371069 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1070 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101071 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371072 <iossim_arguments> -c <app_arguments>
1073 out/Coverage-iphonesimulator/url_unittests.app"
1074
Yuke Liao545db322018-02-15 17:12:011075
Yuke Liao506e8822017-12-04 16:52:541076 Args:
1077 command: A command used to run a target.
1078
1079 Returns:
1080 A relative path to the binary.
1081 """
Yuke Liao545db322018-02-15 17:12:011082 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1083
Yuke Liaob2926832018-03-02 17:34:291084 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011085 if os.path.basename(command_parts[0]) == 'python':
1086 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1087 'This tool doesn\'t understand the command: "%s"' % command)
1088 return command_parts[2]
1089
1090 if os.path.basename(command_parts[0]) == xvfb_script_name:
1091 return command_parts[1]
1092
Yuke Liaob2926832018-03-02 17:34:291093 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101094 # For a given application bundle, the binary resides in the bundle and has
1095 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371096 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101097 app_name = os.path.splitext(os.path.basename(app_path))[0]
1098 return os.path.join(app_path, app_name)
1099
Yuke Liaob2926832018-03-02 17:34:291100 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541101
1102
Yuke Liaob2926832018-03-02 17:34:291103def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101104 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291105 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101106
1107
Yuke Liao95d13d72017-12-07 18:18:501108def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1109 """Verifies that the target executables specified in the commands are inside
1110 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541111 for command in commands:
1112 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501113 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1114 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1115 'Target executable "%s" in command: "%s" is outside of '
1116 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541117
1118
1119def _ValidateBuildingWithClangCoverage():
1120 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201121 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541122
1123 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1124 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591125 assert False, ('\'{} = true\' is required in args.gn.'
1126 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541127
1128
Yuke Liaoc60b2d02018-03-02 21:40:431129def _ValidateCurrentPlatformIsSupported():
1130 """Asserts that this script suports running on the current platform"""
1131 target_os = _GetTargetOS()
1132 if target_os:
1133 current_platform = target_os
1134 else:
1135 current_platform = _GetHostPlatform()
1136
1137 assert current_platform in [
1138 'linux', 'mac', 'chromeos', 'ios'
1139 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1140
1141
Yuke Liao80afff32018-03-07 01:26:201142def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541143 """Parses args.gn file and returns results as a dictionary.
1144
1145 Returns:
1146 A dictionary representing the build args.
1147 """
Yuke Liao80afff32018-03-07 01:26:201148 global _BUILD_ARGS
1149 if _BUILD_ARGS is not None:
1150 return _BUILD_ARGS
1151
1152 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541153 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1154 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1155 'missing args.gn file.' % BUILD_DIR)
1156 with open(build_args_path) as build_args_file:
1157 build_args_lines = build_args_file.readlines()
1158
Yuke Liao506e8822017-12-04 16:52:541159 for build_arg_line in build_args_lines:
1160 build_arg_without_comments = build_arg_line.split('#')[0]
1161 key_value_pair = build_arg_without_comments.split('=')
1162 if len(key_value_pair) != 2:
1163 continue
1164
1165 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431166
1167 # Values are wrapped within a pair of double-quotes, so remove the leading
1168 # and trailing double-quotes.
1169 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201170 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541171
Yuke Liao80afff32018-03-07 01:26:201172 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541173
1174
Abhishek Arya16f059a2017-12-07 17:47:321175def _VerifyPathsAndReturnAbsolutes(paths):
1176 """Verifies that the paths specified in |paths| exist and returns absolute
1177 versions.
Yuke Liao66da1732017-12-05 22:19:421178
1179 Args:
1180 paths: A list of files or directories.
1181 """
Abhishek Arya16f059a2017-12-07 17:47:321182 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421183 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321184 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1185 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1186
1187 absolute_paths.append(absolute_path)
1188
1189 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421190
1191
Yuke Liaodd1ec0592018-02-02 01:26:371192def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1193 """Returns a target path relative to the directory of base_path.
1194
1195 This method requires base_path to be a file, otherwise, one should call
1196 os.path.relpath directly.
1197 """
1198 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141199 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371200 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141201 base_dir = os.path.dirname(base_path)
1202 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371203
1204
Yuke Liao506e8822017-12-04 16:52:541205def _ParseCommandArguments():
1206 """Adds and parses relevant arguments for tool comands.
1207
1208 Returns:
1209 A dictionary representing the arguments.
1210 """
1211 arg_parser = argparse.ArgumentParser()
1212 arg_parser.usage = __doc__
1213
Abhishek Arya1ec832c2017-12-05 18:06:591214 arg_parser.add_argument(
1215 '-b',
1216 '--build-dir',
1217 type=str,
1218 required=True,
1219 help='The build directory, the path needs to be relative to the root of '
1220 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541221
Abhishek Arya1ec832c2017-12-05 18:06:591222 arg_parser.add_argument(
1223 '-o',
1224 '--output-dir',
1225 type=str,
1226 required=True,
1227 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541228
Abhishek Arya1ec832c2017-12-05 18:06:591229 arg_parser.add_argument(
1230 '-c',
1231 '--command',
1232 action='append',
1233 required=True,
1234 help='Commands used to run test targets, one test target needs one and '
1235 'only one command, when specifying commands, one should assume the '
1236 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541237
Abhishek Arya1ec832c2017-12-05 18:06:591238 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421239 '-f',
1240 '--filters',
1241 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321242 required=False,
Yuke Liao66da1732017-12-05 22:19:421243 help='Directories or files to get code coverage for, and all files under '
1244 'the directories are included recursively.')
1245
1246 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591247 '-i',
1248 '--ignore-filename-regex',
1249 type=str,
1250 help='Skip source code files with file paths that match the given '
1251 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1252 'to exclude files in third_party/ and out/ folders from the report.')
1253
1254 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591255 '-j',
1256 '--jobs',
1257 type=int,
1258 default=None,
1259 help='Run N jobs to build in parallel. If not specified, a default value '
1260 'will be derived based on CPUs availability. Please refer to '
1261 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541262
Abhishek Arya1ec832c2017-12-05 18:06:591263 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101264 '-v',
1265 '--verbose',
1266 action='store_true',
1267 help='Prints additional output for diagnostics.')
1268
1269 arg_parser.add_argument(
1270 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1271
1272 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591273 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541274
1275 args = arg_parser.parse_args()
1276 return args
1277
1278
1279def Main():
1280 """Execute tool commands."""
1281 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1282 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591283 'of checkout.')
Abhishek Arya8a0751a2018-05-03 18:53:111284
1285 # This helps to setup coverage binaries even when script is called with
1286 # empty params. This is used by coverage bot for initial setup.
1287 DownloadCoverageToolsIfNeeded()
1288
Yuke Liao506e8822017-12-04 16:52:541289 args = _ParseCommandArguments()
1290 global BUILD_DIR
1291 BUILD_DIR = args.build_dir
1292 global OUTPUT_DIR
1293 OUTPUT_DIR = args.output_dir
1294
1295 assert len(args.targets) == len(args.command), ('Number of targets must be '
1296 'equal to the number of test '
1297 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431298
1299 # logging should be configured before it is used.
1300 log_level = logging.DEBUG if args.verbose else logging.INFO
1301 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1302 log_file = args.log_file if args.log_file else None
1303 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1304
Abhishek Arya1ec832c2017-12-05 18:06:591305 assert os.path.exists(BUILD_DIR), (
1306 'Build directory: {} doesn\'t exist. '
1307 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431308 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541309 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501310 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321311
1312 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421313 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321314 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421315
Yuke Liao506e8822017-12-04 16:52:541316 if not os.path.exists(OUTPUT_DIR):
1317 os.makedirs(OUTPUT_DIR)
1318
Abhishek Arya1ec832c2017-12-05 18:06:591319 profdata_file_path = _CreateCoverageProfileDataForTargets(
1320 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541321 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331322
Yuke Liao481d3482018-01-29 19:17:101323 logging.info('Generating code coverage report in html (this can take a while '
1324 'depending on size of target!)')
Max Morozd73e45f2018-04-24 18:32:471325 binary_paths.extend(_GetSharedLibraries(binary_paths))
Yuke Liaodd1ec0592018-02-02 01:26:371326 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591327 binary_paths, profdata_file_path, absolute_filter_paths,
1328 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371329 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591330 absolute_filter_paths,
1331 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371332 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1333
1334 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1335 per_file_coverage_summary)
1336 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1337 per_file_coverage_summary)
1338 _GenerateDirectoryViewHtmlIndexFile()
1339
1340 component_to_directories = _ExtractComponentToDirectoriesMapping()
1341 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1342 component_to_directories, per_directory_coverage_summary)
1343 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1344 component_to_directories,
1345 per_directory_coverage_summary)
1346 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331347
1348 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371349 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331350 _OverwriteHtmlReportsIndexFile()
1351
Yuke Liao506e8822017-12-04 16:52:541352 html_index_file_path = 'file://' + os.path.abspath(
1353 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101354 logging.info('Index file for html report is generated as: %s',
1355 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541356
Abhishek Arya1ec832c2017-12-05 18:06:591357
Yuke Liao506e8822017-12-04 16:52:541358if __name__ == '__main__':
1359 sys.exit(Main())