blob: 69fc11ebc6265563eadeb3114317739a1c842751 [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
67import subprocess
Yuke Liao506e8822017-12-04 16:52:5468import urllib2
69
Abhishek Arya1ec832c2017-12-05 18:06:5970sys.path.append(
71 os.path.join(
72 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
73 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5474import update as clang_update
75
Yuke Liaoea228d02018-01-05 19:10:3376sys.path.append(
77 os.path.join(
78 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
79 'third_party'))
80import jinja2
81from collections import defaultdict
82
Yuke Liao506e8822017-12-04 16:52:5483# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5984SRC_ROOT_PATH = os.path.abspath(
85 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5486
87# Absolute path to the code coverage tools binary.
88LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
89LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
90LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
91
92# Build directory, the value is parsed from command line arguments.
93BUILD_DIR = None
94
95# Output directory for generated artifacts, the value is parsed from command
96# line arguemnts.
97OUTPUT_DIR = None
98
99# Default number of jobs used to build when goma is configured and enabled.
100DEFAULT_GOMA_JOBS = 100
101
102# Name of the file extension for profraw data files.
103PROFRAW_FILE_EXTENSION = 'profraw'
104
105# Name of the final profdata file, and this file needs to be passed to
106# "llvm-cov" command in order to call "llvm-cov show" to inspect the
107# line-by-line coverage of specific files.
108PROFDATA_FILE_NAME = 'coverage.profdata'
109
110# Build arg required for generating code coverage data.
111CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
112
Yuke Liaoea228d02018-01-05 19:10:33113# The default name of the html coverage report for a directory.
114DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
115
Yuke Liaodd1ec0592018-02-02 01:26:37116# Name of the html index files for different views.
117DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
118COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
119FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
120
121# Used to extract a mapping between directories and components.
122COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
123
Yuke Liaoea228d02018-01-05 19:10:33124
125class _CoverageSummary(object):
126 """Encapsulates coverage summary representation."""
127
Yuke Liaodd1ec0592018-02-02 01:26:37128 def __init__(self,
129 regions_total=0,
130 regions_covered=0,
131 functions_total=0,
132 functions_covered=0,
133 lines_total=0,
134 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33135 """Initializes _CoverageSummary object."""
136 self._summary = {
137 'regions': {
138 'total': regions_total,
139 'covered': regions_covered
140 },
141 'functions': {
142 'total': functions_total,
143 'covered': functions_covered
144 },
145 'lines': {
146 'total': lines_total,
147 'covered': lines_covered
148 }
149 }
150
151 def Get(self):
152 """Returns summary as a dictionary."""
153 return self._summary
154
155 def AddSummary(self, other_summary):
156 """Adds another summary to this one element-wise."""
157 for feature in self._summary:
158 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
159 self._summary[feature]['covered'] += other_summary.Get()[feature][
160 'covered']
161
162
Yuke Liaodd1ec0592018-02-02 01:26:37163class _CoverageReportHtmlGenerator(object):
164 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33165
Yuke Liaodd1ec0592018-02-02 01:26:37166 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33167 """
168
Yuke Liaodd1ec0592018-02-02 01:26:37169 def __init__(self, output_path, table_entry_type):
170 """Initializes _CoverageReportHtmlGenerator object.
171
172 Args:
173 output_path: Path to the html report that will be generated.
174 table_entry_type: Type of the table entries to be displayed in the table
175 header. For example: 'Path', 'Component'.
176 """
Yuke Liaoea228d02018-01-05 19:10:33177 css_file_name = os.extsep.join(['style', 'css'])
178 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
179 assert os.path.exists(css_absolute_path), (
180 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
181 'is called first, and the css file is generated at: "%s"' %
182 css_absolute_path)
183
184 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37185 self._output_path = output_path
186 self._table_entry_type = table_entry_type
187
Yuke Liaoea228d02018-01-05 19:10:33188 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12189 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33190 template_dir = os.path.join(
191 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
192
193 jinja_env = jinja2.Environment(
194 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
195 self._header_template = jinja_env.get_template('header.html')
196 self._table_template = jinja_env.get_template('table.html')
197 self._footer_template = jinja_env.get_template('footer.html')
198
199 def AddLinkToAnotherReport(self, html_report_path, name, summary):
200 """Adds a link to another html report in this report.
201
202 The link to be added is assumed to be an entry in this directory.
203 """
Yuke Liaodd1ec0592018-02-02 01:26:37204 # Use relative paths instead of absolute paths to make the generated reports
205 # portable.
206 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
207 html_report_path, self._output_path)
208
Yuke Liaod54030e2018-01-08 17:34:12209 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37210 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12211 os.path.basename(html_report_path) ==
212 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
213 self._table_entries.append(table_entry)
214
215 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35216 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12217 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
218
219 def _CreateTableEntryFromCoverageSummary(self,
220 summary,
221 href=None,
222 name=None,
223 is_dir=None):
224 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37225 assert (href is None and name is None and is_dir is None) or (
226 href is not None and name is not None and is_dir is not None), (
227 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35228 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37229 'attributes must be None.')
230
Yuke Liaod54030e2018-01-08 17:34:12231 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37232 if href is not None:
233 entry['href'] = href
234 if name is not None:
235 entry['name'] = name
236 if is_dir is not None:
237 entry['is_dir'] = is_dir
238
Yuke Liaoea228d02018-01-05 19:10:33239 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12240 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37241 if summary_dict[feature]['total'] == 0:
242 percentage = 0.0
243 else:
Yuke Liaoa785f4d32018-02-13 21:41:35244 percentage = float(summary_dict[feature]['covered']) / summary_dict[
245 feature]['total'] * 100
246
Yuke Liaoea228d02018-01-05 19:10:33247 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12248 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33249 'total': summary_dict[feature]['total'],
250 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35251 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33252 'color_class': color_class
253 }
Yuke Liaod54030e2018-01-08 17:34:12254
Yuke Liaod54030e2018-01-08 17:34:12255 return entry
Yuke Liaoea228d02018-01-05 19:10:33256
257 def _GetColorClass(self, percentage):
258 """Returns the css color class based on coverage percentage."""
259 if percentage >= 0 and percentage < 80:
260 return 'red'
261 if percentage >= 80 and percentage < 100:
262 return 'yellow'
263 if percentage == 100:
264 return 'green'
265
266 assert False, 'Invalid coverage percentage: "%d"' % percentage
267
Yuke Liaodd1ec0592018-02-02 01:26:37268 def WriteHtmlCoverageReport(self):
269 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33270
271 In the report, sub-directories are displayed before files and within each
272 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33273 """
274
275 def EntryCmp(left, right):
276 """Compare function for table entries."""
277 if left['is_dir'] != right['is_dir']:
278 return -1 if left['is_dir'] == True else 1
279
Yuke Liaodd1ec0592018-02-02 01:26:37280 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33281
282 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
283
284 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37285 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
286 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
287 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
288
Yuke Liaoea228d02018-01-05 19:10:33289 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37290 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
291 directory_view_href=_GetRelativePathToDirectoryOfFile(
292 directory_view_path, self._output_path),
293 component_view_href=_GetRelativePathToDirectoryOfFile(
294 component_view_path, self._output_path),
295 file_view_href=_GetRelativePathToDirectoryOfFile(
296 file_view_path, self._output_path))
297
Yuke Liaod54030e2018-01-08 17:34:12298 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37299 entries=self._table_entries,
300 total_entry=self._total_entry,
301 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33302 html_footer = self._footer_template.render()
303
Yuke Liaodd1ec0592018-02-02 01:26:37304 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33305 html_file.write(html_header + html_table + html_footer)
306
Yuke Liao506e8822017-12-04 16:52:54307
Abhishek Arya1ec832c2017-12-05 18:06:59308def _GetPlatform():
309 """Returns current running platform."""
310 if sys.platform == 'win32' or sys.platform == 'cygwin':
311 return 'win'
312 if sys.platform.startswith('linux'):
313 return 'linux'
314 else:
315 assert sys.platform == 'darwin'
316 return 'mac'
317
318
Yuke Liaoa0c8c2f2018-02-28 20:14:10319def _IsTargetOsIos():
320 """Returns true if the target_os specified in args.gn file is ios"""
321 build_args = _ParseArgsGnFile()
322 return 'target_os' in build_args and build_args['target_os'] == '"ios"'
323
324
Yuke Liao506e8822017-12-04 16:52:54325# TODO(crbug.com/759794): remove this function once tools get included to
326# Clang bundle:
327# https://chromium-review.googlesource.com/c/chromium/src/+/688221
328def DownloadCoverageToolsIfNeeded():
329 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59330
331 def _GetRevisionFromStampFile(stamp_file_path, platform):
Yuke Liao506e8822017-12-04 16:52:54332 """Returns a pair of revision number by reading the build stamp file.
333
334 Args:
335 stamp_file_path: A path the build stamp file created by
336 tools/clang/scripts/update.py.
337 Returns:
338 A pair of integers represeting the main and sub revision respectively.
339 """
340 if not os.path.exists(stamp_file_path):
341 return 0, 0
342
343 with open(stamp_file_path) as stamp_file:
Abhishek Arya1ec832c2017-12-05 18:06:59344 for stamp_file_line in stamp_file.readlines():
345 if ',' in stamp_file_line:
346 package_version, target_os = stamp_file_line.rstrip().split(',')
347 else:
348 package_version = stamp_file_line.rstrip()
349 target_os = ''
Yuke Liao506e8822017-12-04 16:52:54350
Yuke Liaoa0c8c2f2018-02-28 20:14:10351 if target_os and target_os != 'ios' and platform != target_os:
Abhishek Arya1ec832c2017-12-05 18:06:59352 continue
353
354 clang_revision_str, clang_sub_revision_str = package_version.split('-')
355 return int(clang_revision_str), int(clang_sub_revision_str)
356
Yuke Liaoa0c8c2f2018-02-28 20:14:10357 assert False, 'Coverage is only supported on target_os - linux, mac and ios'
Abhishek Arya1ec832c2017-12-05 18:06:59358
359 platform = _GetPlatform()
Yuke Liao506e8822017-12-04 16:52:54360 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59361 clang_update.STAMP_FILE, platform)
Yuke Liao506e8822017-12-04 16:52:54362
363 coverage_revision_stamp_file = os.path.join(
364 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
365 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59366 coverage_revision_stamp_file, platform)
Yuke Liao506e8822017-12-04 16:52:54367
Yuke Liaoea228d02018-01-05 19:10:33368 has_coverage_tools = (
369 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32370
Yuke Liaoea228d02018-01-05 19:10:33371 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54372 coverage_sub_revision == clang_sub_revision):
373 # LLVM coverage tools are up to date, bail out.
374 return clang_revision
375
376 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
377 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
378
379 # The code bellow follows the code from tools/clang/scripts/update.py.
Abhishek Arya1ec832c2017-12-05 18:06:59380 if platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54381 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
382 else:
Abhishek Arya1ec832c2017-12-05 18:06:59383 assert platform == 'linux'
Yuke Liao506e8822017-12-04 16:52:54384 coverage_tools_url = (
385 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
386
387 try:
388 clang_update.DownloadAndUnpack(coverage_tools_url,
389 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10390 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54391 with open(coverage_revision_stamp_file, 'w') as file_handle:
Abhishek Arya1ec832c2017-12-05 18:06:59392 file_handle.write('%s,%s' % (package_version, platform))
Yuke Liao506e8822017-12-04 16:52:54393 file_handle.write('\n')
394 except urllib2.URLError:
395 raise Exception(
396 'Failed to download coverage tools: %s.' % coverage_tools_url)
397
398
Yuke Liaodd1ec0592018-02-02 01:26:37399def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
400 filters):
Yuke Liao506e8822017-12-04 16:52:54401 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
402
403 For a file with absolute path /a/b/x.cc, a html report is generated as:
404 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
405 OUTPUT_DIR/index.html.
406
407 Args:
408 binary_paths: A list of paths to the instrumented binaries.
409 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42410 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54411 """
Yuke Liao506e8822017-12-04 16:52:54412 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
413 # [[-object BIN]] [SOURCES]
414 # NOTE: For object files, the first one is specified as a positional argument,
415 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10416 logging.debug('Generating per file line by line coverage reports using '
417 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59418 subprocess_cmd = [
419 LLVM_COV_PATH, 'show', '-format=html',
420 '-output-dir={}'.format(OUTPUT_DIR),
421 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
422 ]
423 subprocess_cmd.extend(
424 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaoa0c8c2f2018-02-28 20:14:10425 if _IsTargetOsIos():
426 # iOS binaries are universal binaries, and it requires specifying the
427 # architecture to use.
428 subprocess_cmd.append('-arch=x86_64')
429
Yuke Liao66da1732017-12-05 22:19:42430 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54431 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10432 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54433
434
Yuke Liaodd1ec0592018-02-02 01:26:37435def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
436 """Generates html index file for file view."""
437 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
438 logging.debug('Generating file view html index file as: "%s".',
439 file_view_index_file_path)
440 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
441 'Path')
442 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33443
Yuke Liaodd1ec0592018-02-02 01:26:37444 for file_path in per_file_coverage_summary:
445 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
446
447 html_generator.AddLinkToAnotherReport(
448 _GetCoverageHtmlReportPathForFile(file_path),
449 os.path.relpath(file_path, SRC_ROOT_PATH),
450 per_file_coverage_summary[file_path])
451
452 html_generator.CreateTotalsEntry(totals_coverage_summary)
453 html_generator.WriteHtmlCoverageReport()
454 logging.debug('Finished generating file view html index file.')
455
456
457def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
458 """Calculates per directory coverage summary."""
459 logging.debug('Calculating per-directory coverage summary')
460 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
461
Yuke Liaoea228d02018-01-05 19:10:33462 for file_path in per_file_coverage_summary:
463 summary = per_file_coverage_summary[file_path]
464 parent_dir = os.path.dirname(file_path)
465 while True:
466 per_directory_coverage_summary[parent_dir].AddSummary(summary)
467
468 if parent_dir == SRC_ROOT_PATH:
469 break
470 parent_dir = os.path.dirname(parent_dir)
471
Yuke Liaodd1ec0592018-02-02 01:26:37472 logging.debug('Finished calculating per-directory coverage summary')
473 return per_directory_coverage_summary
474
475
476def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
477 per_file_coverage_summary):
478 """Generates per directory coverage breakdown in html."""
479 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33480 for dir_path in per_directory_coverage_summary:
481 _GenerateCoverageInHtmlForDirectory(
482 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
483
Yuke Liaodd1ec0592018-02-02 01:26:37484 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10485
Yuke Liaoea228d02018-01-05 19:10:33486
487def _GenerateCoverageInHtmlForDirectory(
488 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
489 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37490 html_generator = _CoverageReportHtmlGenerator(
491 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33492
493 for entry_name in os.listdir(dir_path):
494 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33495
Yuke Liaodd1ec0592018-02-02 01:26:37496 if entry_path in per_file_coverage_summary:
497 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
498 entry_coverage_summary = per_file_coverage_summary[entry_path]
499 elif entry_path in per_directory_coverage_summary:
500 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
501 entry_path)
502 entry_coverage_summary = per_directory_coverage_summary[entry_path]
503 else:
Yuke Liaoc7e607142018-02-05 20:26:14504 # Any file without executable lines shouldn't be included into the report.
505 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37506 continue
Yuke Liaoea228d02018-01-05 19:10:33507
Yuke Liaodd1ec0592018-02-02 01:26:37508 html_generator.AddLinkToAnotherReport(entry_html_report_path,
509 os.path.basename(entry_path),
510 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33511
Yuke Liaod54030e2018-01-08 17:34:12512 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37513 html_generator.WriteHtmlCoverageReport()
514
515
516def _GenerateDirectoryViewHtmlIndexFile():
517 """Generates the html index file for directory view.
518
519 Note that the index file is already generated under SRC_ROOT_PATH, so this
520 file simply redirects to it, and the reason of this extra layer is for
521 structural consistency with other views.
522 """
523 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
524 DIRECTORY_VIEW_INDEX_FILE)
525 logging.debug('Generating directory view html index file as: "%s".',
526 directory_view_index_file_path)
527 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
528 SRC_ROOT_PATH)
529 _WriteRedirectHtmlFile(directory_view_index_file_path,
530 src_root_html_report_path)
531 logging.debug('Finished generating directory view html index file.')
532
533
534def _CalculatePerComponentCoverageSummary(component_to_directories,
535 per_directory_coverage_summary):
536 """Calculates per component coverage summary."""
537 logging.debug('Calculating per-component coverage summary')
538 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
539
540 for component in component_to_directories:
541 for directory in component_to_directories[component]:
542 absolute_directory_path = os.path.abspath(directory)
543 if absolute_directory_path in per_directory_coverage_summary:
544 per_component_coverage_summary[component].AddSummary(
545 per_directory_coverage_summary[absolute_directory_path])
546
547 logging.debug('Finished calculating per-component coverage summary')
548 return per_component_coverage_summary
549
550
551def _ExtractComponentToDirectoriesMapping():
552 """Returns a mapping from components to directories."""
553 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
554 directory_to_component = component_mappings['dir-to-component']
555
556 component_to_directories = defaultdict(list)
557 for directory in directory_to_component:
558 component = directory_to_component[directory]
559 component_to_directories[component].append(directory)
560
561 return component_to_directories
562
563
564def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
565 component_to_directories,
566 per_directory_coverage_summary):
567 """Generates per-component coverage reports in html."""
568 logging.debug('Writing per-component coverage html reports.')
569 for component in per_component_coverage_summary:
570 _GenerateCoverageInHtmlForComponent(
571 component, per_component_coverage_summary, component_to_directories,
572 per_directory_coverage_summary)
573
574 logging.debug('Finished writing per-component coverage html reports.')
575
576
577def _GenerateCoverageInHtmlForComponent(
578 component_name, per_component_coverage_summary, component_to_directories,
579 per_directory_coverage_summary):
580 """Generates coverage html report for a component."""
581 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
582 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14583 component_html_report_dir = os.path.dirname(component_html_report_path)
584 if not os.path.exists(component_html_report_dir):
585 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37586
587 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
588 'Path')
589
590 for dir_path in component_to_directories[component_name]:
591 dir_absolute_path = os.path.abspath(dir_path)
592 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14593 # Any directory without an excercised file shouldn't be included into the
594 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37595 continue
596
597 html_generator.AddLinkToAnotherReport(
598 _GetCoverageHtmlReportPathForDirectory(dir_path),
599 os.path.relpath(dir_path, SRC_ROOT_PATH),
600 per_directory_coverage_summary[dir_absolute_path])
601
602 html_generator.CreateTotalsEntry(
603 per_component_coverage_summary[component_name])
604 html_generator.WriteHtmlCoverageReport()
605
606
607def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
608 """Generates the html index file for component view."""
609 component_view_index_file_path = os.path.join(OUTPUT_DIR,
610 COMPONENT_VIEW_INDEX_FILE)
611 logging.debug('Generating component view html index file as: "%s".',
612 component_view_index_file_path)
613 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
614 'Component')
615 totals_coverage_summary = _CoverageSummary()
616
617 for component in per_component_coverage_summary:
618 totals_coverage_summary.AddSummary(
619 per_component_coverage_summary[component])
620
621 html_generator.AddLinkToAnotherReport(
622 _GetCoverageHtmlReportPathForComponent(component), component,
623 per_component_coverage_summary[component])
624
625 html_generator.CreateTotalsEntry(totals_coverage_summary)
626 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14627 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33628
629
630def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37631 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33632 html_index_file_path = os.path.join(OUTPUT_DIR,
633 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37634 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
635 DIRECTORY_VIEW_INDEX_FILE)
636 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
637
638
639def _WriteRedirectHtmlFile(from_html_path, to_html_path):
640 """Writes a html file that redirects to another html file."""
641 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
642 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33643 content = ("""
644 <!DOCTYPE html>
645 <html>
646 <head>
647 <!-- HTML meta refresh URL redirection -->
648 <meta http-equiv="refresh" content="0; url=%s">
649 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37650 </html>""" % to_html_relative_path)
651 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33652 f.write(content)
653
654
Yuke Liaodd1ec0592018-02-02 01:26:37655def _GetCoverageHtmlReportPathForFile(file_path):
656 """Given a file path, returns the corresponding html report path."""
657 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
658 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
659
660 # '+' is used instead of os.path.join because both of them are absolute paths
661 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14662 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37663 return _GetCoverageReportRootDirPath() + html_report_path
664
665
666def _GetCoverageHtmlReportPathForDirectory(dir_path):
667 """Given a directory path, returns the corresponding html report path."""
668 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
669 html_report_path = os.path.join(
670 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
671
672 # '+' is used instead of os.path.join because both of them are absolute paths
673 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14674 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37675 return _GetCoverageReportRootDirPath() + html_report_path
676
677
678def _GetCoverageHtmlReportPathForComponent(component_name):
679 """Given a component, returns the corresponding html report path."""
680 component_file_name = component_name.lower().replace('>', '-')
681 html_report_name = os.extsep.join([component_file_name, 'html'])
682 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
683 html_report_name)
684
685
686def _GetCoverageReportRootDirPath():
687 """The root directory that contains all generated coverage html reports."""
688 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33689
690
Yuke Liao506e8822017-12-04 16:52:54691def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
692 """Builds and runs target to generate the coverage profile data.
693
694 Args:
695 targets: A list of targets to build with coverage instrumentation.
696 commands: A list of commands used to run the targets.
697 jobs_count: Number of jobs to run in parallel for building. If None, a
698 default value is derived based on CPUs availability.
699
700 Returns:
701 A relative path to the generated profdata file.
702 """
703 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59704 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
705 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54706 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
707 profraw_file_paths)
708
Yuke Liaod4a9865202018-01-12 23:17:52709 for profraw_file_path in profraw_file_paths:
710 os.remove(profraw_file_path)
711
Yuke Liao506e8822017-12-04 16:52:54712 return profdata_file_path
713
714
715def _BuildTargets(targets, jobs_count):
716 """Builds target with Clang coverage instrumentation.
717
718 This function requires current working directory to be the root of checkout.
719
720 Args:
721 targets: A list of targets to build with coverage instrumentation.
722 jobs_count: Number of jobs to run in parallel for compilation. If None, a
723 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54724 """
Abhishek Arya1ec832c2017-12-05 18:06:59725
Yuke Liao506e8822017-12-04 16:52:54726 def _IsGomaConfigured():
727 """Returns True if goma is enabled in the gn build args.
728
729 Returns:
730 A boolean indicates whether goma is configured for building or not.
731 """
732 build_args = _ParseArgsGnFile()
733 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
734
Yuke Liao481d3482018-01-29 19:17:10735 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54736 if jobs_count is None and _IsGomaConfigured():
737 jobs_count = DEFAULT_GOMA_JOBS
738
739 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
740 if jobs_count is not None:
741 subprocess_cmd.append('-j' + str(jobs_count))
742
743 subprocess_cmd.extend(targets)
744 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10745 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54746
747
748def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
749 """Runs commands and returns the relative paths to the profraw data files.
750
751 Args:
752 targets: A list of targets built with coverage instrumentation.
753 commands: A list of commands used to run the targets.
754
755 Returns:
756 A list of relative paths to the generated profraw data files.
757 """
Yuke Liao481d3482018-01-29 19:17:10758 logging.debug('Executing the test commands')
759
Yuke Liao506e8822017-12-04 16:52:54760 # Remove existing profraw data files.
761 for file_or_dir in os.listdir(OUTPUT_DIR):
762 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
763 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
764
Yuke Liaoa0c8c2f2018-02-28 20:14:10765 profraw_file_paths = []
766
Yuke Liaod4a9865202018-01-12 23:17:52767 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54768 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10769 output_file_name = os.extsep.join([target + '_output', 'txt'])
770 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
771 logging.info('Running command: "%s", the output is redirected to "%s"',
772 command, output_file_path)
773
774 if _IsIosCommand(command):
775 # On iOS platform, due to lack of write permissions, profraw files are
776 # generated outside of the OUTPUT_DIR, and the exact paths are contained
777 # in the output of the command execution.
778 output = _ExecuteIosCommand(target, command)
779 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
780 else:
781 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
782 output = _ExecuteCommand(target, command)
783
784 with open(output_file_path, 'w') as output_file:
785 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54786
Yuke Liao481d3482018-01-29 19:17:10787 logging.debug('Finished executing the test commands')
788
Yuke Liaoa0c8c2f2018-02-28 20:14:10789 if _IsTargetOsIos():
790 return profraw_file_paths
791
Yuke Liao506e8822017-12-04 16:52:54792 for file_or_dir in os.listdir(OUTPUT_DIR):
793 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
794 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
795
796 # Assert one target/command generates at least one profraw data file.
797 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59798 assert any(
799 os.path.basename(profraw_file).startswith(target)
800 for profraw_file in profraw_file_paths), (
801 'Running target: %s failed to generate any profraw data file, '
802 'please make sure the binary exists and is properly instrumented.' %
803 target)
Yuke Liao506e8822017-12-04 16:52:54804
805 return profraw_file_paths
806
807
808def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10809 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52810 # Per Clang "Source-based Code Coverage" doc:
811 # "%Nm" expands out to the instrumented binary's signature. When this pattern
812 # is specified, the runtime creates a pool of N raw profiles which are used
813 # for on-line profile merging. The runtime takes care of selecting a raw
814 # profile from the pool, locking it, and updating it before the program exits.
815 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
816 # N must be between 1 and 9. The merge pool specifier can only occur once per
817 # filename pattern.
818 #
819 # 4 is chosen because it creates some level of parallelism, but it's not too
820 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59821 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52822 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54823 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
824 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54825
Yuke Liaoa0c8c2f2018-02-28 20:14:10826 try:
827 output = subprocess.check_output(
828 command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
829 except subprocess.CalledProcessError as e:
830 output = e.output
831 logging.warning('Command: "%s" exited with non-zero return code', command)
832
833 return output
834
835
836def _ExecuteIosCommand(target, command):
837 """Runs a single iOS command and generates a profraw data file.
838
839 iOS application doesn't have write access to folders outside of the app, so
840 it's impossible to instruct the app to flush the profraw data file to the
841 desired location. The profraw data file will be generated somewhere within the
842 application's Documents folder, and the full path can be obtained by parsing
843 the output.
844 """
845 assert _IsIosCommand(command)
846
847 try:
848 output = subprocess.check_output(command.split())
849 except subprocess.CalledProcessError as e:
850 # iossim emits non-zero return code even if tests run successfully, so
851 # ignore the return code.
852 output = e.output
853
854 return output
855
856
857def _GetProfrawDataFileByParsingOutput(output):
858 """Returns the path to the profraw data file obtained by parsing the output.
859
860 The output of running the test target has no format, but it is guaranteed to
861 have a single line containing the path to the generated profraw data file.
862 NOTE: This should only be called when target os is iOS.
863 """
864 assert _IsTargetOsIos()
865
866 output_by_lines = ''.join(output).split('\n')
867 profraw_file_identifier = 'Coverage data at '
868
869 for line in output_by_lines:
870 if profraw_file_identifier in line:
871 profraw_file_path = line.split(profraw_file_identifier)[1][:-1]
872 return profraw_file_path
873
874 assert False, ('No profraw data file was generated, did you call '
875 'coverage_util::ConfigureCoverageReportPath() in test setup? '
876 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54877
878
879def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
880 """Returns a relative path to the profdata file by merging profraw data files.
881
882 Args:
883 profraw_file_paths: A list of relative paths to the profraw data files that
884 are to be merged.
885
886 Returns:
887 A relative path to the generated profdata file.
888
889 Raises:
890 CalledProcessError: An error occurred merging profraw data files.
891 """
Yuke Liao481d3482018-01-29 19:17:10892 logging.info('Creating the coverage profile data file')
893 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54894 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54895 try:
Abhishek Arya1ec832c2017-12-05 18:06:59896 subprocess_cmd = [
897 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
898 ]
Yuke Liao506e8822017-12-04 16:52:54899 subprocess_cmd.extend(profraw_file_paths)
900 subprocess.check_call(subprocess_cmd)
901 except subprocess.CalledProcessError as error:
902 print('Failed to merge profraw files to create profdata file')
903 raise error
904
Yuke Liao481d3482018-01-29 19:17:10905 logging.debug('Finished merging profraw files')
906 logging.info('Code coverage profile data is created as: %s',
907 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54908 return profdata_file_path
909
910
Yuke Liaoea228d02018-01-05 19:10:33911def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
912 """Generates per file coverage summary using "llvm-cov export" command."""
913 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
914 # [[-object BIN]] [SOURCES].
915 # NOTE: For object files, the first one is specified as a positional argument,
916 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10917 logging.debug('Generating per-file code coverage summary using "llvm-cov '
918 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33919 subprocess_cmd = [
920 LLVM_COV_PATH, 'export', '-summary-only',
921 '-instr-profile=' + profdata_file_path, binary_paths[0]
922 ]
923 subprocess_cmd.extend(
924 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaoa0c8c2f2018-02-28 20:14:10925 if _IsTargetOsIos():
926 # iOS binaries are universal binaries, and it requires specifying the
927 # architecture to use.
928 subprocess_cmd.append('-arch=x86_64')
929
Yuke Liaoea228d02018-01-05 19:10:33930 subprocess_cmd.extend(filters)
931
932 json_output = json.loads(subprocess.check_output(subprocess_cmd))
933 assert len(json_output['data']) == 1
934 files_coverage_data = json_output['data'][0]['files']
935
936 per_file_coverage_summary = {}
937 for file_coverage_data in files_coverage_data:
938 file_path = file_coverage_data['filename']
939 summary = file_coverage_data['summary']
940
Yuke Liaoea228d02018-01-05 19:10:33941 if summary['lines']['count'] == 0:
942 continue
943
944 per_file_coverage_summary[file_path] = _CoverageSummary(
945 regions_total=summary['regions']['count'],
946 regions_covered=summary['regions']['covered'],
947 functions_total=summary['functions']['count'],
948 functions_covered=summary['functions']['covered'],
949 lines_total=summary['lines']['count'],
950 lines_covered=summary['lines']['covered'])
951
Yuke Liao481d3482018-01-29 19:17:10952 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33953 return per_file_coverage_summary
954
955
Yuke Liao506e8822017-12-04 16:52:54956def _GetBinaryPath(command):
957 """Returns a relative path to the binary to be run by the command.
958
Yuke Liao545db322018-02-15 17:12:01959 Currently, following types of commands are supported (e.g. url_unittests):
960 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
961 2. Use xvfb.
962 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
963 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liaoa0c8c2f2018-02-28 20:14:10964 3. Use iossim to run tests on iOS platform.
965 3.1. "out/Coverage-iphonesimulator/iossim
966 out/Coverage-iphonesimulator/url_unittests.app <arguments>"
Yuke Liao545db322018-02-15 17:12:01967
Yuke Liao506e8822017-12-04 16:52:54968 Args:
969 command: A command used to run a target.
970
971 Returns:
972 A relative path to the binary.
973 """
Yuke Liao545db322018-02-15 17:12:01974 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
975
976 command_parts = command.split()
977 if os.path.basename(command_parts[0]) == 'python':
978 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
979 'This tool doesn\'t understand the command: "%s"' % command)
980 return command_parts[2]
981
982 if os.path.basename(command_parts[0]) == xvfb_script_name:
983 return command_parts[1]
984
Yuke Liaoa0c8c2f2018-02-28 20:14:10985 if _IsIosCommand(command):
986 # For a given application bundle, the binary resides in the bundle and has
987 # the same name with the application without the .app extension.
988 app_path = command_parts[1]
989 app_name = os.path.splitext(os.path.basename(app_path))[0]
990 return os.path.join(app_path, app_name)
991
Yuke Liao506e8822017-12-04 16:52:54992 return command.split()[0]
993
994
Yuke Liaoa0c8c2f2018-02-28 20:14:10995def _IsIosCommand(command):
996 """Returns true if command is used to run tests on iOS platform."""
997 return os.path.basename(command.split()[0]) == 'iossim'
998
999
Yuke Liao95d13d72017-12-07 18:18:501000def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1001 """Verifies that the target executables specified in the commands are inside
1002 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541003 for command in commands:
1004 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501005 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1006 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1007 'Target executable "%s" in command: "%s" is outside of '
1008 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541009
1010
1011def _ValidateBuildingWithClangCoverage():
1012 """Asserts that targets are built with Clang coverage enabled."""
1013 build_args = _ParseArgsGnFile()
1014
1015 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1016 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591017 assert False, ('\'{} = true\' is required in args.gn.'
1018 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541019
1020
1021def _ParseArgsGnFile():
1022 """Parses args.gn file and returns results as a dictionary.
1023
1024 Returns:
1025 A dictionary representing the build args.
1026 """
1027 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1028 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1029 'missing args.gn file.' % BUILD_DIR)
1030 with open(build_args_path) as build_args_file:
1031 build_args_lines = build_args_file.readlines()
1032
1033 build_args = {}
1034 for build_arg_line in build_args_lines:
1035 build_arg_without_comments = build_arg_line.split('#')[0]
1036 key_value_pair = build_arg_without_comments.split('=')
1037 if len(key_value_pair) != 2:
1038 continue
1039
1040 key = key_value_pair[0].strip()
1041 value = key_value_pair[1].strip()
1042 build_args[key] = value
1043
1044 return build_args
1045
1046
Abhishek Arya16f059a2017-12-07 17:47:321047def _VerifyPathsAndReturnAbsolutes(paths):
1048 """Verifies that the paths specified in |paths| exist and returns absolute
1049 versions.
Yuke Liao66da1732017-12-05 22:19:421050
1051 Args:
1052 paths: A list of files or directories.
1053 """
Abhishek Arya16f059a2017-12-07 17:47:321054 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421055 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321056 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1057 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1058
1059 absolute_paths.append(absolute_path)
1060
1061 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421062
1063
Yuke Liaodd1ec0592018-02-02 01:26:371064def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1065 """Returns a target path relative to the directory of base_path.
1066
1067 This method requires base_path to be a file, otherwise, one should call
1068 os.path.relpath directly.
1069 """
1070 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141071 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371072 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141073 base_dir = os.path.dirname(base_path)
1074 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371075
1076
Yuke Liao506e8822017-12-04 16:52:541077def _ParseCommandArguments():
1078 """Adds and parses relevant arguments for tool comands.
1079
1080 Returns:
1081 A dictionary representing the arguments.
1082 """
1083 arg_parser = argparse.ArgumentParser()
1084 arg_parser.usage = __doc__
1085
Abhishek Arya1ec832c2017-12-05 18:06:591086 arg_parser.add_argument(
1087 '-b',
1088 '--build-dir',
1089 type=str,
1090 required=True,
1091 help='The build directory, the path needs to be relative to the root of '
1092 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541093
Abhishek Arya1ec832c2017-12-05 18:06:591094 arg_parser.add_argument(
1095 '-o',
1096 '--output-dir',
1097 type=str,
1098 required=True,
1099 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541100
Abhishek Arya1ec832c2017-12-05 18:06:591101 arg_parser.add_argument(
1102 '-c',
1103 '--command',
1104 action='append',
1105 required=True,
1106 help='Commands used to run test targets, one test target needs one and '
1107 'only one command, when specifying commands, one should assume the '
1108 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541109
Abhishek Arya1ec832c2017-12-05 18:06:591110 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421111 '-f',
1112 '--filters',
1113 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321114 required=False,
Yuke Liao66da1732017-12-05 22:19:421115 help='Directories or files to get code coverage for, and all files under '
1116 'the directories are included recursively.')
1117
1118 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591119 '-j',
1120 '--jobs',
1121 type=int,
1122 default=None,
1123 help='Run N jobs to build in parallel. If not specified, a default value '
1124 'will be derived based on CPUs availability. Please refer to '
1125 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541126
Abhishek Arya1ec832c2017-12-05 18:06:591127 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101128 '-v',
1129 '--verbose',
1130 action='store_true',
1131 help='Prints additional output for diagnostics.')
1132
1133 arg_parser.add_argument(
1134 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1135
1136 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591137 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541138
1139 args = arg_parser.parse_args()
1140 return args
1141
1142
1143def Main():
1144 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371145 assert _GetPlatform() in [
1146 'linux', 'mac'
1147 ], ('Coverage is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541148 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1149 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591150 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541151 DownloadCoverageToolsIfNeeded()
1152
1153 args = _ParseCommandArguments()
1154 global BUILD_DIR
1155 BUILD_DIR = args.build_dir
1156 global OUTPUT_DIR
1157 OUTPUT_DIR = args.output_dir
1158
Yuke Liao481d3482018-01-29 19:17:101159 log_level = logging.DEBUG if args.verbose else logging.INFO
1160 log_format = '[%(asctime)s] %(message)s'
1161 log_file = args.log_file if args.log_file else None
1162 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1163
Yuke Liao506e8822017-12-04 16:52:541164 assert len(args.targets) == len(args.command), ('Number of targets must be '
1165 'equal to the number of test '
1166 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591167 assert os.path.exists(BUILD_DIR), (
1168 'Build directory: {} doesn\'t exist. '
1169 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541170 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501171 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321172
1173 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421174 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321175 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421176
Yuke Liao506e8822017-12-04 16:52:541177 if not os.path.exists(OUTPUT_DIR):
1178 os.makedirs(OUTPUT_DIR)
1179
Abhishek Arya1ec832c2017-12-05 18:06:591180 profdata_file_path = _CreateCoverageProfileDataForTargets(
1181 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541182 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331183
Yuke Liao481d3482018-01-29 19:17:101184 logging.info('Generating code coverage report in html (this can take a while '
1185 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371186 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1187 binary_paths, profdata_file_path, absolute_filter_paths)
1188 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1189 absolute_filter_paths)
1190 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1191
1192 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1193 per_file_coverage_summary)
1194 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1195 per_file_coverage_summary)
1196 _GenerateDirectoryViewHtmlIndexFile()
1197
1198 component_to_directories = _ExtractComponentToDirectoriesMapping()
1199 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1200 component_to_directories, per_directory_coverage_summary)
1201 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1202 component_to_directories,
1203 per_directory_coverage_summary)
1204 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331205
1206 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371207 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331208 _OverwriteHtmlReportsIndexFile()
1209
Yuke Liao506e8822017-12-04 16:52:541210 html_index_file_path = 'file://' + os.path.abspath(
1211 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101212 logging.info('Index file for html report is generated as: %s',
1213 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541214
Abhishek Arya1ec832c2017-12-05 18:06:591215
Yuke Liao506e8822017-12-04 16:52:541216if __name__ == '__main__':
1217 sys.exit(Main())