blob: 6d8b6e4d6fd58c12bebbc447ebc69b6f19c93a07 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3856
57 For an overview of how code coverage works in Chromium, please refer to
58 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5459"""
60
61from __future__ import print_function
62
63import sys
64
65import argparse
Yuke Liaoea228d02018-01-05 19:10:3366import json
Yuke Liao481d3482018-01-29 19:17:1067import logging
Yuke Liao506e8822017-12-04 16:52:5468import os
Yuke Liaob2926832018-03-02 17:34:2969import re
70import shlex
Yuke Liao506e8822017-12-04 16:52:5471import subprocess
Yuke Liao506e8822017-12-04 16:52:5472import urllib2
73
Abhishek Arya1ec832c2017-12-05 18:06:5974sys.path.append(
75 os.path.join(
76 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
77 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5478import update as clang_update
79
Yuke Liaoea228d02018-01-05 19:10:3380sys.path.append(
81 os.path.join(
82 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
83 'third_party'))
84import jinja2
85from collections import defaultdict
86
Yuke Liao506e8822017-12-04 16:52:5487# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5988SRC_ROOT_PATH = os.path.abspath(
89 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5490
91# Absolute path to the code coverage tools binary.
92LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
93LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
94LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
95
96# Build directory, the value is parsed from command line arguments.
97BUILD_DIR = None
98
99# Output directory for generated artifacts, the value is parsed from command
100# line arguemnts.
101OUTPUT_DIR = None
102
103# Default number of jobs used to build when goma is configured and enabled.
104DEFAULT_GOMA_JOBS = 100
105
106# Name of the file extension for profraw data files.
107PROFRAW_FILE_EXTENSION = 'profraw'
108
109# Name of the final profdata file, and this file needs to be passed to
110# "llvm-cov" command in order to call "llvm-cov show" to inspect the
111# line-by-line coverage of specific files.
112PROFDATA_FILE_NAME = 'coverage.profdata'
113
114# Build arg required for generating code coverage data.
115CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
116
Yuke Liaoea228d02018-01-05 19:10:33117# The default name of the html coverage report for a directory.
118DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
119
Yuke Liaodd1ec0592018-02-02 01:26:37120# Name of the html index files for different views.
121DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
122COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
123FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
124
125# Used to extract a mapping between directories and components.
126COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
127
Yuke Liao80afff32018-03-07 01:26:20128# Caches the results returned by _GetBuildArgs, don't use this variable
129# directly, call _GetBuildArgs instead.
130_BUILD_ARGS = None
131
Yuke Liaoea228d02018-01-05 19:10:33132
133class _CoverageSummary(object):
134 """Encapsulates coverage summary representation."""
135
Yuke Liaodd1ec0592018-02-02 01:26:37136 def __init__(self,
137 regions_total=0,
138 regions_covered=0,
139 functions_total=0,
140 functions_covered=0,
141 lines_total=0,
142 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33143 """Initializes _CoverageSummary object."""
144 self._summary = {
145 'regions': {
146 'total': regions_total,
147 'covered': regions_covered
148 },
149 'functions': {
150 'total': functions_total,
151 'covered': functions_covered
152 },
153 'lines': {
154 'total': lines_total,
155 'covered': lines_covered
156 }
157 }
158
159 def Get(self):
160 """Returns summary as a dictionary."""
161 return self._summary
162
163 def AddSummary(self, other_summary):
164 """Adds another summary to this one element-wise."""
165 for feature in self._summary:
166 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
167 self._summary[feature]['covered'] += other_summary.Get()[feature][
168 'covered']
169
170
Yuke Liaodd1ec0592018-02-02 01:26:37171class _CoverageReportHtmlGenerator(object):
172 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33173
Yuke Liaodd1ec0592018-02-02 01:26:37174 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33175 """
176
Yuke Liaodd1ec0592018-02-02 01:26:37177 def __init__(self, output_path, table_entry_type):
178 """Initializes _CoverageReportHtmlGenerator object.
179
180 Args:
181 output_path: Path to the html report that will be generated.
182 table_entry_type: Type of the table entries to be displayed in the table
183 header. For example: 'Path', 'Component'.
184 """
Yuke Liaoea228d02018-01-05 19:10:33185 css_file_name = os.extsep.join(['style', 'css'])
186 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
187 assert os.path.exists(css_absolute_path), (
188 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
189 'is called first, and the css file is generated at: "%s"' %
190 css_absolute_path)
191
192 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37193 self._output_path = output_path
194 self._table_entry_type = table_entry_type
195
Yuke Liaoea228d02018-01-05 19:10:33196 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12197 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33198 template_dir = os.path.join(
199 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
200
201 jinja_env = jinja2.Environment(
202 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
203 self._header_template = jinja_env.get_template('header.html')
204 self._table_template = jinja_env.get_template('table.html')
205 self._footer_template = jinja_env.get_template('footer.html')
206
207 def AddLinkToAnotherReport(self, html_report_path, name, summary):
208 """Adds a link to another html report in this report.
209
210 The link to be added is assumed to be an entry in this directory.
211 """
Yuke Liaodd1ec0592018-02-02 01:26:37212 # Use relative paths instead of absolute paths to make the generated reports
213 # portable.
214 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
215 html_report_path, self._output_path)
216
Yuke Liaod54030e2018-01-08 17:34:12217 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37218 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12219 os.path.basename(html_report_path) ==
220 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
221 self._table_entries.append(table_entry)
222
223 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35224 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12225 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
226
227 def _CreateTableEntryFromCoverageSummary(self,
228 summary,
229 href=None,
230 name=None,
231 is_dir=None):
232 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37233 assert (href is None and name is None and is_dir is None) or (
234 href is not None and name is not None and is_dir is not None), (
235 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35236 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37237 'attributes must be None.')
238
Yuke Liaod54030e2018-01-08 17:34:12239 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37240 if href is not None:
241 entry['href'] = href
242 if name is not None:
243 entry['name'] = name
244 if is_dir is not None:
245 entry['is_dir'] = is_dir
246
Yuke Liaoea228d02018-01-05 19:10:33247 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12248 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37249 if summary_dict[feature]['total'] == 0:
250 percentage = 0.0
251 else:
Yuke Liao0e4c8682018-04-18 21:06:59252 percentage = float(summary_dict[feature]
253 ['covered']) / summary_dict[feature]['total'] * 100
Yuke Liaoa785f4d32018-02-13 21:41:35254
Yuke Liaoea228d02018-01-05 19:10:33255 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12256 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33257 'total': summary_dict[feature]['total'],
258 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35259 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33260 'color_class': color_class
261 }
Yuke Liaod54030e2018-01-08 17:34:12262
Yuke Liaod54030e2018-01-08 17:34:12263 return entry
Yuke Liaoea228d02018-01-05 19:10:33264
265 def _GetColorClass(self, percentage):
266 """Returns the css color class based on coverage percentage."""
267 if percentage >= 0 and percentage < 80:
268 return 'red'
269 if percentage >= 80 and percentage < 100:
270 return 'yellow'
271 if percentage == 100:
272 return 'green'
273
274 assert False, 'Invalid coverage percentage: "%d"' % percentage
275
Yuke Liaodd1ec0592018-02-02 01:26:37276 def WriteHtmlCoverageReport(self):
277 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33278
279 In the report, sub-directories are displayed before files and within each
280 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33281 """
282
283 def EntryCmp(left, right):
284 """Compare function for table entries."""
285 if left['is_dir'] != right['is_dir']:
286 return -1 if left['is_dir'] == True else 1
287
Yuke Liaodd1ec0592018-02-02 01:26:37288 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33289
290 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
291
292 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37293 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
294 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
295 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
296
Yuke Liaoea228d02018-01-05 19:10:33297 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37298 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
299 directory_view_href=_GetRelativePathToDirectoryOfFile(
300 directory_view_path, self._output_path),
301 component_view_href=_GetRelativePathToDirectoryOfFile(
302 component_view_path, self._output_path),
303 file_view_href=_GetRelativePathToDirectoryOfFile(
304 file_view_path, self._output_path))
305
Yuke Liaod54030e2018-01-08 17:34:12306 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37307 entries=self._table_entries,
308 total_entry=self._total_entry,
309 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33310 html_footer = self._footer_template.render()
311
Yuke Liaodd1ec0592018-02-02 01:26:37312 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33313 html_file.write(html_header + html_table + html_footer)
314
Yuke Liao506e8822017-12-04 16:52:54315
Max Morozd73e45f2018-04-24 18:32:47316def _GetSharedLibraries(binary_paths):
317 """Returns set of shared libraries used by specified binaries."""
318 libraries = set()
319 cmd = []
320 shared_library_re = None
321
322 if sys.platform.startswith('linux'):
323 cmd.extend(['ldd'])
324 shared_library_re = re.compile(
325 r'.*\.so\s=>\s(.*' + BUILD_DIR + '.*\.so)\s.*')
326 elif sys.platform.startswith('darwin'):
327 cmd.extend(['otool', '-L'])
328 shared_library_re = re.compile(r'\s+(@rpath/.*\.dylib)\s.*')
329 else:
330 assert False, ('Cannot detect shared libraries used by the given targets.')
331
332 assert shared_library_re is not None
333
334 cmd.extend(binary_paths)
335 output = subprocess.check_output(cmd)
336
337 for line in output.splitlines():
338 m = shared_library_re.match(line)
339 if not m:
340 continue
341
342 shared_library_path = m.group(1)
343 if sys.platform.startswith('darwin'):
344 # otool outputs "@rpath" macro instead of the dirname of the given binary.
345 shared_library_path = shared_library_path.replace('@rpath', BUILD_DIR)
346
347 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
348 'the given target(s) does not '
349 'exist.' % shared_library_path)
350 with open(shared_library_path) as f:
351 data = f.read()
352
353 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors outs.
354 if '__llvm_cov' in data:
355 libraries.add(shared_library_path)
356
357 return list(libraries)
358
359
Yuke Liaoc60b2d02018-03-02 21:40:43360def _GetHostPlatform():
361 """Returns the host platform.
362
363 This is separate from the target platform/os that coverage is running for.
364 """
Abhishek Arya1ec832c2017-12-05 18:06:59365 if sys.platform == 'win32' or sys.platform == 'cygwin':
366 return 'win'
367 if sys.platform.startswith('linux'):
368 return 'linux'
369 else:
370 assert sys.platform == 'darwin'
371 return 'mac'
372
373
Yuke Liaoc60b2d02018-03-02 21:40:43374def _GetTargetOS():
375 """Returns the target os specified in args.gn file.
376
377 Returns an empty string is target_os is not specified.
378 """
Yuke Liao80afff32018-03-07 01:26:20379 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43380 return build_args['target_os'] if 'target_os' in build_args else ''
381
382
Yuke Liaob2926832018-03-02 17:34:29383def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10384 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43385 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10386
387
Yuke Liao506e8822017-12-04 16:52:54388# TODO(crbug.com/759794): remove this function once tools get included to
389# Clang bundle:
390# https://chromium-review.googlesource.com/c/chromium/src/+/688221
391def DownloadCoverageToolsIfNeeded():
392 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59393
Yuke Liaoc60b2d02018-03-02 21:40:43394 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54395 """Returns a pair of revision number by reading the build stamp file.
396
397 Args:
398 stamp_file_path: A path the build stamp file created by
399 tools/clang/scripts/update.py.
400 Returns:
401 A pair of integers represeting the main and sub revision respectively.
402 """
403 if not os.path.exists(stamp_file_path):
404 return 0, 0
405
406 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43407 stamp_file_line = stamp_file.readline()
408 if ',' in stamp_file_line:
409 package_version = stamp_file_line.rstrip().split(',')[0]
410 else:
411 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54412
Yuke Liaoc60b2d02018-03-02 21:40:43413 clang_revision_str, clang_sub_revision_str = package_version.split('-')
414 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59415
Yuke Liaoc60b2d02018-03-02 21:40:43416 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54417 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43418 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54419
420 coverage_revision_stamp_file = os.path.join(
421 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
422 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43423 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54424
Yuke Liaoea228d02018-01-05 19:10:33425 has_coverage_tools = (
426 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32427
Yuke Liaoea228d02018-01-05 19:10:33428 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54429 coverage_sub_revision == clang_sub_revision):
430 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43431 return
Yuke Liao506e8822017-12-04 16:52:54432
433 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
434 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
435
436 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43437 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54438 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43439 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54440 coverage_tools_url = (
441 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43442 else:
443 assert host_platform == 'win'
444 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54445
446 try:
447 clang_update.DownloadAndUnpack(coverage_tools_url,
448 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10449 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54450 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43451 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54452 file_handle.write('\n')
453 except urllib2.URLError:
454 raise Exception(
455 'Failed to download coverage tools: %s.' % coverage_tools_url)
456
457
Yuke Liaodd1ec0592018-02-02 01:26:37458def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59459 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54460 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
461
462 For a file with absolute path /a/b/x.cc, a html report is generated as:
463 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
464 OUTPUT_DIR/index.html.
465
466 Args:
467 binary_paths: A list of paths to the instrumented binaries.
468 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42469 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54470 """
Yuke Liao506e8822017-12-04 16:52:54471 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
472 # [[-object BIN]] [SOURCES]
473 # NOTE: For object files, the first one is specified as a positional argument,
474 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10475 logging.debug('Generating per file line by line coverage reports using '
476 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59477 subprocess_cmd = [
478 LLVM_COV_PATH, 'show', '-format=html',
479 '-output-dir={}'.format(OUTPUT_DIR),
480 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
481 ]
482 subprocess_cmd.extend(
483 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29484 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42485 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59486 if ignore_filename_regex:
487 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
488
Yuke Liao506e8822017-12-04 16:52:54489 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10490 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54491
492
Yuke Liaodd1ec0592018-02-02 01:26:37493def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
494 """Generates html index file for file view."""
495 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
496 logging.debug('Generating file view html index file as: "%s".',
497 file_view_index_file_path)
498 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
499 'Path')
500 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33501
Yuke Liaodd1ec0592018-02-02 01:26:37502 for file_path in per_file_coverage_summary:
503 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
504
505 html_generator.AddLinkToAnotherReport(
506 _GetCoverageHtmlReportPathForFile(file_path),
507 os.path.relpath(file_path, SRC_ROOT_PATH),
508 per_file_coverage_summary[file_path])
509
510 html_generator.CreateTotalsEntry(totals_coverage_summary)
511 html_generator.WriteHtmlCoverageReport()
512 logging.debug('Finished generating file view html index file.')
513
514
515def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
516 """Calculates per directory coverage summary."""
517 logging.debug('Calculating per-directory coverage summary')
518 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
519
Yuke Liaoea228d02018-01-05 19:10:33520 for file_path in per_file_coverage_summary:
521 summary = per_file_coverage_summary[file_path]
522 parent_dir = os.path.dirname(file_path)
523 while True:
524 per_directory_coverage_summary[parent_dir].AddSummary(summary)
525
526 if parent_dir == SRC_ROOT_PATH:
527 break
528 parent_dir = os.path.dirname(parent_dir)
529
Yuke Liaodd1ec0592018-02-02 01:26:37530 logging.debug('Finished calculating per-directory coverage summary')
531 return per_directory_coverage_summary
532
533
534def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
535 per_file_coverage_summary):
536 """Generates per directory coverage breakdown in html."""
537 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33538 for dir_path in per_directory_coverage_summary:
539 _GenerateCoverageInHtmlForDirectory(
540 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
541
Yuke Liaodd1ec0592018-02-02 01:26:37542 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10543
Yuke Liaoea228d02018-01-05 19:10:33544
545def _GenerateCoverageInHtmlForDirectory(
546 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
547 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37548 html_generator = _CoverageReportHtmlGenerator(
549 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33550
551 for entry_name in os.listdir(dir_path):
552 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33553
Yuke Liaodd1ec0592018-02-02 01:26:37554 if entry_path in per_file_coverage_summary:
555 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
556 entry_coverage_summary = per_file_coverage_summary[entry_path]
557 elif entry_path in per_directory_coverage_summary:
558 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
559 entry_path)
560 entry_coverage_summary = per_directory_coverage_summary[entry_path]
561 else:
Yuke Liaoc7e607142018-02-05 20:26:14562 # Any file without executable lines shouldn't be included into the report.
563 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37564 continue
Yuke Liaoea228d02018-01-05 19:10:33565
Yuke Liaodd1ec0592018-02-02 01:26:37566 html_generator.AddLinkToAnotherReport(entry_html_report_path,
567 os.path.basename(entry_path),
568 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33569
Yuke Liaod54030e2018-01-08 17:34:12570 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37571 html_generator.WriteHtmlCoverageReport()
572
573
574def _GenerateDirectoryViewHtmlIndexFile():
575 """Generates the html index file for directory view.
576
577 Note that the index file is already generated under SRC_ROOT_PATH, so this
578 file simply redirects to it, and the reason of this extra layer is for
579 structural consistency with other views.
580 """
581 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
582 DIRECTORY_VIEW_INDEX_FILE)
583 logging.debug('Generating directory view html index file as: "%s".',
584 directory_view_index_file_path)
585 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
586 SRC_ROOT_PATH)
587 _WriteRedirectHtmlFile(directory_view_index_file_path,
588 src_root_html_report_path)
589 logging.debug('Finished generating directory view html index file.')
590
591
592def _CalculatePerComponentCoverageSummary(component_to_directories,
593 per_directory_coverage_summary):
594 """Calculates per component coverage summary."""
595 logging.debug('Calculating per-component coverage summary')
596 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
597
598 for component in component_to_directories:
599 for directory in component_to_directories[component]:
600 absolute_directory_path = os.path.abspath(directory)
601 if absolute_directory_path in per_directory_coverage_summary:
602 per_component_coverage_summary[component].AddSummary(
603 per_directory_coverage_summary[absolute_directory_path])
604
605 logging.debug('Finished calculating per-component coverage summary')
606 return per_component_coverage_summary
607
608
609def _ExtractComponentToDirectoriesMapping():
610 """Returns a mapping from components to directories."""
611 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
612 directory_to_component = component_mappings['dir-to-component']
613
614 component_to_directories = defaultdict(list)
615 for directory in directory_to_component:
616 component = directory_to_component[directory]
617 component_to_directories[component].append(directory)
618
619 return component_to_directories
620
621
622def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
623 component_to_directories,
624 per_directory_coverage_summary):
625 """Generates per-component coverage reports in html."""
626 logging.debug('Writing per-component coverage html reports.')
627 for component in per_component_coverage_summary:
628 _GenerateCoverageInHtmlForComponent(
629 component, per_component_coverage_summary, component_to_directories,
630 per_directory_coverage_summary)
631
632 logging.debug('Finished writing per-component coverage html reports.')
633
634
635def _GenerateCoverageInHtmlForComponent(
636 component_name, per_component_coverage_summary, component_to_directories,
637 per_directory_coverage_summary):
638 """Generates coverage html report for a component."""
639 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
640 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14641 component_html_report_dir = os.path.dirname(component_html_report_path)
642 if not os.path.exists(component_html_report_dir):
643 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37644
645 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
646 'Path')
647
648 for dir_path in component_to_directories[component_name]:
649 dir_absolute_path = os.path.abspath(dir_path)
650 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14651 # Any directory without an excercised file shouldn't be included into the
652 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37653 continue
654
655 html_generator.AddLinkToAnotherReport(
656 _GetCoverageHtmlReportPathForDirectory(dir_path),
657 os.path.relpath(dir_path, SRC_ROOT_PATH),
658 per_directory_coverage_summary[dir_absolute_path])
659
660 html_generator.CreateTotalsEntry(
661 per_component_coverage_summary[component_name])
662 html_generator.WriteHtmlCoverageReport()
663
664
665def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
666 """Generates the html index file for component view."""
667 component_view_index_file_path = os.path.join(OUTPUT_DIR,
668 COMPONENT_VIEW_INDEX_FILE)
669 logging.debug('Generating component view html index file as: "%s".',
670 component_view_index_file_path)
671 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
672 'Component')
673 totals_coverage_summary = _CoverageSummary()
674
675 for component in per_component_coverage_summary:
676 totals_coverage_summary.AddSummary(
677 per_component_coverage_summary[component])
678
679 html_generator.AddLinkToAnotherReport(
680 _GetCoverageHtmlReportPathForComponent(component), component,
681 per_component_coverage_summary[component])
682
683 html_generator.CreateTotalsEntry(totals_coverage_summary)
684 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14685 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33686
687
688def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37689 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33690 html_index_file_path = os.path.join(OUTPUT_DIR,
691 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37692 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
693 DIRECTORY_VIEW_INDEX_FILE)
694 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
695
696
697def _WriteRedirectHtmlFile(from_html_path, to_html_path):
698 """Writes a html file that redirects to another html file."""
699 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
700 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33701 content = ("""
702 <!DOCTYPE html>
703 <html>
704 <head>
705 <!-- HTML meta refresh URL redirection -->
706 <meta http-equiv="refresh" content="0; url=%s">
707 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37708 </html>""" % to_html_relative_path)
709 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33710 f.write(content)
711
712
Yuke Liaodd1ec0592018-02-02 01:26:37713def _GetCoverageHtmlReportPathForFile(file_path):
714 """Given a file path, returns the corresponding html report path."""
715 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
716 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
717
718 # '+' is used instead of os.path.join because both of them are absolute paths
719 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14720 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37721 return _GetCoverageReportRootDirPath() + html_report_path
722
723
724def _GetCoverageHtmlReportPathForDirectory(dir_path):
725 """Given a directory path, returns the corresponding html report path."""
726 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
727 html_report_path = os.path.join(
728 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
729
730 # '+' is used instead of os.path.join because both of them are absolute paths
731 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14732 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37733 return _GetCoverageReportRootDirPath() + html_report_path
734
735
736def _GetCoverageHtmlReportPathForComponent(component_name):
737 """Given a component, returns the corresponding html report path."""
738 component_file_name = component_name.lower().replace('>', '-')
739 html_report_name = os.extsep.join([component_file_name, 'html'])
740 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
741 html_report_name)
742
743
744def _GetCoverageReportRootDirPath():
745 """The root directory that contains all generated coverage html reports."""
746 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33747
748
Yuke Liao506e8822017-12-04 16:52:54749def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
750 """Builds and runs target to generate the coverage profile data.
751
752 Args:
753 targets: A list of targets to build with coverage instrumentation.
754 commands: A list of commands used to run the targets.
755 jobs_count: Number of jobs to run in parallel for building. If None, a
756 default value is derived based on CPUs availability.
757
758 Returns:
759 A relative path to the generated profdata file.
760 """
761 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59762 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
763 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54764 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
765 profraw_file_paths)
766
Yuke Liaod4a9865202018-01-12 23:17:52767 for profraw_file_path in profraw_file_paths:
768 os.remove(profraw_file_path)
769
Yuke Liao506e8822017-12-04 16:52:54770 return profdata_file_path
771
772
773def _BuildTargets(targets, jobs_count):
774 """Builds target with Clang coverage instrumentation.
775
776 This function requires current working directory to be the root of checkout.
777
778 Args:
779 targets: A list of targets to build with coverage instrumentation.
780 jobs_count: Number of jobs to run in parallel for compilation. If None, a
781 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54782 """
Abhishek Arya1ec832c2017-12-05 18:06:59783
Yuke Liao506e8822017-12-04 16:52:54784 def _IsGomaConfigured():
785 """Returns True if goma is enabled in the gn build args.
786
787 Returns:
788 A boolean indicates whether goma is configured for building or not.
789 """
Yuke Liao80afff32018-03-07 01:26:20790 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54791 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
792
Yuke Liao481d3482018-01-29 19:17:10793 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54794 if jobs_count is None and _IsGomaConfigured():
795 jobs_count = DEFAULT_GOMA_JOBS
796
797 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
798 if jobs_count is not None:
799 subprocess_cmd.append('-j' + str(jobs_count))
800
801 subprocess_cmd.extend(targets)
802 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10803 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54804
805
806def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
807 """Runs commands and returns the relative paths to the profraw data files.
808
809 Args:
810 targets: A list of targets built with coverage instrumentation.
811 commands: A list of commands used to run the targets.
812
813 Returns:
814 A list of relative paths to the generated profraw data files.
815 """
Yuke Liao481d3482018-01-29 19:17:10816 logging.debug('Executing the test commands')
817
Yuke Liao506e8822017-12-04 16:52:54818 # Remove existing profraw data files.
819 for file_or_dir in os.listdir(OUTPUT_DIR):
820 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
821 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
822
Yuke Liaoa0c8c2f2018-02-28 20:14:10823 profraw_file_paths = []
824
Yuke Liaod4a9865202018-01-12 23:17:52825 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54826 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10827 output_file_name = os.extsep.join([target + '_output', 'txt'])
828 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
829 logging.info('Running command: "%s", the output is redirected to "%s"',
830 command, output_file_path)
831
Yuke Liaob2926832018-03-02 17:34:29832 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10833 # On iOS platform, due to lack of write permissions, profraw files are
834 # generated outside of the OUTPUT_DIR, and the exact paths are contained
835 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29836 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10837 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
838 else:
839 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
840 output = _ExecuteCommand(target, command)
841
842 with open(output_file_path, 'w') as output_file:
843 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54844
Yuke Liao481d3482018-01-29 19:17:10845 logging.debug('Finished executing the test commands')
846
Yuke Liaob2926832018-03-02 17:34:29847 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10848 return profraw_file_paths
849
Yuke Liao506e8822017-12-04 16:52:54850 for file_or_dir in os.listdir(OUTPUT_DIR):
851 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
852 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
853
854 # Assert one target/command generates at least one profraw data file.
855 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59856 assert any(
857 os.path.basename(profraw_file).startswith(target)
858 for profraw_file in profraw_file_paths), (
859 'Running target: %s failed to generate any profraw data file, '
860 'please make sure the binary exists and is properly instrumented.' %
861 target)
Yuke Liao506e8822017-12-04 16:52:54862
863 return profraw_file_paths
864
865
866def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10867 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52868 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01869 #
Max Morozd73e45f2018-04-24 18:32:47870 # "%p" expands out to the process ID. It's not used by this scripts due to:
871 # 1) If a target program spawns too many processess, it may exhaust all disk
872 # space available. For example, unit_tests writes thousands of .profraw
873 # files each of size 1GB+.
874 # 2) If a target binary uses shared libraries, coverage profile data for them
875 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01876 #
Yuke Liaod4a9865202018-01-12 23:17:52877 # "%Nm" expands out to the instrumented binary's signature. When this pattern
878 # is specified, the runtime creates a pool of N raw profiles which are used
879 # for on-line profile merging. The runtime takes care of selecting a raw
880 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52881 # N must be between 1 and 9. The merge pool specifier can only occur once per
882 # filename pattern.
883 #
Max Morozd73e45f2018-04-24 18:32:47884 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01885 #
Max Morozd73e45f2018-04-24 18:32:47886 # For other cases, "%4m" is chosen as it creates some level of parallelism,
887 # but it's not too big to consume too much computing resource or disk space.
888 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59889 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01890 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54891 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
892 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54893
Yuke Liaoa0c8c2f2018-02-28 20:14:10894 try:
895 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29896 shlex.split(command),
897 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10898 except subprocess.CalledProcessError as e:
899 output = e.output
900 logging.warning('Command: "%s" exited with non-zero return code', command)
901
902 return output
903
904
Yuke Liao27349c92018-03-22 21:10:01905def _IsFuzzerTarget(target):
906 """Returns true if the target is a fuzzer target."""
907 build_args = _GetBuildArgs()
908 use_libfuzzer = ('use_libfuzzer' in build_args and
909 build_args['use_libfuzzer'] == 'true')
910 return use_libfuzzer and target.endswith('_fuzzer')
911
912
Yuke Liaob2926832018-03-02 17:34:29913def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10914 """Runs a single iOS command and generates a profraw data file.
915
916 iOS application doesn't have write access to folders outside of the app, so
917 it's impossible to instruct the app to flush the profraw data file to the
918 desired location. The profraw data file will be generated somewhere within the
919 application's Documents folder, and the full path can be obtained by parsing
920 the output.
921 """
Yuke Liaob2926832018-03-02 17:34:29922 assert _IsIOSCommand(command)
923
924 # After running tests, iossim generates a profraw data file, it won't be
925 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
926 # checkout.
927 iossim_profraw_file_path = os.path.join(
928 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10929
930 try:
Yuke Liaob2926832018-03-02 17:34:29931 output = subprocess.check_output(
932 shlex.split(command),
933 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10934 except subprocess.CalledProcessError as e:
935 # iossim emits non-zero return code even if tests run successfully, so
936 # ignore the return code.
937 output = e.output
938
939 return output
940
941
942def _GetProfrawDataFileByParsingOutput(output):
943 """Returns the path to the profraw data file obtained by parsing the output.
944
945 The output of running the test target has no format, but it is guaranteed to
946 have a single line containing the path to the generated profraw data file.
947 NOTE: This should only be called when target os is iOS.
948 """
Yuke Liaob2926832018-03-02 17:34:29949 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10950
Yuke Liaob2926832018-03-02 17:34:29951 output_by_lines = ''.join(output).splitlines()
952 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10953
954 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29955 result = profraw_file_pattern.match(line)
956 if result:
957 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10958
959 assert False, ('No profraw data file was generated, did you call '
960 'coverage_util::ConfigureCoverageReportPath() in test setup? '
961 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54962
963
964def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
965 """Returns a relative path to the profdata file by merging profraw data files.
966
967 Args:
968 profraw_file_paths: A list of relative paths to the profraw data files that
969 are to be merged.
970
971 Returns:
972 A relative path to the generated profdata file.
973
974 Raises:
975 CalledProcessError: An error occurred merging profraw data files.
976 """
Yuke Liao481d3482018-01-29 19:17:10977 logging.info('Creating the coverage profile data file')
978 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54979 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
980 try:
Abhishek Arya1ec832c2017-12-05 18:06:59981 subprocess_cmd = [
982 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
983 ]
Yuke Liao506e8822017-12-04 16:52:54984 subprocess_cmd.extend(profraw_file_paths)
985 subprocess.check_call(subprocess_cmd)
986 except subprocess.CalledProcessError as error:
987 print('Failed to merge profraw files to create profdata file')
988 raise error
989
Yuke Liao481d3482018-01-29 19:17:10990 logging.debug('Finished merging profraw files')
991 logging.info('Code coverage profile data is created as: %s',
992 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54993 return profdata_file_path
994
995
Yuke Liao0e4c8682018-04-18 21:06:59996def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
997 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:33998 """Generates per file coverage summary using "llvm-cov export" command."""
999 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
1000 # [[-object BIN]] [SOURCES].
1001 # NOTE: For object files, the first one is specified as a positional argument,
1002 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:101003 logging.debug('Generating per-file code coverage summary using "llvm-cov '
1004 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:331005 subprocess_cmd = [
1006 LLVM_COV_PATH, 'export', '-summary-only',
1007 '-instr-profile=' + profdata_file_path, binary_paths[0]
1008 ]
1009 subprocess_cmd.extend(
1010 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:291011 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:331012 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:591013 if ignore_filename_regex:
1014 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:331015
1016 json_output = json.loads(subprocess.check_output(subprocess_cmd))
1017 assert len(json_output['data']) == 1
1018 files_coverage_data = json_output['data'][0]['files']
1019
1020 per_file_coverage_summary = {}
1021 for file_coverage_data in files_coverage_data:
1022 file_path = file_coverage_data['filename']
1023 summary = file_coverage_data['summary']
1024
Yuke Liaoea228d02018-01-05 19:10:331025 if summary['lines']['count'] == 0:
1026 continue
1027
1028 per_file_coverage_summary[file_path] = _CoverageSummary(
1029 regions_total=summary['regions']['count'],
1030 regions_covered=summary['regions']['covered'],
1031 functions_total=summary['functions']['count'],
1032 functions_covered=summary['functions']['covered'],
1033 lines_total=summary['lines']['count'],
1034 lines_covered=summary['lines']['covered'])
1035
Yuke Liao481d3482018-01-29 19:17:101036 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:331037 return per_file_coverage_summary
1038
1039
Yuke Liaob2926832018-03-02 17:34:291040def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
1041 """Appends -arch arguments to the command list if it's ios platform.
1042
1043 iOS binaries are universal binaries, and require specifying the architecture
1044 to use, and one architecture needs to be specified for each binary.
1045 """
1046 if _IsIOS():
1047 cmd_list.extend(['-arch=x86_64'] * num_archs)
1048
1049
Yuke Liao506e8822017-12-04 16:52:541050def _GetBinaryPath(command):
1051 """Returns a relative path to the binary to be run by the command.
1052
Yuke Liao545db322018-02-15 17:12:011053 Currently, following types of commands are supported (e.g. url_unittests):
1054 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1055 2. Use xvfb.
1056 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1057 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371058 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1059 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101060 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371061 <iossim_arguments> -c <app_arguments>
1062 out/Coverage-iphonesimulator/url_unittests.app"
1063
Yuke Liao545db322018-02-15 17:12:011064
Yuke Liao506e8822017-12-04 16:52:541065 Args:
1066 command: A command used to run a target.
1067
1068 Returns:
1069 A relative path to the binary.
1070 """
Yuke Liao545db322018-02-15 17:12:011071 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1072
Yuke Liaob2926832018-03-02 17:34:291073 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011074 if os.path.basename(command_parts[0]) == 'python':
1075 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1076 'This tool doesn\'t understand the command: "%s"' % command)
1077 return command_parts[2]
1078
1079 if os.path.basename(command_parts[0]) == xvfb_script_name:
1080 return command_parts[1]
1081
Yuke Liaob2926832018-03-02 17:34:291082 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101083 # For a given application bundle, the binary resides in the bundle and has
1084 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371085 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101086 app_name = os.path.splitext(os.path.basename(app_path))[0]
1087 return os.path.join(app_path, app_name)
1088
Yuke Liaob2926832018-03-02 17:34:291089 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541090
1091
Yuke Liaob2926832018-03-02 17:34:291092def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101093 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291094 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101095
1096
Yuke Liao95d13d72017-12-07 18:18:501097def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1098 """Verifies that the target executables specified in the commands are inside
1099 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541100 for command in commands:
1101 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501102 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1103 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1104 'Target executable "%s" in command: "%s" is outside of '
1105 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541106
1107
1108def _ValidateBuildingWithClangCoverage():
1109 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201110 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541111
1112 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1113 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591114 assert False, ('\'{} = true\' is required in args.gn.'
1115 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541116
1117
Yuke Liaoc60b2d02018-03-02 21:40:431118def _ValidateCurrentPlatformIsSupported():
1119 """Asserts that this script suports running on the current platform"""
1120 target_os = _GetTargetOS()
1121 if target_os:
1122 current_platform = target_os
1123 else:
1124 current_platform = _GetHostPlatform()
1125
1126 assert current_platform in [
1127 'linux', 'mac', 'chromeos', 'ios'
1128 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1129
1130
Yuke Liao80afff32018-03-07 01:26:201131def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541132 """Parses args.gn file and returns results as a dictionary.
1133
1134 Returns:
1135 A dictionary representing the build args.
1136 """
Yuke Liao80afff32018-03-07 01:26:201137 global _BUILD_ARGS
1138 if _BUILD_ARGS is not None:
1139 return _BUILD_ARGS
1140
1141 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541142 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1143 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1144 'missing args.gn file.' % BUILD_DIR)
1145 with open(build_args_path) as build_args_file:
1146 build_args_lines = build_args_file.readlines()
1147
Yuke Liao506e8822017-12-04 16:52:541148 for build_arg_line in build_args_lines:
1149 build_arg_without_comments = build_arg_line.split('#')[0]
1150 key_value_pair = build_arg_without_comments.split('=')
1151 if len(key_value_pair) != 2:
1152 continue
1153
1154 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431155
1156 # Values are wrapped within a pair of double-quotes, so remove the leading
1157 # and trailing double-quotes.
1158 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201159 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541160
Yuke Liao80afff32018-03-07 01:26:201161 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541162
1163
Abhishek Arya16f059a2017-12-07 17:47:321164def _VerifyPathsAndReturnAbsolutes(paths):
1165 """Verifies that the paths specified in |paths| exist and returns absolute
1166 versions.
Yuke Liao66da1732017-12-05 22:19:421167
1168 Args:
1169 paths: A list of files or directories.
1170 """
Abhishek Arya16f059a2017-12-07 17:47:321171 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421172 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321173 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1174 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1175
1176 absolute_paths.append(absolute_path)
1177
1178 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421179
1180
Yuke Liaodd1ec0592018-02-02 01:26:371181def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1182 """Returns a target path relative to the directory of base_path.
1183
1184 This method requires base_path to be a file, otherwise, one should call
1185 os.path.relpath directly.
1186 """
1187 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141188 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371189 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141190 base_dir = os.path.dirname(base_path)
1191 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371192
1193
Yuke Liao506e8822017-12-04 16:52:541194def _ParseCommandArguments():
1195 """Adds and parses relevant arguments for tool comands.
1196
1197 Returns:
1198 A dictionary representing the arguments.
1199 """
1200 arg_parser = argparse.ArgumentParser()
1201 arg_parser.usage = __doc__
1202
Abhishek Arya1ec832c2017-12-05 18:06:591203 arg_parser.add_argument(
1204 '-b',
1205 '--build-dir',
1206 type=str,
1207 required=True,
1208 help='The build directory, the path needs to be relative to the root of '
1209 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541210
Abhishek Arya1ec832c2017-12-05 18:06:591211 arg_parser.add_argument(
1212 '-o',
1213 '--output-dir',
1214 type=str,
1215 required=True,
1216 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541217
Abhishek Arya1ec832c2017-12-05 18:06:591218 arg_parser.add_argument(
1219 '-c',
1220 '--command',
1221 action='append',
1222 required=True,
1223 help='Commands used to run test targets, one test target needs one and '
1224 'only one command, when specifying commands, one should assume the '
1225 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541226
Abhishek Arya1ec832c2017-12-05 18:06:591227 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421228 '-f',
1229 '--filters',
1230 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321231 required=False,
Yuke Liao66da1732017-12-05 22:19:421232 help='Directories or files to get code coverage for, and all files under '
1233 'the directories are included recursively.')
1234
1235 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:591236 '-i',
1237 '--ignore-filename-regex',
1238 type=str,
1239 help='Skip source code files with file paths that match the given '
1240 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
1241 'to exclude files in third_party/ and out/ folders from the report.')
1242
1243 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591244 '-j',
1245 '--jobs',
1246 type=int,
1247 default=None,
1248 help='Run N jobs to build in parallel. If not specified, a default value '
1249 'will be derived based on CPUs availability. Please refer to '
1250 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541251
Abhishek Arya1ec832c2017-12-05 18:06:591252 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101253 '-v',
1254 '--verbose',
1255 action='store_true',
1256 help='Prints additional output for diagnostics.')
1257
1258 arg_parser.add_argument(
1259 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1260
1261 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591262 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541263
1264 args = arg_parser.parse_args()
1265 return args
1266
1267
1268def Main():
1269 """Execute tool commands."""
1270 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1271 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591272 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541273 args = _ParseCommandArguments()
1274 global BUILD_DIR
1275 BUILD_DIR = args.build_dir
1276 global OUTPUT_DIR
1277 OUTPUT_DIR = args.output_dir
1278
1279 assert len(args.targets) == len(args.command), ('Number of targets must be '
1280 'equal to the number of test '
1281 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431282
1283 # logging should be configured before it is used.
1284 log_level = logging.DEBUG if args.verbose else logging.INFO
1285 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1286 log_file = args.log_file if args.log_file else None
1287 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1288
Abhishek Arya1ec832c2017-12-05 18:06:591289 assert os.path.exists(BUILD_DIR), (
1290 'Build directory: {} doesn\'t exist. '
1291 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431292 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541293 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501294 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321295
Yuke Liaoc60b2d02018-03-02 21:40:431296 DownloadCoverageToolsIfNeeded()
1297
Abhishek Arya16f059a2017-12-07 17:47:321298 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421299 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321300 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421301
Yuke Liao506e8822017-12-04 16:52:541302 if not os.path.exists(OUTPUT_DIR):
1303 os.makedirs(OUTPUT_DIR)
1304
Abhishek Arya1ec832c2017-12-05 18:06:591305 profdata_file_path = _CreateCoverageProfileDataForTargets(
1306 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541307 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331308
Yuke Liao481d3482018-01-29 19:17:101309 logging.info('Generating code coverage report in html (this can take a while '
1310 'depending on size of target!)')
Max Morozd73e45f2018-04-24 18:32:471311 binary_paths.extend(_GetSharedLibraries(binary_paths))
Yuke Liaodd1ec0592018-02-02 01:26:371312 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591313 binary_paths, profdata_file_path, absolute_filter_paths,
1314 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371315 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591316 absolute_filter_paths,
1317 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371318 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1319
1320 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1321 per_file_coverage_summary)
1322 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1323 per_file_coverage_summary)
1324 _GenerateDirectoryViewHtmlIndexFile()
1325
1326 component_to_directories = _ExtractComponentToDirectoriesMapping()
1327 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1328 component_to_directories, per_directory_coverage_summary)
1329 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1330 component_to_directories,
1331 per_directory_coverage_summary)
1332 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331333
1334 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371335 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331336 _OverwriteHtmlReportsIndexFile()
1337
Yuke Liao506e8822017-12-04 16:52:541338 html_index_file_path = 'file://' + os.path.abspath(
1339 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101340 logging.info('Index file for html report is generated as: %s',
1341 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541342
Abhishek Arya1ec832c2017-12-05 18:06:591343
Yuke Liao506e8822017-12-04 16:52:541344if __name__ == '__main__':
1345 sys.exit(Main())