blob: 1e608181c8f87307caa99ccbc077ba333b1be170 [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 Liaoab9c44e2018-02-21 00:24:4014 Clang Source-based Code Coverage requires "is_component_build=false" flag
15 because: There will be no coverage info for libraries in component builds and
16 "is_component_build" is set to true by "is_debug" unless it is explicitly set
17 to false.
Yuke Liao506e8822017-12-04 16:52:5418
Abhishek Arya1ec832c2017-12-05 18:06:5919 Example usage:
20
Abhishek Arya16f059a2017-12-07 17:47:3221 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
22 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5923 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3224 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
25 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
26 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5927
Abhishek Arya16f059a2017-12-07 17:47:3228 The command above builds crypto_unittests and url_unittests targets and then
29 runs them with specified command line arguments. For url_unittests, it only
30 runs the test URLParser.PathURL. The coverage report is filtered to include
31 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5932
Yuke Liao545db322018-02-15 17:12:0133 If you want to run tests that try to draw to the screen but don't have a
34 display connected, you can run tests in headless mode with xvfb.
35
36 Sample flow for running a test target with xvfb (e.g. unit_tests):
37
38 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
39 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
40
Abhishek Arya1ec832c2017-12-05 18:06:5941 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
42 flag as well.
43
44 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
45
Abhishek Arya16f059a2017-12-07 17:47:3246 python tools/code_coverage/coverage.py pdfium_fuzzer \\
47 -b out/coverage -o out/report \\
48 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
49 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5950
51 where:
52 <corpus_dir> - directory containing samples files for this format.
53 <runs> - number of times to fuzz target function. Should be 0 when you just
54 want to see the coverage on corpus and don't want to fuzz at all.
55
56 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao506e8822017-12-04 16:52:5457"""
58
59from __future__ import print_function
60
61import sys
62
63import argparse
Yuke Liaoea228d02018-01-05 19:10:3364import json
Yuke Liao481d3482018-01-29 19:17:1065import logging
Yuke Liao506e8822017-12-04 16:52:5466import os
Yuke Liaob2926832018-03-02 17:34:2967import re
68import shlex
Yuke Liao506e8822017-12-04 16:52:5469import subprocess
Yuke Liao506e8822017-12-04 16:52:5470import urllib2
71
Abhishek Arya1ec832c2017-12-05 18:06:5972sys.path.append(
73 os.path.join(
74 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
75 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5476import update as clang_update
77
Yuke Liaoea228d02018-01-05 19:10:3378sys.path.append(
79 os.path.join(
80 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
81 'third_party'))
82import jinja2
83from collections import defaultdict
84
Yuke Liao506e8822017-12-04 16:52:5485# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5986SRC_ROOT_PATH = os.path.abspath(
87 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5488
89# Absolute path to the code coverage tools binary.
90LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
91LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
92LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
93
94# Build directory, the value is parsed from command line arguments.
95BUILD_DIR = None
96
97# Output directory for generated artifacts, the value is parsed from command
98# line arguemnts.
99OUTPUT_DIR = None
100
101# Default number of jobs used to build when goma is configured and enabled.
102DEFAULT_GOMA_JOBS = 100
103
104# Name of the file extension for profraw data files.
105PROFRAW_FILE_EXTENSION = 'profraw'
106
107# Name of the final profdata file, and this file needs to be passed to
108# "llvm-cov" command in order to call "llvm-cov show" to inspect the
109# line-by-line coverage of specific files.
110PROFDATA_FILE_NAME = 'coverage.profdata'
111
112# Build arg required for generating code coverage data.
113CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
114
Yuke Liaoea228d02018-01-05 19:10:33115# The default name of the html coverage report for a directory.
116DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
117
Yuke Liaodd1ec0592018-02-02 01:26:37118# Name of the html index files for different views.
119DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
120COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
121FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
122
123# Used to extract a mapping between directories and components.
124COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
125
Yuke Liaoea228d02018-01-05 19:10:33126
127class _CoverageSummary(object):
128 """Encapsulates coverage summary representation."""
129
Yuke Liaodd1ec0592018-02-02 01:26:37130 def __init__(self,
131 regions_total=0,
132 regions_covered=0,
133 functions_total=0,
134 functions_covered=0,
135 lines_total=0,
136 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33137 """Initializes _CoverageSummary object."""
138 self._summary = {
139 'regions': {
140 'total': regions_total,
141 'covered': regions_covered
142 },
143 'functions': {
144 'total': functions_total,
145 'covered': functions_covered
146 },
147 'lines': {
148 'total': lines_total,
149 'covered': lines_covered
150 }
151 }
152
153 def Get(self):
154 """Returns summary as a dictionary."""
155 return self._summary
156
157 def AddSummary(self, other_summary):
158 """Adds another summary to this one element-wise."""
159 for feature in self._summary:
160 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
161 self._summary[feature]['covered'] += other_summary.Get()[feature][
162 'covered']
163
164
Yuke Liaodd1ec0592018-02-02 01:26:37165class _CoverageReportHtmlGenerator(object):
166 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33167
Yuke Liaodd1ec0592018-02-02 01:26:37168 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33169 """
170
Yuke Liaodd1ec0592018-02-02 01:26:37171 def __init__(self, output_path, table_entry_type):
172 """Initializes _CoverageReportHtmlGenerator object.
173
174 Args:
175 output_path: Path to the html report that will be generated.
176 table_entry_type: Type of the table entries to be displayed in the table
177 header. For example: 'Path', 'Component'.
178 """
Yuke Liaoea228d02018-01-05 19:10:33179 css_file_name = os.extsep.join(['style', 'css'])
180 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
181 assert os.path.exists(css_absolute_path), (
182 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
183 'is called first, and the css file is generated at: "%s"' %
184 css_absolute_path)
185
186 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37187 self._output_path = output_path
188 self._table_entry_type = table_entry_type
189
Yuke Liaoea228d02018-01-05 19:10:33190 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12191 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33192 template_dir = os.path.join(
193 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
194
195 jinja_env = jinja2.Environment(
196 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
197 self._header_template = jinja_env.get_template('header.html')
198 self._table_template = jinja_env.get_template('table.html')
199 self._footer_template = jinja_env.get_template('footer.html')
200
201 def AddLinkToAnotherReport(self, html_report_path, name, summary):
202 """Adds a link to another html report in this report.
203
204 The link to be added is assumed to be an entry in this directory.
205 """
Yuke Liaodd1ec0592018-02-02 01:26:37206 # Use relative paths instead of absolute paths to make the generated reports
207 # portable.
208 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
209 html_report_path, self._output_path)
210
Yuke Liaod54030e2018-01-08 17:34:12211 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37212 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12213 os.path.basename(html_report_path) ==
214 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
215 self._table_entries.append(table_entry)
216
217 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35218 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12219 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
220
221 def _CreateTableEntryFromCoverageSummary(self,
222 summary,
223 href=None,
224 name=None,
225 is_dir=None):
226 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37227 assert (href is None and name is None and is_dir is None) or (
228 href is not None and name is not None and is_dir is not None), (
229 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35230 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37231 'attributes must be None.')
232
Yuke Liaod54030e2018-01-08 17:34:12233 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37234 if href is not None:
235 entry['href'] = href
236 if name is not None:
237 entry['name'] = name
238 if is_dir is not None:
239 entry['is_dir'] = is_dir
240
Yuke Liaoea228d02018-01-05 19:10:33241 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12242 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37243 if summary_dict[feature]['total'] == 0:
244 percentage = 0.0
245 else:
Yuke Liaoa785f4d32018-02-13 21:41:35246 percentage = float(summary_dict[feature]['covered']) / summary_dict[
247 feature]['total'] * 100
248
Yuke Liaoea228d02018-01-05 19:10:33249 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12250 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33251 'total': summary_dict[feature]['total'],
252 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35253 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33254 'color_class': color_class
255 }
Yuke Liaod54030e2018-01-08 17:34:12256
Yuke Liaod54030e2018-01-08 17:34:12257 return entry
Yuke Liaoea228d02018-01-05 19:10:33258
259 def _GetColorClass(self, percentage):
260 """Returns the css color class based on coverage percentage."""
261 if percentage >= 0 and percentage < 80:
262 return 'red'
263 if percentage >= 80 and percentage < 100:
264 return 'yellow'
265 if percentage == 100:
266 return 'green'
267
268 assert False, 'Invalid coverage percentage: "%d"' % percentage
269
Yuke Liaodd1ec0592018-02-02 01:26:37270 def WriteHtmlCoverageReport(self):
271 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33272
273 In the report, sub-directories are displayed before files and within each
274 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33275 """
276
277 def EntryCmp(left, right):
278 """Compare function for table entries."""
279 if left['is_dir'] != right['is_dir']:
280 return -1 if left['is_dir'] == True else 1
281
Yuke Liaodd1ec0592018-02-02 01:26:37282 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33283
284 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
285
286 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37287 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
288 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
289 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
290
Yuke Liaoea228d02018-01-05 19:10:33291 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37292 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
293 directory_view_href=_GetRelativePathToDirectoryOfFile(
294 directory_view_path, self._output_path),
295 component_view_href=_GetRelativePathToDirectoryOfFile(
296 component_view_path, self._output_path),
297 file_view_href=_GetRelativePathToDirectoryOfFile(
298 file_view_path, self._output_path))
299
Yuke Liaod54030e2018-01-08 17:34:12300 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37301 entries=self._table_entries,
302 total_entry=self._total_entry,
303 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33304 html_footer = self._footer_template.render()
305
Yuke Liaodd1ec0592018-02-02 01:26:37306 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33307 html_file.write(html_header + html_table + html_footer)
308
Yuke Liao506e8822017-12-04 16:52:54309
Yuke Liaoc60b2d02018-03-02 21:40:43310def _GetHostPlatform():
311 """Returns the host platform.
312
313 This is separate from the target platform/os that coverage is running for.
314 """
Abhishek Arya1ec832c2017-12-05 18:06:59315 if sys.platform == 'win32' or sys.platform == 'cygwin':
316 return 'win'
317 if sys.platform.startswith('linux'):
318 return 'linux'
319 else:
320 assert sys.platform == 'darwin'
321 return 'mac'
322
323
Yuke Liaoc60b2d02018-03-02 21:40:43324def _GetTargetOS():
325 """Returns the target os specified in args.gn file.
326
327 Returns an empty string is target_os is not specified.
328 """
329 build_args = _ParseArgsGnFile()
330 return build_args['target_os'] if 'target_os' in build_args else ''
331
332
Yuke Liaob2926832018-03-02 17:34:29333def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10334 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43335 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10336
337
Yuke Liao506e8822017-12-04 16:52:54338# TODO(crbug.com/759794): remove this function once tools get included to
339# Clang bundle:
340# https://chromium-review.googlesource.com/c/chromium/src/+/688221
341def DownloadCoverageToolsIfNeeded():
342 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59343
Yuke Liaoc60b2d02018-03-02 21:40:43344 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54345 """Returns a pair of revision number by reading the build stamp file.
346
347 Args:
348 stamp_file_path: A path the build stamp file created by
349 tools/clang/scripts/update.py.
350 Returns:
351 A pair of integers represeting the main and sub revision respectively.
352 """
353 if not os.path.exists(stamp_file_path):
354 return 0, 0
355
356 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43357 stamp_file_line = stamp_file.readline()
358 if ',' in stamp_file_line:
359 package_version = stamp_file_line.rstrip().split(',')[0]
360 else:
361 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54362
Yuke Liaoc60b2d02018-03-02 21:40:43363 clang_revision_str, clang_sub_revision_str = package_version.split('-')
364 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59365
Yuke Liaoc60b2d02018-03-02 21:40:43366 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54367 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43368 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54369
370 coverage_revision_stamp_file = os.path.join(
371 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
372 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43373 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54374
Yuke Liaoea228d02018-01-05 19:10:33375 has_coverage_tools = (
376 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32377
Yuke Liaoea228d02018-01-05 19:10:33378 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54379 coverage_sub_revision == clang_sub_revision):
380 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43381 return
Yuke Liao506e8822017-12-04 16:52:54382
383 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
384 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
385
386 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43387 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54388 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43389 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54390 coverage_tools_url = (
391 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43392 else:
393 assert host_platform == 'win'
394 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54395
396 try:
397 clang_update.DownloadAndUnpack(coverage_tools_url,
398 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10399 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54400 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43401 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54402 file_handle.write('\n')
403 except urllib2.URLError:
404 raise Exception(
405 'Failed to download coverage tools: %s.' % coverage_tools_url)
406
407
Yuke Liaodd1ec0592018-02-02 01:26:37408def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
409 filters):
Yuke Liao506e8822017-12-04 16:52:54410 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
411
412 For a file with absolute path /a/b/x.cc, a html report is generated as:
413 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
414 OUTPUT_DIR/index.html.
415
416 Args:
417 binary_paths: A list of paths to the instrumented binaries.
418 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42419 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54420 """
Yuke Liao506e8822017-12-04 16:52:54421 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
422 # [[-object BIN]] [SOURCES]
423 # NOTE: For object files, the first one is specified as a positional argument,
424 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10425 logging.debug('Generating per file line by line coverage reports using '
426 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59427 subprocess_cmd = [
428 LLVM_COV_PATH, 'show', '-format=html',
429 '-output-dir={}'.format(OUTPUT_DIR),
430 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
431 ]
432 subprocess_cmd.extend(
433 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29434 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42435 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54436 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10437 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54438
439
Yuke Liaodd1ec0592018-02-02 01:26:37440def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
441 """Generates html index file for file view."""
442 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
443 logging.debug('Generating file view html index file as: "%s".',
444 file_view_index_file_path)
445 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
446 'Path')
447 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33448
Yuke Liaodd1ec0592018-02-02 01:26:37449 for file_path in per_file_coverage_summary:
450 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
451
452 html_generator.AddLinkToAnotherReport(
453 _GetCoverageHtmlReportPathForFile(file_path),
454 os.path.relpath(file_path, SRC_ROOT_PATH),
455 per_file_coverage_summary[file_path])
456
457 html_generator.CreateTotalsEntry(totals_coverage_summary)
458 html_generator.WriteHtmlCoverageReport()
459 logging.debug('Finished generating file view html index file.')
460
461
462def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
463 """Calculates per directory coverage summary."""
464 logging.debug('Calculating per-directory coverage summary')
465 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
466
Yuke Liaoea228d02018-01-05 19:10:33467 for file_path in per_file_coverage_summary:
468 summary = per_file_coverage_summary[file_path]
469 parent_dir = os.path.dirname(file_path)
470 while True:
471 per_directory_coverage_summary[parent_dir].AddSummary(summary)
472
473 if parent_dir == SRC_ROOT_PATH:
474 break
475 parent_dir = os.path.dirname(parent_dir)
476
Yuke Liaodd1ec0592018-02-02 01:26:37477 logging.debug('Finished calculating per-directory coverage summary')
478 return per_directory_coverage_summary
479
480
481def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
482 per_file_coverage_summary):
483 """Generates per directory coverage breakdown in html."""
484 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33485 for dir_path in per_directory_coverage_summary:
486 _GenerateCoverageInHtmlForDirectory(
487 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
488
Yuke Liaodd1ec0592018-02-02 01:26:37489 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10490
Yuke Liaoea228d02018-01-05 19:10:33491
492def _GenerateCoverageInHtmlForDirectory(
493 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
494 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37495 html_generator = _CoverageReportHtmlGenerator(
496 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33497
498 for entry_name in os.listdir(dir_path):
499 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33500
Yuke Liaodd1ec0592018-02-02 01:26:37501 if entry_path in per_file_coverage_summary:
502 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
503 entry_coverage_summary = per_file_coverage_summary[entry_path]
504 elif entry_path in per_directory_coverage_summary:
505 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
506 entry_path)
507 entry_coverage_summary = per_directory_coverage_summary[entry_path]
508 else:
Yuke Liaoc7e607142018-02-05 20:26:14509 # Any file without executable lines shouldn't be included into the report.
510 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37511 continue
Yuke Liaoea228d02018-01-05 19:10:33512
Yuke Liaodd1ec0592018-02-02 01:26:37513 html_generator.AddLinkToAnotherReport(entry_html_report_path,
514 os.path.basename(entry_path),
515 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33516
Yuke Liaod54030e2018-01-08 17:34:12517 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37518 html_generator.WriteHtmlCoverageReport()
519
520
521def _GenerateDirectoryViewHtmlIndexFile():
522 """Generates the html index file for directory view.
523
524 Note that the index file is already generated under SRC_ROOT_PATH, so this
525 file simply redirects to it, and the reason of this extra layer is for
526 structural consistency with other views.
527 """
528 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
529 DIRECTORY_VIEW_INDEX_FILE)
530 logging.debug('Generating directory view html index file as: "%s".',
531 directory_view_index_file_path)
532 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
533 SRC_ROOT_PATH)
534 _WriteRedirectHtmlFile(directory_view_index_file_path,
535 src_root_html_report_path)
536 logging.debug('Finished generating directory view html index file.')
537
538
539def _CalculatePerComponentCoverageSummary(component_to_directories,
540 per_directory_coverage_summary):
541 """Calculates per component coverage summary."""
542 logging.debug('Calculating per-component coverage summary')
543 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
544
545 for component in component_to_directories:
546 for directory in component_to_directories[component]:
547 absolute_directory_path = os.path.abspath(directory)
548 if absolute_directory_path in per_directory_coverage_summary:
549 per_component_coverage_summary[component].AddSummary(
550 per_directory_coverage_summary[absolute_directory_path])
551
552 logging.debug('Finished calculating per-component coverage summary')
553 return per_component_coverage_summary
554
555
556def _ExtractComponentToDirectoriesMapping():
557 """Returns a mapping from components to directories."""
558 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
559 directory_to_component = component_mappings['dir-to-component']
560
561 component_to_directories = defaultdict(list)
562 for directory in directory_to_component:
563 component = directory_to_component[directory]
564 component_to_directories[component].append(directory)
565
566 return component_to_directories
567
568
569def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
570 component_to_directories,
571 per_directory_coverage_summary):
572 """Generates per-component coverage reports in html."""
573 logging.debug('Writing per-component coverage html reports.')
574 for component in per_component_coverage_summary:
575 _GenerateCoverageInHtmlForComponent(
576 component, per_component_coverage_summary, component_to_directories,
577 per_directory_coverage_summary)
578
579 logging.debug('Finished writing per-component coverage html reports.')
580
581
582def _GenerateCoverageInHtmlForComponent(
583 component_name, per_component_coverage_summary, component_to_directories,
584 per_directory_coverage_summary):
585 """Generates coverage html report for a component."""
586 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
587 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14588 component_html_report_dir = os.path.dirname(component_html_report_path)
589 if not os.path.exists(component_html_report_dir):
590 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37591
592 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
593 'Path')
594
595 for dir_path in component_to_directories[component_name]:
596 dir_absolute_path = os.path.abspath(dir_path)
597 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14598 # Any directory without an excercised file shouldn't be included into the
599 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37600 continue
601
602 html_generator.AddLinkToAnotherReport(
603 _GetCoverageHtmlReportPathForDirectory(dir_path),
604 os.path.relpath(dir_path, SRC_ROOT_PATH),
605 per_directory_coverage_summary[dir_absolute_path])
606
607 html_generator.CreateTotalsEntry(
608 per_component_coverage_summary[component_name])
609 html_generator.WriteHtmlCoverageReport()
610
611
612def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
613 """Generates the html index file for component view."""
614 component_view_index_file_path = os.path.join(OUTPUT_DIR,
615 COMPONENT_VIEW_INDEX_FILE)
616 logging.debug('Generating component view html index file as: "%s".',
617 component_view_index_file_path)
618 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
619 'Component')
620 totals_coverage_summary = _CoverageSummary()
621
622 for component in per_component_coverage_summary:
623 totals_coverage_summary.AddSummary(
624 per_component_coverage_summary[component])
625
626 html_generator.AddLinkToAnotherReport(
627 _GetCoverageHtmlReportPathForComponent(component), component,
628 per_component_coverage_summary[component])
629
630 html_generator.CreateTotalsEntry(totals_coverage_summary)
631 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14632 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33633
634
635def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37636 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33637 html_index_file_path = os.path.join(OUTPUT_DIR,
638 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37639 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
640 DIRECTORY_VIEW_INDEX_FILE)
641 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
642
643
644def _WriteRedirectHtmlFile(from_html_path, to_html_path):
645 """Writes a html file that redirects to another html file."""
646 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
647 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33648 content = ("""
649 <!DOCTYPE html>
650 <html>
651 <head>
652 <!-- HTML meta refresh URL redirection -->
653 <meta http-equiv="refresh" content="0; url=%s">
654 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37655 </html>""" % to_html_relative_path)
656 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33657 f.write(content)
658
659
Yuke Liaodd1ec0592018-02-02 01:26:37660def _GetCoverageHtmlReportPathForFile(file_path):
661 """Given a file path, returns the corresponding html report path."""
662 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
663 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
664
665 # '+' is used instead of os.path.join because both of them are absolute paths
666 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14667 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37668 return _GetCoverageReportRootDirPath() + html_report_path
669
670
671def _GetCoverageHtmlReportPathForDirectory(dir_path):
672 """Given a directory path, returns the corresponding html report path."""
673 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
674 html_report_path = os.path.join(
675 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
676
677 # '+' is used instead of os.path.join because both of them are absolute paths
678 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14679 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37680 return _GetCoverageReportRootDirPath() + html_report_path
681
682
683def _GetCoverageHtmlReportPathForComponent(component_name):
684 """Given a component, returns the corresponding html report path."""
685 component_file_name = component_name.lower().replace('>', '-')
686 html_report_name = os.extsep.join([component_file_name, 'html'])
687 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
688 html_report_name)
689
690
691def _GetCoverageReportRootDirPath():
692 """The root directory that contains all generated coverage html reports."""
693 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33694
695
Yuke Liao506e8822017-12-04 16:52:54696def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
697 """Builds and runs target to generate the coverage profile data.
698
699 Args:
700 targets: A list of targets to build with coverage instrumentation.
701 commands: A list of commands used to run the targets.
702 jobs_count: Number of jobs to run in parallel for building. If None, a
703 default value is derived based on CPUs availability.
704
705 Returns:
706 A relative path to the generated profdata file.
707 """
708 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59709 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
710 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54711 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
712 profraw_file_paths)
713
Yuke Liaod4a9865202018-01-12 23:17:52714 for profraw_file_path in profraw_file_paths:
715 os.remove(profraw_file_path)
716
Yuke Liao506e8822017-12-04 16:52:54717 return profdata_file_path
718
719
720def _BuildTargets(targets, jobs_count):
721 """Builds target with Clang coverage instrumentation.
722
723 This function requires current working directory to be the root of checkout.
724
725 Args:
726 targets: A list of targets to build with coverage instrumentation.
727 jobs_count: Number of jobs to run in parallel for compilation. If None, a
728 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54729 """
Abhishek Arya1ec832c2017-12-05 18:06:59730
Yuke Liao506e8822017-12-04 16:52:54731 def _IsGomaConfigured():
732 """Returns True if goma is enabled in the gn build args.
733
734 Returns:
735 A boolean indicates whether goma is configured for building or not.
736 """
737 build_args = _ParseArgsGnFile()
738 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
739
Yuke Liao481d3482018-01-29 19:17:10740 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54741 if jobs_count is None and _IsGomaConfigured():
742 jobs_count = DEFAULT_GOMA_JOBS
743
744 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
745 if jobs_count is not None:
746 subprocess_cmd.append('-j' + str(jobs_count))
747
748 subprocess_cmd.extend(targets)
749 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10750 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54751
752
753def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
754 """Runs commands and returns the relative paths to the profraw data files.
755
756 Args:
757 targets: A list of targets built with coverage instrumentation.
758 commands: A list of commands used to run the targets.
759
760 Returns:
761 A list of relative paths to the generated profraw data files.
762 """
Yuke Liao481d3482018-01-29 19:17:10763 logging.debug('Executing the test commands')
764
Yuke Liao506e8822017-12-04 16:52:54765 # Remove existing profraw data files.
766 for file_or_dir in os.listdir(OUTPUT_DIR):
767 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
768 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
769
Yuke Liaoa0c8c2f2018-02-28 20:14:10770 profraw_file_paths = []
771
Yuke Liaod4a9865202018-01-12 23:17:52772 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54773 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10774 output_file_name = os.extsep.join([target + '_output', 'txt'])
775 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
776 logging.info('Running command: "%s", the output is redirected to "%s"',
777 command, output_file_path)
778
Yuke Liaob2926832018-03-02 17:34:29779 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10780 # On iOS platform, due to lack of write permissions, profraw files are
781 # generated outside of the OUTPUT_DIR, and the exact paths are contained
782 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29783 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10784 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
785 else:
786 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
787 output = _ExecuteCommand(target, command)
788
789 with open(output_file_path, 'w') as output_file:
790 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54791
Yuke Liao481d3482018-01-29 19:17:10792 logging.debug('Finished executing the test commands')
793
Yuke Liaob2926832018-03-02 17:34:29794 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10795 return profraw_file_paths
796
Yuke Liao506e8822017-12-04 16:52:54797 for file_or_dir in os.listdir(OUTPUT_DIR):
798 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
799 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
800
801 # Assert one target/command generates at least one profraw data file.
802 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59803 assert any(
804 os.path.basename(profraw_file).startswith(target)
805 for profraw_file in profraw_file_paths), (
806 'Running target: %s failed to generate any profraw data file, '
807 'please make sure the binary exists and is properly instrumented.' %
808 target)
Yuke Liao506e8822017-12-04 16:52:54809
810 return profraw_file_paths
811
812
813def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10814 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52815 # Per Clang "Source-based Code Coverage" doc:
816 # "%Nm" expands out to the instrumented binary's signature. When this pattern
817 # is specified, the runtime creates a pool of N raw profiles which are used
818 # for on-line profile merging. The runtime takes care of selecting a raw
819 # profile from the pool, locking it, and updating it before the program exits.
820 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
821 # N must be between 1 and 9. The merge pool specifier can only occur once per
822 # filename pattern.
823 #
824 # 4 is chosen because it creates some level of parallelism, but it's not too
825 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59826 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52827 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54828 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
829 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54830
Yuke Liaoa0c8c2f2018-02-28 20:14:10831 try:
832 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29833 shlex.split(command),
834 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10835 except subprocess.CalledProcessError as e:
836 output = e.output
837 logging.warning('Command: "%s" exited with non-zero return code', command)
838
839 return output
840
841
Yuke Liaob2926832018-03-02 17:34:29842def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10843 """Runs a single iOS command and generates a profraw data file.
844
845 iOS application doesn't have write access to folders outside of the app, so
846 it's impossible to instruct the app to flush the profraw data file to the
847 desired location. The profraw data file will be generated somewhere within the
848 application's Documents folder, and the full path can be obtained by parsing
849 the output.
850 """
Yuke Liaob2926832018-03-02 17:34:29851 assert _IsIOSCommand(command)
852
853 # After running tests, iossim generates a profraw data file, it won't be
854 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
855 # checkout.
856 iossim_profraw_file_path = os.path.join(
857 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10858
859 try:
Yuke Liaob2926832018-03-02 17:34:29860 output = subprocess.check_output(
861 shlex.split(command),
862 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10863 except subprocess.CalledProcessError as e:
864 # iossim emits non-zero return code even if tests run successfully, so
865 # ignore the return code.
866 output = e.output
867
868 return output
869
870
871def _GetProfrawDataFileByParsingOutput(output):
872 """Returns the path to the profraw data file obtained by parsing the output.
873
874 The output of running the test target has no format, but it is guaranteed to
875 have a single line containing the path to the generated profraw data file.
876 NOTE: This should only be called when target os is iOS.
877 """
Yuke Liaob2926832018-03-02 17:34:29878 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10879
Yuke Liaob2926832018-03-02 17:34:29880 output_by_lines = ''.join(output).splitlines()
881 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10882
883 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29884 result = profraw_file_pattern.match(line)
885 if result:
886 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10887
888 assert False, ('No profraw data file was generated, did you call '
889 'coverage_util::ConfigureCoverageReportPath() in test setup? '
890 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54891
892
893def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
894 """Returns a relative path to the profdata file by merging profraw data files.
895
896 Args:
897 profraw_file_paths: A list of relative paths to the profraw data files that
898 are to be merged.
899
900 Returns:
901 A relative path to the generated profdata file.
902
903 Raises:
904 CalledProcessError: An error occurred merging profraw data files.
905 """
Yuke Liao481d3482018-01-29 19:17:10906 logging.info('Creating the coverage profile data file')
907 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54908 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
909 try:
Abhishek Arya1ec832c2017-12-05 18:06:59910 subprocess_cmd = [
911 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
912 ]
Yuke Liao506e8822017-12-04 16:52:54913 subprocess_cmd.extend(profraw_file_paths)
914 subprocess.check_call(subprocess_cmd)
915 except subprocess.CalledProcessError as error:
916 print('Failed to merge profraw files to create profdata file')
917 raise error
918
Yuke Liao481d3482018-01-29 19:17:10919 logging.debug('Finished merging profraw files')
920 logging.info('Code coverage profile data is created as: %s',
921 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54922 return profdata_file_path
923
924
Yuke Liaoea228d02018-01-05 19:10:33925def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
926 """Generates per file coverage summary using "llvm-cov export" command."""
927 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
928 # [[-object BIN]] [SOURCES].
929 # NOTE: For object files, the first one is specified as a positional argument,
930 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10931 logging.debug('Generating per-file code coverage summary using "llvm-cov '
932 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33933 subprocess_cmd = [
934 LLVM_COV_PATH, 'export', '-summary-only',
935 '-instr-profile=' + profdata_file_path, binary_paths[0]
936 ]
937 subprocess_cmd.extend(
938 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29939 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33940 subprocess_cmd.extend(filters)
941
942 json_output = json.loads(subprocess.check_output(subprocess_cmd))
943 assert len(json_output['data']) == 1
944 files_coverage_data = json_output['data'][0]['files']
945
946 per_file_coverage_summary = {}
947 for file_coverage_data in files_coverage_data:
948 file_path = file_coverage_data['filename']
949 summary = file_coverage_data['summary']
950
Yuke Liaoea228d02018-01-05 19:10:33951 if summary['lines']['count'] == 0:
952 continue
953
954 per_file_coverage_summary[file_path] = _CoverageSummary(
955 regions_total=summary['regions']['count'],
956 regions_covered=summary['regions']['covered'],
957 functions_total=summary['functions']['count'],
958 functions_covered=summary['functions']['covered'],
959 lines_total=summary['lines']['count'],
960 lines_covered=summary['lines']['covered'])
961
Yuke Liao481d3482018-01-29 19:17:10962 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33963 return per_file_coverage_summary
964
965
Yuke Liaob2926832018-03-02 17:34:29966def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
967 """Appends -arch arguments to the command list if it's ios platform.
968
969 iOS binaries are universal binaries, and require specifying the architecture
970 to use, and one architecture needs to be specified for each binary.
971 """
972 if _IsIOS():
973 cmd_list.extend(['-arch=x86_64'] * num_archs)
974
975
Yuke Liao506e8822017-12-04 16:52:54976def _GetBinaryPath(command):
977 """Returns a relative path to the binary to be run by the command.
978
Yuke Liao545db322018-02-15 17:12:01979 Currently, following types of commands are supported (e.g. url_unittests):
980 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
981 2. Use xvfb.
982 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
983 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liaoa0c8c2f2018-02-28 20:14:10984 3. Use iossim to run tests on iOS platform.
985 3.1. "out/Coverage-iphonesimulator/iossim
986 out/Coverage-iphonesimulator/url_unittests.app <arguments>"
Yuke Liao545db322018-02-15 17:12:01987
Yuke Liao506e8822017-12-04 16:52:54988 Args:
989 command: A command used to run a target.
990
991 Returns:
992 A relative path to the binary.
993 """
Yuke Liao545db322018-02-15 17:12:01994 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
995
Yuke Liaob2926832018-03-02 17:34:29996 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:01997 if os.path.basename(command_parts[0]) == 'python':
998 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
999 'This tool doesn\'t understand the command: "%s"' % command)
1000 return command_parts[2]
1001
1002 if os.path.basename(command_parts[0]) == xvfb_script_name:
1003 return command_parts[1]
1004
Yuke Liaob2926832018-03-02 17:34:291005 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101006 # For a given application bundle, the binary resides in the bundle and has
1007 # the same name with the application without the .app extension.
1008 app_path = command_parts[1]
1009 app_name = os.path.splitext(os.path.basename(app_path))[0]
1010 return os.path.join(app_path, app_name)
1011
Yuke Liaob2926832018-03-02 17:34:291012 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541013
1014
Yuke Liaob2926832018-03-02 17:34:291015def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101016 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291017 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101018
1019
Yuke Liao95d13d72017-12-07 18:18:501020def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1021 """Verifies that the target executables specified in the commands are inside
1022 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541023 for command in commands:
1024 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501025 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1026 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1027 'Target executable "%s" in command: "%s" is outside of '
1028 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541029
1030
1031def _ValidateBuildingWithClangCoverage():
1032 """Asserts that targets are built with Clang coverage enabled."""
1033 build_args = _ParseArgsGnFile()
1034
1035 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1036 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591037 assert False, ('\'{} = true\' is required in args.gn.'
1038 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541039
1040
Yuke Liaoc60b2d02018-03-02 21:40:431041def _ValidateCurrentPlatformIsSupported():
1042 """Asserts that this script suports running on the current platform"""
1043 target_os = _GetTargetOS()
1044 if target_os:
1045 current_platform = target_os
1046 else:
1047 current_platform = _GetHostPlatform()
1048
1049 assert current_platform in [
1050 'linux', 'mac', 'chromeos', 'ios'
1051 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1052
1053
Yuke Liao506e8822017-12-04 16:52:541054def _ParseArgsGnFile():
1055 """Parses args.gn file and returns results as a dictionary.
1056
1057 Returns:
1058 A dictionary representing the build args.
1059 """
1060 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1061 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1062 'missing args.gn file.' % BUILD_DIR)
1063 with open(build_args_path) as build_args_file:
1064 build_args_lines = build_args_file.readlines()
1065
1066 build_args = {}
1067 for build_arg_line in build_args_lines:
1068 build_arg_without_comments = build_arg_line.split('#')[0]
1069 key_value_pair = build_arg_without_comments.split('=')
1070 if len(key_value_pair) != 2:
1071 continue
1072
1073 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431074
1075 # Values are wrapped within a pair of double-quotes, so remove the leading
1076 # and trailing double-quotes.
1077 value = key_value_pair[1].strip().strip('"')
Yuke Liao506e8822017-12-04 16:52:541078 build_args[key] = value
1079
1080 return build_args
1081
1082
Abhishek Arya16f059a2017-12-07 17:47:321083def _VerifyPathsAndReturnAbsolutes(paths):
1084 """Verifies that the paths specified in |paths| exist and returns absolute
1085 versions.
Yuke Liao66da1732017-12-05 22:19:421086
1087 Args:
1088 paths: A list of files or directories.
1089 """
Abhishek Arya16f059a2017-12-07 17:47:321090 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421091 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321092 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1093 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1094
1095 absolute_paths.append(absolute_path)
1096
1097 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421098
1099
Yuke Liaodd1ec0592018-02-02 01:26:371100def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1101 """Returns a target path relative to the directory of base_path.
1102
1103 This method requires base_path to be a file, otherwise, one should call
1104 os.path.relpath directly.
1105 """
1106 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141107 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371108 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141109 base_dir = os.path.dirname(base_path)
1110 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371111
1112
Yuke Liao506e8822017-12-04 16:52:541113def _ParseCommandArguments():
1114 """Adds and parses relevant arguments for tool comands.
1115
1116 Returns:
1117 A dictionary representing the arguments.
1118 """
1119 arg_parser = argparse.ArgumentParser()
1120 arg_parser.usage = __doc__
1121
Abhishek Arya1ec832c2017-12-05 18:06:591122 arg_parser.add_argument(
1123 '-b',
1124 '--build-dir',
1125 type=str,
1126 required=True,
1127 help='The build directory, the path needs to be relative to the root of '
1128 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541129
Abhishek Arya1ec832c2017-12-05 18:06:591130 arg_parser.add_argument(
1131 '-o',
1132 '--output-dir',
1133 type=str,
1134 required=True,
1135 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541136
Abhishek Arya1ec832c2017-12-05 18:06:591137 arg_parser.add_argument(
1138 '-c',
1139 '--command',
1140 action='append',
1141 required=True,
1142 help='Commands used to run test targets, one test target needs one and '
1143 'only one command, when specifying commands, one should assume the '
1144 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541145
Abhishek Arya1ec832c2017-12-05 18:06:591146 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421147 '-f',
1148 '--filters',
1149 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321150 required=False,
Yuke Liao66da1732017-12-05 22:19:421151 help='Directories or files to get code coverage for, and all files under '
1152 'the directories are included recursively.')
1153
1154 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591155 '-j',
1156 '--jobs',
1157 type=int,
1158 default=None,
1159 help='Run N jobs to build in parallel. If not specified, a default value '
1160 'will be derived based on CPUs availability. Please refer to '
1161 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541162
Abhishek Arya1ec832c2017-12-05 18:06:591163 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101164 '-v',
1165 '--verbose',
1166 action='store_true',
1167 help='Prints additional output for diagnostics.')
1168
1169 arg_parser.add_argument(
1170 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1171
1172 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591173 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541174
1175 args = arg_parser.parse_args()
1176 return args
1177
1178
1179def Main():
1180 """Execute tool commands."""
1181 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1182 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591183 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541184 args = _ParseCommandArguments()
1185 global BUILD_DIR
1186 BUILD_DIR = args.build_dir
1187 global OUTPUT_DIR
1188 OUTPUT_DIR = args.output_dir
1189
1190 assert len(args.targets) == len(args.command), ('Number of targets must be '
1191 'equal to the number of test '
1192 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431193
1194 # logging should be configured before it is used.
1195 log_level = logging.DEBUG if args.verbose else logging.INFO
1196 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1197 log_file = args.log_file if args.log_file else None
1198 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1199
Abhishek Arya1ec832c2017-12-05 18:06:591200 assert os.path.exists(BUILD_DIR), (
1201 'Build directory: {} doesn\'t exist. '
1202 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431203 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541204 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501205 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321206
Yuke Liaoc60b2d02018-03-02 21:40:431207 DownloadCoverageToolsIfNeeded()
1208
Abhishek Arya16f059a2017-12-07 17:47:321209 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421210 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321211 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421212
Yuke Liao506e8822017-12-04 16:52:541213 if not os.path.exists(OUTPUT_DIR):
1214 os.makedirs(OUTPUT_DIR)
1215
Abhishek Arya1ec832c2017-12-05 18:06:591216 profdata_file_path = _CreateCoverageProfileDataForTargets(
1217 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541218 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331219
Yuke Liao481d3482018-01-29 19:17:101220 logging.info('Generating code coverage report in html (this can take a while '
1221 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371222 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1223 binary_paths, profdata_file_path, absolute_filter_paths)
1224 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1225 absolute_filter_paths)
1226 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1227
1228 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1229 per_file_coverage_summary)
1230 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1231 per_file_coverage_summary)
1232 _GenerateDirectoryViewHtmlIndexFile()
1233
1234 component_to_directories = _ExtractComponentToDirectoriesMapping()
1235 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1236 component_to_directories, per_directory_coverage_summary)
1237 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1238 component_to_directories,
1239 per_directory_coverage_summary)
1240 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331241
1242 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371243 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331244 _OverwriteHtmlReportsIndexFile()
1245
Yuke Liao506e8822017-12-04 16:52:541246 html_index_file_path = 'file://' + os.path.abspath(
1247 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101248 logging.info('Index file for html report is generated as: %s',
1249 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541250
Abhishek Arya1ec832c2017-12-05 18:06:591251
Yuke Liao506e8822017-12-04 16:52:541252if __name__ == '__main__':
1253 sys.exit(Main())