blob: bdd100018e15304f3db7afc90863cab34415149f [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
Abhishek Arya1ec832c2017-12-05 18:06:59310def _GetPlatform():
311 """Returns current running platform."""
312 if sys.platform == 'win32' or sys.platform == 'cygwin':
313 return 'win'
314 if sys.platform.startswith('linux'):
315 return 'linux'
316 else:
317 assert sys.platform == 'darwin'
318 return 'mac'
319
320
Yuke Liaob2926832018-03-02 17:34:29321def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10322 """Returns true if the target_os specified in args.gn file is ios"""
323 build_args = _ParseArgsGnFile()
324 return 'target_os' in build_args and build_args['target_os'] == '"ios"'
325
326
Yuke Liao506e8822017-12-04 16:52:54327# TODO(crbug.com/759794): remove this function once tools get included to
328# Clang bundle:
329# https://chromium-review.googlesource.com/c/chromium/src/+/688221
330def DownloadCoverageToolsIfNeeded():
331 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59332
333 def _GetRevisionFromStampFile(stamp_file_path, platform):
Yuke Liao506e8822017-12-04 16:52:54334 """Returns a pair of revision number by reading the build stamp file.
335
336 Args:
337 stamp_file_path: A path the build stamp file created by
338 tools/clang/scripts/update.py.
339 Returns:
340 A pair of integers represeting the main and sub revision respectively.
341 """
342 if not os.path.exists(stamp_file_path):
343 return 0, 0
344
345 with open(stamp_file_path) as stamp_file:
Abhishek Arya1ec832c2017-12-05 18:06:59346 for stamp_file_line in stamp_file.readlines():
347 if ',' in stamp_file_line:
348 package_version, target_os = stamp_file_line.rstrip().split(',')
349 else:
350 package_version = stamp_file_line.rstrip()
351 target_os = ''
Yuke Liao506e8822017-12-04 16:52:54352
Yuke Liaoa0c8c2f2018-02-28 20:14:10353 if target_os and target_os != 'ios' and platform != target_os:
Abhishek Arya1ec832c2017-12-05 18:06:59354 continue
355
356 clang_revision_str, clang_sub_revision_str = package_version.split('-')
357 return int(clang_revision_str), int(clang_sub_revision_str)
358
Yuke Liaoa0c8c2f2018-02-28 20:14:10359 assert False, 'Coverage is only supported on target_os - linux, mac and ios'
Abhishek Arya1ec832c2017-12-05 18:06:59360
361 platform = _GetPlatform()
Yuke Liao506e8822017-12-04 16:52:54362 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59363 clang_update.STAMP_FILE, platform)
Yuke Liao506e8822017-12-04 16:52:54364
365 coverage_revision_stamp_file = os.path.join(
366 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
367 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59368 coverage_revision_stamp_file, platform)
Yuke Liao506e8822017-12-04 16:52:54369
Yuke Liaoea228d02018-01-05 19:10:33370 has_coverage_tools = (
371 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32372
Yuke Liaoea228d02018-01-05 19:10:33373 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54374 coverage_sub_revision == clang_sub_revision):
375 # LLVM coverage tools are up to date, bail out.
376 return clang_revision
377
378 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
379 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
380
381 # The code bellow follows the code from tools/clang/scripts/update.py.
Abhishek Arya1ec832c2017-12-05 18:06:59382 if platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54383 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
384 else:
Abhishek Arya1ec832c2017-12-05 18:06:59385 assert platform == 'linux'
Yuke Liao506e8822017-12-04 16:52:54386 coverage_tools_url = (
387 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
388
389 try:
390 clang_update.DownloadAndUnpack(coverage_tools_url,
391 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10392 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54393 with open(coverage_revision_stamp_file, 'w') as file_handle:
Abhishek Arya1ec832c2017-12-05 18:06:59394 file_handle.write('%s,%s' % (package_version, platform))
Yuke Liao506e8822017-12-04 16:52:54395 file_handle.write('\n')
396 except urllib2.URLError:
397 raise Exception(
398 'Failed to download coverage tools: %s.' % coverage_tools_url)
399
400
Yuke Liaodd1ec0592018-02-02 01:26:37401def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
402 filters):
Yuke Liao506e8822017-12-04 16:52:54403 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
404
405 For a file with absolute path /a/b/x.cc, a html report is generated as:
406 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
407 OUTPUT_DIR/index.html.
408
409 Args:
410 binary_paths: A list of paths to the instrumented binaries.
411 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42412 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54413 """
Yuke Liao506e8822017-12-04 16:52:54414 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
415 # [[-object BIN]] [SOURCES]
416 # NOTE: For object files, the first one is specified as a positional argument,
417 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10418 logging.debug('Generating per file line by line coverage reports using '
419 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59420 subprocess_cmd = [
421 LLVM_COV_PATH, 'show', '-format=html',
422 '-output-dir={}'.format(OUTPUT_DIR),
423 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
424 ]
425 subprocess_cmd.extend(
426 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29427 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42428 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54429 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10430 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54431
432
Yuke Liaodd1ec0592018-02-02 01:26:37433def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
434 """Generates html index file for file view."""
435 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
436 logging.debug('Generating file view html index file as: "%s".',
437 file_view_index_file_path)
438 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
439 'Path')
440 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33441
Yuke Liaodd1ec0592018-02-02 01:26:37442 for file_path in per_file_coverage_summary:
443 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
444
445 html_generator.AddLinkToAnotherReport(
446 _GetCoverageHtmlReportPathForFile(file_path),
447 os.path.relpath(file_path, SRC_ROOT_PATH),
448 per_file_coverage_summary[file_path])
449
450 html_generator.CreateTotalsEntry(totals_coverage_summary)
451 html_generator.WriteHtmlCoverageReport()
452 logging.debug('Finished generating file view html index file.')
453
454
455def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
456 """Calculates per directory coverage summary."""
457 logging.debug('Calculating per-directory coverage summary')
458 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
459
Yuke Liaoea228d02018-01-05 19:10:33460 for file_path in per_file_coverage_summary:
461 summary = per_file_coverage_summary[file_path]
462 parent_dir = os.path.dirname(file_path)
463 while True:
464 per_directory_coverage_summary[parent_dir].AddSummary(summary)
465
466 if parent_dir == SRC_ROOT_PATH:
467 break
468 parent_dir = os.path.dirname(parent_dir)
469
Yuke Liaodd1ec0592018-02-02 01:26:37470 logging.debug('Finished calculating per-directory coverage summary')
471 return per_directory_coverage_summary
472
473
474def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
475 per_file_coverage_summary):
476 """Generates per directory coverage breakdown in html."""
477 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33478 for dir_path in per_directory_coverage_summary:
479 _GenerateCoverageInHtmlForDirectory(
480 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
481
Yuke Liaodd1ec0592018-02-02 01:26:37482 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10483
Yuke Liaoea228d02018-01-05 19:10:33484
485def _GenerateCoverageInHtmlForDirectory(
486 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
487 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37488 html_generator = _CoverageReportHtmlGenerator(
489 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33490
491 for entry_name in os.listdir(dir_path):
492 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33493
Yuke Liaodd1ec0592018-02-02 01:26:37494 if entry_path in per_file_coverage_summary:
495 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
496 entry_coverage_summary = per_file_coverage_summary[entry_path]
497 elif entry_path in per_directory_coverage_summary:
498 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
499 entry_path)
500 entry_coverage_summary = per_directory_coverage_summary[entry_path]
501 else:
Yuke Liaoc7e607142018-02-05 20:26:14502 # Any file without executable lines shouldn't be included into the report.
503 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37504 continue
Yuke Liaoea228d02018-01-05 19:10:33505
Yuke Liaodd1ec0592018-02-02 01:26:37506 html_generator.AddLinkToAnotherReport(entry_html_report_path,
507 os.path.basename(entry_path),
508 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33509
Yuke Liaod54030e2018-01-08 17:34:12510 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37511 html_generator.WriteHtmlCoverageReport()
512
513
514def _GenerateDirectoryViewHtmlIndexFile():
515 """Generates the html index file for directory view.
516
517 Note that the index file is already generated under SRC_ROOT_PATH, so this
518 file simply redirects to it, and the reason of this extra layer is for
519 structural consistency with other views.
520 """
521 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
522 DIRECTORY_VIEW_INDEX_FILE)
523 logging.debug('Generating directory view html index file as: "%s".',
524 directory_view_index_file_path)
525 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
526 SRC_ROOT_PATH)
527 _WriteRedirectHtmlFile(directory_view_index_file_path,
528 src_root_html_report_path)
529 logging.debug('Finished generating directory view html index file.')
530
531
532def _CalculatePerComponentCoverageSummary(component_to_directories,
533 per_directory_coverage_summary):
534 """Calculates per component coverage summary."""
535 logging.debug('Calculating per-component coverage summary')
536 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
537
538 for component in component_to_directories:
539 for directory in component_to_directories[component]:
540 absolute_directory_path = os.path.abspath(directory)
541 if absolute_directory_path in per_directory_coverage_summary:
542 per_component_coverage_summary[component].AddSummary(
543 per_directory_coverage_summary[absolute_directory_path])
544
545 logging.debug('Finished calculating per-component coverage summary')
546 return per_component_coverage_summary
547
548
549def _ExtractComponentToDirectoriesMapping():
550 """Returns a mapping from components to directories."""
551 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
552 directory_to_component = component_mappings['dir-to-component']
553
554 component_to_directories = defaultdict(list)
555 for directory in directory_to_component:
556 component = directory_to_component[directory]
557 component_to_directories[component].append(directory)
558
559 return component_to_directories
560
561
562def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
563 component_to_directories,
564 per_directory_coverage_summary):
565 """Generates per-component coverage reports in html."""
566 logging.debug('Writing per-component coverage html reports.')
567 for component in per_component_coverage_summary:
568 _GenerateCoverageInHtmlForComponent(
569 component, per_component_coverage_summary, component_to_directories,
570 per_directory_coverage_summary)
571
572 logging.debug('Finished writing per-component coverage html reports.')
573
574
575def _GenerateCoverageInHtmlForComponent(
576 component_name, per_component_coverage_summary, component_to_directories,
577 per_directory_coverage_summary):
578 """Generates coverage html report for a component."""
579 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
580 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14581 component_html_report_dir = os.path.dirname(component_html_report_path)
582 if not os.path.exists(component_html_report_dir):
583 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37584
585 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
586 'Path')
587
588 for dir_path in component_to_directories[component_name]:
589 dir_absolute_path = os.path.abspath(dir_path)
590 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14591 # Any directory without an excercised file shouldn't be included into the
592 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37593 continue
594
595 html_generator.AddLinkToAnotherReport(
596 _GetCoverageHtmlReportPathForDirectory(dir_path),
597 os.path.relpath(dir_path, SRC_ROOT_PATH),
598 per_directory_coverage_summary[dir_absolute_path])
599
600 html_generator.CreateTotalsEntry(
601 per_component_coverage_summary[component_name])
602 html_generator.WriteHtmlCoverageReport()
603
604
605def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
606 """Generates the html index file for component view."""
607 component_view_index_file_path = os.path.join(OUTPUT_DIR,
608 COMPONENT_VIEW_INDEX_FILE)
609 logging.debug('Generating component view html index file as: "%s".',
610 component_view_index_file_path)
611 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
612 'Component')
613 totals_coverage_summary = _CoverageSummary()
614
615 for component in per_component_coverage_summary:
616 totals_coverage_summary.AddSummary(
617 per_component_coverage_summary[component])
618
619 html_generator.AddLinkToAnotherReport(
620 _GetCoverageHtmlReportPathForComponent(component), component,
621 per_component_coverage_summary[component])
622
623 html_generator.CreateTotalsEntry(totals_coverage_summary)
624 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14625 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33626
627
628def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37629 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33630 html_index_file_path = os.path.join(OUTPUT_DIR,
631 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37632 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
633 DIRECTORY_VIEW_INDEX_FILE)
634 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
635
636
637def _WriteRedirectHtmlFile(from_html_path, to_html_path):
638 """Writes a html file that redirects to another html file."""
639 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
640 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33641 content = ("""
642 <!DOCTYPE html>
643 <html>
644 <head>
645 <!-- HTML meta refresh URL redirection -->
646 <meta http-equiv="refresh" content="0; url=%s">
647 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37648 </html>""" % to_html_relative_path)
649 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33650 f.write(content)
651
652
Yuke Liaodd1ec0592018-02-02 01:26:37653def _GetCoverageHtmlReportPathForFile(file_path):
654 """Given a file path, returns the corresponding html report path."""
655 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
656 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
657
658 # '+' is used instead of os.path.join because both of them are absolute paths
659 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14660 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37661 return _GetCoverageReportRootDirPath() + html_report_path
662
663
664def _GetCoverageHtmlReportPathForDirectory(dir_path):
665 """Given a directory path, returns the corresponding html report path."""
666 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
667 html_report_path = os.path.join(
668 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
669
670 # '+' is used instead of os.path.join because both of them are absolute paths
671 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14672 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37673 return _GetCoverageReportRootDirPath() + html_report_path
674
675
676def _GetCoverageHtmlReportPathForComponent(component_name):
677 """Given a component, returns the corresponding html report path."""
678 component_file_name = component_name.lower().replace('>', '-')
679 html_report_name = os.extsep.join([component_file_name, 'html'])
680 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
681 html_report_name)
682
683
684def _GetCoverageReportRootDirPath():
685 """The root directory that contains all generated coverage html reports."""
686 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33687
688
Yuke Liao506e8822017-12-04 16:52:54689def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
690 """Builds and runs target to generate the coverage profile data.
691
692 Args:
693 targets: A list of targets to build with coverage instrumentation.
694 commands: A list of commands used to run the targets.
695 jobs_count: Number of jobs to run in parallel for building. If None, a
696 default value is derived based on CPUs availability.
697
698 Returns:
699 A relative path to the generated profdata file.
700 """
701 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59702 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
703 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54704 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
705 profraw_file_paths)
706
Yuke Liaod4a9865202018-01-12 23:17:52707 for profraw_file_path in profraw_file_paths:
708 os.remove(profraw_file_path)
709
Yuke Liao506e8822017-12-04 16:52:54710 return profdata_file_path
711
712
713def _BuildTargets(targets, jobs_count):
714 """Builds target with Clang coverage instrumentation.
715
716 This function requires current working directory to be the root of checkout.
717
718 Args:
719 targets: A list of targets to build with coverage instrumentation.
720 jobs_count: Number of jobs to run in parallel for compilation. If None, a
721 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54722 """
Abhishek Arya1ec832c2017-12-05 18:06:59723
Yuke Liao506e8822017-12-04 16:52:54724 def _IsGomaConfigured():
725 """Returns True if goma is enabled in the gn build args.
726
727 Returns:
728 A boolean indicates whether goma is configured for building or not.
729 """
730 build_args = _ParseArgsGnFile()
731 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
732
Yuke Liao481d3482018-01-29 19:17:10733 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54734 if jobs_count is None and _IsGomaConfigured():
735 jobs_count = DEFAULT_GOMA_JOBS
736
737 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
738 if jobs_count is not None:
739 subprocess_cmd.append('-j' + str(jobs_count))
740
741 subprocess_cmd.extend(targets)
742 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10743 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54744
745
746def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
747 """Runs commands and returns the relative paths to the profraw data files.
748
749 Args:
750 targets: A list of targets built with coverage instrumentation.
751 commands: A list of commands used to run the targets.
752
753 Returns:
754 A list of relative paths to the generated profraw data files.
755 """
Yuke Liao481d3482018-01-29 19:17:10756 logging.debug('Executing the test commands')
757
Yuke Liao506e8822017-12-04 16:52:54758 # Remove existing profraw data files.
759 for file_or_dir in os.listdir(OUTPUT_DIR):
760 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
761 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
762
Yuke Liaoa0c8c2f2018-02-28 20:14:10763 profraw_file_paths = []
764
Yuke Liaod4a9865202018-01-12 23:17:52765 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54766 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10767 output_file_name = os.extsep.join([target + '_output', 'txt'])
768 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
769 logging.info('Running command: "%s", the output is redirected to "%s"',
770 command, output_file_path)
771
Yuke Liaob2926832018-03-02 17:34:29772 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10773 # On iOS platform, due to lack of write permissions, profraw files are
774 # generated outside of the OUTPUT_DIR, and the exact paths are contained
775 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29776 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10777 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
778 else:
779 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
780 output = _ExecuteCommand(target, command)
781
782 with open(output_file_path, 'w') as output_file:
783 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54784
Yuke Liao481d3482018-01-29 19:17:10785 logging.debug('Finished executing the test commands')
786
Yuke Liaob2926832018-03-02 17:34:29787 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10788 return profraw_file_paths
789
Yuke Liao506e8822017-12-04 16:52:54790 for file_or_dir in os.listdir(OUTPUT_DIR):
791 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
792 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
793
794 # Assert one target/command generates at least one profraw data file.
795 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59796 assert any(
797 os.path.basename(profraw_file).startswith(target)
798 for profraw_file in profraw_file_paths), (
799 'Running target: %s failed to generate any profraw data file, '
800 'please make sure the binary exists and is properly instrumented.' %
801 target)
Yuke Liao506e8822017-12-04 16:52:54802
803 return profraw_file_paths
804
805
806def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10807 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52808 # Per Clang "Source-based Code Coverage" doc:
809 # "%Nm" expands out to the instrumented binary's signature. When this pattern
810 # is specified, the runtime creates a pool of N raw profiles which are used
811 # for on-line profile merging. The runtime takes care of selecting a raw
812 # profile from the pool, locking it, and updating it before the program exits.
813 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
814 # N must be between 1 and 9. The merge pool specifier can only occur once per
815 # filename pattern.
816 #
817 # 4 is chosen because it creates some level of parallelism, but it's not too
818 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59819 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52820 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54821 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
822 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54823
Yuke Liaoa0c8c2f2018-02-28 20:14:10824 try:
825 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29826 shlex.split(command),
827 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10828 except subprocess.CalledProcessError as e:
829 output = e.output
830 logging.warning('Command: "%s" exited with non-zero return code', command)
831
832 return output
833
834
Yuke Liaob2926832018-03-02 17:34:29835def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10836 """Runs a single iOS command and generates a profraw data file.
837
838 iOS application doesn't have write access to folders outside of the app, so
839 it's impossible to instruct the app to flush the profraw data file to the
840 desired location. The profraw data file will be generated somewhere within the
841 application's Documents folder, and the full path can be obtained by parsing
842 the output.
843 """
Yuke Liaob2926832018-03-02 17:34:29844 assert _IsIOSCommand(command)
845
846 # After running tests, iossim generates a profraw data file, it won't be
847 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
848 # checkout.
849 iossim_profraw_file_path = os.path.join(
850 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10851
852 try:
Yuke Liaob2926832018-03-02 17:34:29853 output = subprocess.check_output(
854 shlex.split(command),
855 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10856 except subprocess.CalledProcessError as e:
857 # iossim emits non-zero return code even if tests run successfully, so
858 # ignore the return code.
859 output = e.output
860
861 return output
862
863
864def _GetProfrawDataFileByParsingOutput(output):
865 """Returns the path to the profraw data file obtained by parsing the output.
866
867 The output of running the test target has no format, but it is guaranteed to
868 have a single line containing the path to the generated profraw data file.
869 NOTE: This should only be called when target os is iOS.
870 """
Yuke Liaob2926832018-03-02 17:34:29871 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10872
Yuke Liaob2926832018-03-02 17:34:29873 output_by_lines = ''.join(output).splitlines()
874 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10875
876 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29877 result = profraw_file_pattern.match(line)
878 if result:
879 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10880
881 assert False, ('No profraw data file was generated, did you call '
882 'coverage_util::ConfigureCoverageReportPath() in test setup? '
883 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54884
885
886def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
887 """Returns a relative path to the profdata file by merging profraw data files.
888
889 Args:
890 profraw_file_paths: A list of relative paths to the profraw data files that
891 are to be merged.
892
893 Returns:
894 A relative path to the generated profdata file.
895
896 Raises:
897 CalledProcessError: An error occurred merging profraw data files.
898 """
Yuke Liao481d3482018-01-29 19:17:10899 logging.info('Creating the coverage profile data file')
900 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54901 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54902 try:
Abhishek Arya1ec832c2017-12-05 18:06:59903 subprocess_cmd = [
904 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
905 ]
Yuke Liao506e8822017-12-04 16:52:54906 subprocess_cmd.extend(profraw_file_paths)
907 subprocess.check_call(subprocess_cmd)
908 except subprocess.CalledProcessError as error:
909 print('Failed to merge profraw files to create profdata file')
910 raise error
911
Yuke Liao481d3482018-01-29 19:17:10912 logging.debug('Finished merging profraw files')
913 logging.info('Code coverage profile data is created as: %s',
914 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54915 return profdata_file_path
916
917
Yuke Liaoea228d02018-01-05 19:10:33918def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
919 """Generates per file coverage summary using "llvm-cov export" command."""
920 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
921 # [[-object BIN]] [SOURCES].
922 # NOTE: For object files, the first one is specified as a positional argument,
923 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10924 logging.debug('Generating per-file code coverage summary using "llvm-cov '
925 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33926 subprocess_cmd = [
927 LLVM_COV_PATH, 'export', '-summary-only',
928 '-instr-profile=' + profdata_file_path, binary_paths[0]
929 ]
930 subprocess_cmd.extend(
931 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29932 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoa0c8c2f2018-02-28 20:14:10933
Yuke Liaoea228d02018-01-05 19:10:33934 subprocess_cmd.extend(filters)
935
936 json_output = json.loads(subprocess.check_output(subprocess_cmd))
937 assert len(json_output['data']) == 1
938 files_coverage_data = json_output['data'][0]['files']
939
940 per_file_coverage_summary = {}
941 for file_coverage_data in files_coverage_data:
942 file_path = file_coverage_data['filename']
943 summary = file_coverage_data['summary']
944
Yuke Liaoea228d02018-01-05 19:10:33945 if summary['lines']['count'] == 0:
946 continue
947
948 per_file_coverage_summary[file_path] = _CoverageSummary(
949 regions_total=summary['regions']['count'],
950 regions_covered=summary['regions']['covered'],
951 functions_total=summary['functions']['count'],
952 functions_covered=summary['functions']['covered'],
953 lines_total=summary['lines']['count'],
954 lines_covered=summary['lines']['covered'])
955
Yuke Liao481d3482018-01-29 19:17:10956 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33957 return per_file_coverage_summary
958
959
Yuke Liaob2926832018-03-02 17:34:29960def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
961 """Appends -arch arguments to the command list if it's ios platform.
962
963 iOS binaries are universal binaries, and require specifying the architecture
964 to use, and one architecture needs to be specified for each binary.
965 """
966 if _IsIOS():
967 cmd_list.extend(['-arch=x86_64'] * num_archs)
968
969
Yuke Liao506e8822017-12-04 16:52:54970def _GetBinaryPath(command):
971 """Returns a relative path to the binary to be run by the command.
972
Yuke Liao545db322018-02-15 17:12:01973 Currently, following types of commands are supported (e.g. url_unittests):
974 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
975 2. Use xvfb.
976 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
977 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liaoa0c8c2f2018-02-28 20:14:10978 3. Use iossim to run tests on iOS platform.
979 3.1. "out/Coverage-iphonesimulator/iossim
980 out/Coverage-iphonesimulator/url_unittests.app <arguments>"
Yuke Liao545db322018-02-15 17:12:01981
Yuke Liao506e8822017-12-04 16:52:54982 Args:
983 command: A command used to run a target.
984
985 Returns:
986 A relative path to the binary.
987 """
Yuke Liao545db322018-02-15 17:12:01988 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
989
Yuke Liaob2926832018-03-02 17:34:29990 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:01991 if os.path.basename(command_parts[0]) == 'python':
992 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
993 'This tool doesn\'t understand the command: "%s"' % command)
994 return command_parts[2]
995
996 if os.path.basename(command_parts[0]) == xvfb_script_name:
997 return command_parts[1]
998
Yuke Liaob2926832018-03-02 17:34:29999 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101000 # For a given application bundle, the binary resides in the bundle and has
1001 # the same name with the application without the .app extension.
1002 app_path = command_parts[1]
1003 app_name = os.path.splitext(os.path.basename(app_path))[0]
1004 return os.path.join(app_path, app_name)
1005
Yuke Liaob2926832018-03-02 17:34:291006 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541007
1008
Yuke Liaob2926832018-03-02 17:34:291009def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101010 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291011 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101012
1013
Yuke Liao95d13d72017-12-07 18:18:501014def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1015 """Verifies that the target executables specified in the commands are inside
1016 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541017 for command in commands:
1018 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501019 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1020 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1021 'Target executable "%s" in command: "%s" is outside of '
1022 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541023
1024
1025def _ValidateBuildingWithClangCoverage():
1026 """Asserts that targets are built with Clang coverage enabled."""
1027 build_args = _ParseArgsGnFile()
1028
1029 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1030 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591031 assert False, ('\'{} = true\' is required in args.gn.'
1032 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541033
1034
1035def _ParseArgsGnFile():
1036 """Parses args.gn file and returns results as a dictionary.
1037
1038 Returns:
1039 A dictionary representing the build args.
1040 """
1041 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1042 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1043 'missing args.gn file.' % BUILD_DIR)
1044 with open(build_args_path) as build_args_file:
1045 build_args_lines = build_args_file.readlines()
1046
1047 build_args = {}
1048 for build_arg_line in build_args_lines:
1049 build_arg_without_comments = build_arg_line.split('#')[0]
1050 key_value_pair = build_arg_without_comments.split('=')
1051 if len(key_value_pair) != 2:
1052 continue
1053
1054 key = key_value_pair[0].strip()
1055 value = key_value_pair[1].strip()
1056 build_args[key] = value
1057
1058 return build_args
1059
1060
Abhishek Arya16f059a2017-12-07 17:47:321061def _VerifyPathsAndReturnAbsolutes(paths):
1062 """Verifies that the paths specified in |paths| exist and returns absolute
1063 versions.
Yuke Liao66da1732017-12-05 22:19:421064
1065 Args:
1066 paths: A list of files or directories.
1067 """
Abhishek Arya16f059a2017-12-07 17:47:321068 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421069 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321070 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1071 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1072
1073 absolute_paths.append(absolute_path)
1074
1075 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421076
1077
Yuke Liaodd1ec0592018-02-02 01:26:371078def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1079 """Returns a target path relative to the directory of base_path.
1080
1081 This method requires base_path to be a file, otherwise, one should call
1082 os.path.relpath directly.
1083 """
1084 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141085 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371086 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141087 base_dir = os.path.dirname(base_path)
1088 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371089
1090
Yuke Liao506e8822017-12-04 16:52:541091def _ParseCommandArguments():
1092 """Adds and parses relevant arguments for tool comands.
1093
1094 Returns:
1095 A dictionary representing the arguments.
1096 """
1097 arg_parser = argparse.ArgumentParser()
1098 arg_parser.usage = __doc__
1099
Abhishek Arya1ec832c2017-12-05 18:06:591100 arg_parser.add_argument(
1101 '-b',
1102 '--build-dir',
1103 type=str,
1104 required=True,
1105 help='The build directory, the path needs to be relative to the root of '
1106 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541107
Abhishek Arya1ec832c2017-12-05 18:06:591108 arg_parser.add_argument(
1109 '-o',
1110 '--output-dir',
1111 type=str,
1112 required=True,
1113 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541114
Abhishek Arya1ec832c2017-12-05 18:06:591115 arg_parser.add_argument(
1116 '-c',
1117 '--command',
1118 action='append',
1119 required=True,
1120 help='Commands used to run test targets, one test target needs one and '
1121 'only one command, when specifying commands, one should assume the '
1122 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541123
Abhishek Arya1ec832c2017-12-05 18:06:591124 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421125 '-f',
1126 '--filters',
1127 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321128 required=False,
Yuke Liao66da1732017-12-05 22:19:421129 help='Directories or files to get code coverage for, and all files under '
1130 'the directories are included recursively.')
1131
1132 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591133 '-j',
1134 '--jobs',
1135 type=int,
1136 default=None,
1137 help='Run N jobs to build in parallel. If not specified, a default value '
1138 'will be derived based on CPUs availability. Please refer to '
1139 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541140
Abhishek Arya1ec832c2017-12-05 18:06:591141 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101142 '-v',
1143 '--verbose',
1144 action='store_true',
1145 help='Prints additional output for diagnostics.')
1146
1147 arg_parser.add_argument(
1148 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1149
1150 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591151 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541152
1153 args = arg_parser.parse_args()
1154 return args
1155
1156
1157def Main():
1158 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371159 assert _GetPlatform() in [
1160 'linux', 'mac'
Yuke Liaob2926832018-03-02 17:34:291161 ], ('This script is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541162 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1163 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591164 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541165 DownloadCoverageToolsIfNeeded()
1166
1167 args = _ParseCommandArguments()
1168 global BUILD_DIR
1169 BUILD_DIR = args.build_dir
1170 global OUTPUT_DIR
1171 OUTPUT_DIR = args.output_dir
1172
Yuke Liao481d3482018-01-29 19:17:101173 log_level = logging.DEBUG if args.verbose else logging.INFO
1174 log_format = '[%(asctime)s] %(message)s'
1175 log_file = args.log_file if args.log_file else None
1176 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1177
Yuke Liao506e8822017-12-04 16:52:541178 assert len(args.targets) == len(args.command), ('Number of targets must be '
1179 'equal to the number of test '
1180 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591181 assert os.path.exists(BUILD_DIR), (
1182 'Build directory: {} doesn\'t exist. '
1183 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541184 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501185 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321186
1187 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421188 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321189 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421190
Yuke Liao506e8822017-12-04 16:52:541191 if not os.path.exists(OUTPUT_DIR):
1192 os.makedirs(OUTPUT_DIR)
1193
Abhishek Arya1ec832c2017-12-05 18:06:591194 profdata_file_path = _CreateCoverageProfileDataForTargets(
1195 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541196 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331197
Yuke Liao481d3482018-01-29 19:17:101198 logging.info('Generating code coverage report in html (this can take a while '
1199 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371200 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1201 binary_paths, profdata_file_path, absolute_filter_paths)
1202 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1203 absolute_filter_paths)
1204 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1205
1206 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1207 per_file_coverage_summary)
1208 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1209 per_file_coverage_summary)
1210 _GenerateDirectoryViewHtmlIndexFile()
1211
1212 component_to_directories = _ExtractComponentToDirectoriesMapping()
1213 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1214 component_to_directories, per_directory_coverage_summary)
1215 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1216 component_to_directories,
1217 per_directory_coverage_summary)
1218 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331219
1220 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371221 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331222 _OverwriteHtmlReportsIndexFile()
1223
Yuke Liao506e8822017-12-04 16:52:541224 html_index_file_path = 'file://' + os.path.abspath(
1225 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101226 logging.info('Index file for html report is generated as: %s',
1227 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541228
Abhishek Arya1ec832c2017-12-05 18:06:591229
Yuke Liao506e8822017-12-04 16:52:541230if __name__ == '__main__':
1231 sys.exit(Main())