blob: f1ce980def01d2b51e43356898a58be4b3dd56fb [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 Liao80afff32018-03-07 01:26:20126# Caches the results returned by _GetBuildArgs, don't use this variable
127# directly, call _GetBuildArgs instead.
128_BUILD_ARGS = None
129
Yuke Liaoea228d02018-01-05 19:10:33130
131class _CoverageSummary(object):
132 """Encapsulates coverage summary representation."""
133
Yuke Liaodd1ec0592018-02-02 01:26:37134 def __init__(self,
135 regions_total=0,
136 regions_covered=0,
137 functions_total=0,
138 functions_covered=0,
139 lines_total=0,
140 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33141 """Initializes _CoverageSummary object."""
142 self._summary = {
143 'regions': {
144 'total': regions_total,
145 'covered': regions_covered
146 },
147 'functions': {
148 'total': functions_total,
149 'covered': functions_covered
150 },
151 'lines': {
152 'total': lines_total,
153 'covered': lines_covered
154 }
155 }
156
157 def Get(self):
158 """Returns summary as a dictionary."""
159 return self._summary
160
161 def AddSummary(self, other_summary):
162 """Adds another summary to this one element-wise."""
163 for feature in self._summary:
164 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
165 self._summary[feature]['covered'] += other_summary.Get()[feature][
166 'covered']
167
168
Yuke Liaodd1ec0592018-02-02 01:26:37169class _CoverageReportHtmlGenerator(object):
170 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33171
Yuke Liaodd1ec0592018-02-02 01:26:37172 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33173 """
174
Yuke Liaodd1ec0592018-02-02 01:26:37175 def __init__(self, output_path, table_entry_type):
176 """Initializes _CoverageReportHtmlGenerator object.
177
178 Args:
179 output_path: Path to the html report that will be generated.
180 table_entry_type: Type of the table entries to be displayed in the table
181 header. For example: 'Path', 'Component'.
182 """
Yuke Liaoea228d02018-01-05 19:10:33183 css_file_name = os.extsep.join(['style', 'css'])
184 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
185 assert os.path.exists(css_absolute_path), (
186 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
187 'is called first, and the css file is generated at: "%s"' %
188 css_absolute_path)
189
190 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37191 self._output_path = output_path
192 self._table_entry_type = table_entry_type
193
Yuke Liaoea228d02018-01-05 19:10:33194 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12195 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33196 template_dir = os.path.join(
197 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
198
199 jinja_env = jinja2.Environment(
200 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
201 self._header_template = jinja_env.get_template('header.html')
202 self._table_template = jinja_env.get_template('table.html')
203 self._footer_template = jinja_env.get_template('footer.html')
204
205 def AddLinkToAnotherReport(self, html_report_path, name, summary):
206 """Adds a link to another html report in this report.
207
208 The link to be added is assumed to be an entry in this directory.
209 """
Yuke Liaodd1ec0592018-02-02 01:26:37210 # Use relative paths instead of absolute paths to make the generated reports
211 # portable.
212 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
213 html_report_path, self._output_path)
214
Yuke Liaod54030e2018-01-08 17:34:12215 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37216 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12217 os.path.basename(html_report_path) ==
218 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
219 self._table_entries.append(table_entry)
220
221 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35222 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12223 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
224
225 def _CreateTableEntryFromCoverageSummary(self,
226 summary,
227 href=None,
228 name=None,
229 is_dir=None):
230 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37231 assert (href is None and name is None and is_dir is None) or (
232 href is not None and name is not None and is_dir is not None), (
233 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35234 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37235 'attributes must be None.')
236
Yuke Liaod54030e2018-01-08 17:34:12237 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37238 if href is not None:
239 entry['href'] = href
240 if name is not None:
241 entry['name'] = name
242 if is_dir is not None:
243 entry['is_dir'] = is_dir
244
Yuke Liaoea228d02018-01-05 19:10:33245 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12246 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37247 if summary_dict[feature]['total'] == 0:
248 percentage = 0.0
249 else:
Yuke Liaoa785f4d32018-02-13 21:41:35250 percentage = float(summary_dict[feature]['covered']) / summary_dict[
251 feature]['total'] * 100
252
Yuke Liaoea228d02018-01-05 19:10:33253 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12254 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33255 'total': summary_dict[feature]['total'],
256 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35257 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33258 'color_class': color_class
259 }
Yuke Liaod54030e2018-01-08 17:34:12260
Yuke Liaod54030e2018-01-08 17:34:12261 return entry
Yuke Liaoea228d02018-01-05 19:10:33262
263 def _GetColorClass(self, percentage):
264 """Returns the css color class based on coverage percentage."""
265 if percentage >= 0 and percentage < 80:
266 return 'red'
267 if percentage >= 80 and percentage < 100:
268 return 'yellow'
269 if percentage == 100:
270 return 'green'
271
272 assert False, 'Invalid coverage percentage: "%d"' % percentage
273
Yuke Liaodd1ec0592018-02-02 01:26:37274 def WriteHtmlCoverageReport(self):
275 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33276
277 In the report, sub-directories are displayed before files and within each
278 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33279 """
280
281 def EntryCmp(left, right):
282 """Compare function for table entries."""
283 if left['is_dir'] != right['is_dir']:
284 return -1 if left['is_dir'] == True else 1
285
Yuke Liaodd1ec0592018-02-02 01:26:37286 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33287
288 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
289
290 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37291 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
292 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
293 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
294
Yuke Liaoea228d02018-01-05 19:10:33295 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37296 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
297 directory_view_href=_GetRelativePathToDirectoryOfFile(
298 directory_view_path, self._output_path),
299 component_view_href=_GetRelativePathToDirectoryOfFile(
300 component_view_path, self._output_path),
301 file_view_href=_GetRelativePathToDirectoryOfFile(
302 file_view_path, self._output_path))
303
Yuke Liaod54030e2018-01-08 17:34:12304 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37305 entries=self._table_entries,
306 total_entry=self._total_entry,
307 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33308 html_footer = self._footer_template.render()
309
Yuke Liaodd1ec0592018-02-02 01:26:37310 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33311 html_file.write(html_header + html_table + html_footer)
312
Yuke Liao506e8822017-12-04 16:52:54313
Yuke Liaoc60b2d02018-03-02 21:40:43314def _GetHostPlatform():
315 """Returns the host platform.
316
317 This is separate from the target platform/os that coverage is running for.
318 """
Abhishek Arya1ec832c2017-12-05 18:06:59319 if sys.platform == 'win32' or sys.platform == 'cygwin':
320 return 'win'
321 if sys.platform.startswith('linux'):
322 return 'linux'
323 else:
324 assert sys.platform == 'darwin'
325 return 'mac'
326
327
Yuke Liaoc60b2d02018-03-02 21:40:43328def _GetTargetOS():
329 """Returns the target os specified in args.gn file.
330
331 Returns an empty string is target_os is not specified.
332 """
Yuke Liao80afff32018-03-07 01:26:20333 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43334 return build_args['target_os'] if 'target_os' in build_args else ''
335
336
Yuke Liaob2926832018-03-02 17:34:29337def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10338 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43339 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10340
341
Yuke Liao506e8822017-12-04 16:52:54342# TODO(crbug.com/759794): remove this function once tools get included to
343# Clang bundle:
344# https://chromium-review.googlesource.com/c/chromium/src/+/688221
345def DownloadCoverageToolsIfNeeded():
346 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59347
Yuke Liaoc60b2d02018-03-02 21:40:43348 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54349 """Returns a pair of revision number by reading the build stamp file.
350
351 Args:
352 stamp_file_path: A path the build stamp file created by
353 tools/clang/scripts/update.py.
354 Returns:
355 A pair of integers represeting the main and sub revision respectively.
356 """
357 if not os.path.exists(stamp_file_path):
358 return 0, 0
359
360 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43361 stamp_file_line = stamp_file.readline()
362 if ',' in stamp_file_line:
363 package_version = stamp_file_line.rstrip().split(',')[0]
364 else:
365 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54366
Yuke Liaoc60b2d02018-03-02 21:40:43367 clang_revision_str, clang_sub_revision_str = package_version.split('-')
368 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59369
Yuke Liaoc60b2d02018-03-02 21:40:43370 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54371 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43372 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54373
374 coverage_revision_stamp_file = os.path.join(
375 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
376 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43377 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54378
Yuke Liaoea228d02018-01-05 19:10:33379 has_coverage_tools = (
380 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32381
Yuke Liaoea228d02018-01-05 19:10:33382 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54383 coverage_sub_revision == clang_sub_revision):
384 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43385 return
Yuke Liao506e8822017-12-04 16:52:54386
387 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
388 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
389
390 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43391 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54392 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43393 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54394 coverage_tools_url = (
395 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43396 else:
397 assert host_platform == 'win'
398 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54399
400 try:
401 clang_update.DownloadAndUnpack(coverage_tools_url,
402 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10403 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54404 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43405 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54406 file_handle.write('\n')
407 except urllib2.URLError:
408 raise Exception(
409 'Failed to download coverage tools: %s.' % coverage_tools_url)
410
411
Yuke Liaodd1ec0592018-02-02 01:26:37412def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
413 filters):
Yuke Liao506e8822017-12-04 16:52:54414 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
415
416 For a file with absolute path /a/b/x.cc, a html report is generated as:
417 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
418 OUTPUT_DIR/index.html.
419
420 Args:
421 binary_paths: A list of paths to the instrumented binaries.
422 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42423 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54424 """
Yuke Liao506e8822017-12-04 16:52:54425 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
426 # [[-object BIN]] [SOURCES]
427 # NOTE: For object files, the first one is specified as a positional argument,
428 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10429 logging.debug('Generating per file line by line coverage reports using '
430 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59431 subprocess_cmd = [
432 LLVM_COV_PATH, 'show', '-format=html',
433 '-output-dir={}'.format(OUTPUT_DIR),
434 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
435 ]
436 subprocess_cmd.extend(
437 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29438 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42439 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54440 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10441 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54442
443
Yuke Liaodd1ec0592018-02-02 01:26:37444def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
445 """Generates html index file for file view."""
446 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
447 logging.debug('Generating file view html index file as: "%s".',
448 file_view_index_file_path)
449 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
450 'Path')
451 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33452
Yuke Liaodd1ec0592018-02-02 01:26:37453 for file_path in per_file_coverage_summary:
454 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
455
456 html_generator.AddLinkToAnotherReport(
457 _GetCoverageHtmlReportPathForFile(file_path),
458 os.path.relpath(file_path, SRC_ROOT_PATH),
459 per_file_coverage_summary[file_path])
460
461 html_generator.CreateTotalsEntry(totals_coverage_summary)
462 html_generator.WriteHtmlCoverageReport()
463 logging.debug('Finished generating file view html index file.')
464
465
466def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
467 """Calculates per directory coverage summary."""
468 logging.debug('Calculating per-directory coverage summary')
469 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
470
Yuke Liaoea228d02018-01-05 19:10:33471 for file_path in per_file_coverage_summary:
472 summary = per_file_coverage_summary[file_path]
473 parent_dir = os.path.dirname(file_path)
474 while True:
475 per_directory_coverage_summary[parent_dir].AddSummary(summary)
476
477 if parent_dir == SRC_ROOT_PATH:
478 break
479 parent_dir = os.path.dirname(parent_dir)
480
Yuke Liaodd1ec0592018-02-02 01:26:37481 logging.debug('Finished calculating per-directory coverage summary')
482 return per_directory_coverage_summary
483
484
485def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
486 per_file_coverage_summary):
487 """Generates per directory coverage breakdown in html."""
488 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33489 for dir_path in per_directory_coverage_summary:
490 _GenerateCoverageInHtmlForDirectory(
491 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
492
Yuke Liaodd1ec0592018-02-02 01:26:37493 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10494
Yuke Liaoea228d02018-01-05 19:10:33495
496def _GenerateCoverageInHtmlForDirectory(
497 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
498 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37499 html_generator = _CoverageReportHtmlGenerator(
500 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33501
502 for entry_name in os.listdir(dir_path):
503 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33504
Yuke Liaodd1ec0592018-02-02 01:26:37505 if entry_path in per_file_coverage_summary:
506 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
507 entry_coverage_summary = per_file_coverage_summary[entry_path]
508 elif entry_path in per_directory_coverage_summary:
509 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
510 entry_path)
511 entry_coverage_summary = per_directory_coverage_summary[entry_path]
512 else:
Yuke Liaoc7e607142018-02-05 20:26:14513 # Any file without executable lines shouldn't be included into the report.
514 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37515 continue
Yuke Liaoea228d02018-01-05 19:10:33516
Yuke Liaodd1ec0592018-02-02 01:26:37517 html_generator.AddLinkToAnotherReport(entry_html_report_path,
518 os.path.basename(entry_path),
519 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33520
Yuke Liaod54030e2018-01-08 17:34:12521 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37522 html_generator.WriteHtmlCoverageReport()
523
524
525def _GenerateDirectoryViewHtmlIndexFile():
526 """Generates the html index file for directory view.
527
528 Note that the index file is already generated under SRC_ROOT_PATH, so this
529 file simply redirects to it, and the reason of this extra layer is for
530 structural consistency with other views.
531 """
532 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
533 DIRECTORY_VIEW_INDEX_FILE)
534 logging.debug('Generating directory view html index file as: "%s".',
535 directory_view_index_file_path)
536 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
537 SRC_ROOT_PATH)
538 _WriteRedirectHtmlFile(directory_view_index_file_path,
539 src_root_html_report_path)
540 logging.debug('Finished generating directory view html index file.')
541
542
543def _CalculatePerComponentCoverageSummary(component_to_directories,
544 per_directory_coverage_summary):
545 """Calculates per component coverage summary."""
546 logging.debug('Calculating per-component coverage summary')
547 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
548
549 for component in component_to_directories:
550 for directory in component_to_directories[component]:
551 absolute_directory_path = os.path.abspath(directory)
552 if absolute_directory_path in per_directory_coverage_summary:
553 per_component_coverage_summary[component].AddSummary(
554 per_directory_coverage_summary[absolute_directory_path])
555
556 logging.debug('Finished calculating per-component coverage summary')
557 return per_component_coverage_summary
558
559
560def _ExtractComponentToDirectoriesMapping():
561 """Returns a mapping from components to directories."""
562 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
563 directory_to_component = component_mappings['dir-to-component']
564
565 component_to_directories = defaultdict(list)
566 for directory in directory_to_component:
567 component = directory_to_component[directory]
568 component_to_directories[component].append(directory)
569
570 return component_to_directories
571
572
573def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
574 component_to_directories,
575 per_directory_coverage_summary):
576 """Generates per-component coverage reports in html."""
577 logging.debug('Writing per-component coverage html reports.')
578 for component in per_component_coverage_summary:
579 _GenerateCoverageInHtmlForComponent(
580 component, per_component_coverage_summary, component_to_directories,
581 per_directory_coverage_summary)
582
583 logging.debug('Finished writing per-component coverage html reports.')
584
585
586def _GenerateCoverageInHtmlForComponent(
587 component_name, per_component_coverage_summary, component_to_directories,
588 per_directory_coverage_summary):
589 """Generates coverage html report for a component."""
590 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
591 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14592 component_html_report_dir = os.path.dirname(component_html_report_path)
593 if not os.path.exists(component_html_report_dir):
594 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37595
596 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
597 'Path')
598
599 for dir_path in component_to_directories[component_name]:
600 dir_absolute_path = os.path.abspath(dir_path)
601 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14602 # Any directory without an excercised file shouldn't be included into the
603 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37604 continue
605
606 html_generator.AddLinkToAnotherReport(
607 _GetCoverageHtmlReportPathForDirectory(dir_path),
608 os.path.relpath(dir_path, SRC_ROOT_PATH),
609 per_directory_coverage_summary[dir_absolute_path])
610
611 html_generator.CreateTotalsEntry(
612 per_component_coverage_summary[component_name])
613 html_generator.WriteHtmlCoverageReport()
614
615
616def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
617 """Generates the html index file for component view."""
618 component_view_index_file_path = os.path.join(OUTPUT_DIR,
619 COMPONENT_VIEW_INDEX_FILE)
620 logging.debug('Generating component view html index file as: "%s".',
621 component_view_index_file_path)
622 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
623 'Component')
624 totals_coverage_summary = _CoverageSummary()
625
626 for component in per_component_coverage_summary:
627 totals_coverage_summary.AddSummary(
628 per_component_coverage_summary[component])
629
630 html_generator.AddLinkToAnotherReport(
631 _GetCoverageHtmlReportPathForComponent(component), component,
632 per_component_coverage_summary[component])
633
634 html_generator.CreateTotalsEntry(totals_coverage_summary)
635 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14636 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33637
638
639def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37640 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33641 html_index_file_path = os.path.join(OUTPUT_DIR,
642 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37643 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
644 DIRECTORY_VIEW_INDEX_FILE)
645 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
646
647
648def _WriteRedirectHtmlFile(from_html_path, to_html_path):
649 """Writes a html file that redirects to another html file."""
650 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
651 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33652 content = ("""
653 <!DOCTYPE html>
654 <html>
655 <head>
656 <!-- HTML meta refresh URL redirection -->
657 <meta http-equiv="refresh" content="0; url=%s">
658 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37659 </html>""" % to_html_relative_path)
660 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33661 f.write(content)
662
663
Yuke Liaodd1ec0592018-02-02 01:26:37664def _GetCoverageHtmlReportPathForFile(file_path):
665 """Given a file path, returns the corresponding html report path."""
666 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
667 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
668
669 # '+' is used instead of os.path.join because both of them are absolute paths
670 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14671 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37672 return _GetCoverageReportRootDirPath() + html_report_path
673
674
675def _GetCoverageHtmlReportPathForDirectory(dir_path):
676 """Given a directory path, returns the corresponding html report path."""
677 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
678 html_report_path = os.path.join(
679 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
680
681 # '+' is used instead of os.path.join because both of them are absolute paths
682 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14683 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37684 return _GetCoverageReportRootDirPath() + html_report_path
685
686
687def _GetCoverageHtmlReportPathForComponent(component_name):
688 """Given a component, returns the corresponding html report path."""
689 component_file_name = component_name.lower().replace('>', '-')
690 html_report_name = os.extsep.join([component_file_name, 'html'])
691 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
692 html_report_name)
693
694
695def _GetCoverageReportRootDirPath():
696 """The root directory that contains all generated coverage html reports."""
697 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33698
699
Yuke Liao506e8822017-12-04 16:52:54700def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
701 """Builds and runs target to generate the coverage profile data.
702
703 Args:
704 targets: A list of targets to build with coverage instrumentation.
705 commands: A list of commands used to run the targets.
706 jobs_count: Number of jobs to run in parallel for building. If None, a
707 default value is derived based on CPUs availability.
708
709 Returns:
710 A relative path to the generated profdata file.
711 """
712 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59713 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
714 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54715 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
716 profraw_file_paths)
717
Yuke Liaod4a9865202018-01-12 23:17:52718 for profraw_file_path in profraw_file_paths:
719 os.remove(profraw_file_path)
720
Yuke Liao506e8822017-12-04 16:52:54721 return profdata_file_path
722
723
724def _BuildTargets(targets, jobs_count):
725 """Builds target with Clang coverage instrumentation.
726
727 This function requires current working directory to be the root of checkout.
728
729 Args:
730 targets: A list of targets to build with coverage instrumentation.
731 jobs_count: Number of jobs to run in parallel for compilation. If None, a
732 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54733 """
Abhishek Arya1ec832c2017-12-05 18:06:59734
Yuke Liao506e8822017-12-04 16:52:54735 def _IsGomaConfigured():
736 """Returns True if goma is enabled in the gn build args.
737
738 Returns:
739 A boolean indicates whether goma is configured for building or not.
740 """
Yuke Liao80afff32018-03-07 01:26:20741 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54742 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
743
Yuke Liao481d3482018-01-29 19:17:10744 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54745 if jobs_count is None and _IsGomaConfigured():
746 jobs_count = DEFAULT_GOMA_JOBS
747
748 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
749 if jobs_count is not None:
750 subprocess_cmd.append('-j' + str(jobs_count))
751
752 subprocess_cmd.extend(targets)
753 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10754 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54755
756
757def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
758 """Runs commands and returns the relative paths to the profraw data files.
759
760 Args:
761 targets: A list of targets built with coverage instrumentation.
762 commands: A list of commands used to run the targets.
763
764 Returns:
765 A list of relative paths to the generated profraw data files.
766 """
Yuke Liao481d3482018-01-29 19:17:10767 logging.debug('Executing the test commands')
768
Yuke Liao506e8822017-12-04 16:52:54769 # Remove existing profraw data files.
770 for file_or_dir in os.listdir(OUTPUT_DIR):
771 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
772 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
773
Yuke Liaoa0c8c2f2018-02-28 20:14:10774 profraw_file_paths = []
775
Yuke Liaod4a9865202018-01-12 23:17:52776 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54777 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10778 output_file_name = os.extsep.join([target + '_output', 'txt'])
779 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
780 logging.info('Running command: "%s", the output is redirected to "%s"',
781 command, output_file_path)
782
Yuke Liaob2926832018-03-02 17:34:29783 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10784 # On iOS platform, due to lack of write permissions, profraw files are
785 # generated outside of the OUTPUT_DIR, and the exact paths are contained
786 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29787 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10788 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
789 else:
790 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
791 output = _ExecuteCommand(target, command)
792
793 with open(output_file_path, 'w') as output_file:
794 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54795
Yuke Liao481d3482018-01-29 19:17:10796 logging.debug('Finished executing the test commands')
797
Yuke Liaob2926832018-03-02 17:34:29798 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10799 return profraw_file_paths
800
Yuke Liao506e8822017-12-04 16:52:54801 for file_or_dir in os.listdir(OUTPUT_DIR):
802 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
803 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
804
805 # Assert one target/command generates at least one profraw data file.
806 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59807 assert any(
808 os.path.basename(profraw_file).startswith(target)
809 for profraw_file in profraw_file_paths), (
810 'Running target: %s failed to generate any profraw data file, '
811 'please make sure the binary exists and is properly instrumented.' %
812 target)
Yuke Liao506e8822017-12-04 16:52:54813
814 return profraw_file_paths
815
816
817def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10818 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52819 # Per Clang "Source-based Code Coverage" doc:
820 # "%Nm" expands out to the instrumented binary's signature. When this pattern
821 # is specified, the runtime creates a pool of N raw profiles which are used
822 # for on-line profile merging. The runtime takes care of selecting a raw
823 # profile from the pool, locking it, and updating it before the program exits.
824 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
825 # N must be between 1 and 9. The merge pool specifier can only occur once per
826 # filename pattern.
827 #
828 # 4 is chosen because it creates some level of parallelism, but it's not too
829 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59830 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52831 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54832 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
833 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54834
Yuke Liaoa0c8c2f2018-02-28 20:14:10835 try:
836 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29837 shlex.split(command),
838 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10839 except subprocess.CalledProcessError as e:
840 output = e.output
841 logging.warning('Command: "%s" exited with non-zero return code', command)
842
843 return output
844
845
Yuke Liaob2926832018-03-02 17:34:29846def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10847 """Runs a single iOS command and generates a profraw data file.
848
849 iOS application doesn't have write access to folders outside of the app, so
850 it's impossible to instruct the app to flush the profraw data file to the
851 desired location. The profraw data file will be generated somewhere within the
852 application's Documents folder, and the full path can be obtained by parsing
853 the output.
854 """
Yuke Liaob2926832018-03-02 17:34:29855 assert _IsIOSCommand(command)
856
857 # After running tests, iossim generates a profraw data file, it won't be
858 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
859 # checkout.
860 iossim_profraw_file_path = os.path.join(
861 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10862
863 try:
Yuke Liaob2926832018-03-02 17:34:29864 output = subprocess.check_output(
865 shlex.split(command),
866 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10867 except subprocess.CalledProcessError as e:
868 # iossim emits non-zero return code even if tests run successfully, so
869 # ignore the return code.
870 output = e.output
871
872 return output
873
874
875def _GetProfrawDataFileByParsingOutput(output):
876 """Returns the path to the profraw data file obtained by parsing the output.
877
878 The output of running the test target has no format, but it is guaranteed to
879 have a single line containing the path to the generated profraw data file.
880 NOTE: This should only be called when target os is iOS.
881 """
Yuke Liaob2926832018-03-02 17:34:29882 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10883
Yuke Liaob2926832018-03-02 17:34:29884 output_by_lines = ''.join(output).splitlines()
885 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10886
887 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29888 result = profraw_file_pattern.match(line)
889 if result:
890 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10891
892 assert False, ('No profraw data file was generated, did you call '
893 'coverage_util::ConfigureCoverageReportPath() in test setup? '
894 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54895
896
897def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
898 """Returns a relative path to the profdata file by merging profraw data files.
899
900 Args:
901 profraw_file_paths: A list of relative paths to the profraw data files that
902 are to be merged.
903
904 Returns:
905 A relative path to the generated profdata file.
906
907 Raises:
908 CalledProcessError: An error occurred merging profraw data files.
909 """
Yuke Liao481d3482018-01-29 19:17:10910 logging.info('Creating the coverage profile data file')
911 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54912 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
913 try:
Abhishek Arya1ec832c2017-12-05 18:06:59914 subprocess_cmd = [
915 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
916 ]
Yuke Liao506e8822017-12-04 16:52:54917 subprocess_cmd.extend(profraw_file_paths)
918 subprocess.check_call(subprocess_cmd)
919 except subprocess.CalledProcessError as error:
920 print('Failed to merge profraw files to create profdata file')
921 raise error
922
Yuke Liao481d3482018-01-29 19:17:10923 logging.debug('Finished merging profraw files')
924 logging.info('Code coverage profile data is created as: %s',
925 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54926 return profdata_file_path
927
928
Yuke Liaoea228d02018-01-05 19:10:33929def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
930 """Generates per file coverage summary using "llvm-cov export" command."""
931 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
932 # [[-object BIN]] [SOURCES].
933 # NOTE: For object files, the first one is specified as a positional argument,
934 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10935 logging.debug('Generating per-file code coverage summary using "llvm-cov '
936 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33937 subprocess_cmd = [
938 LLVM_COV_PATH, 'export', '-summary-only',
939 '-instr-profile=' + profdata_file_path, binary_paths[0]
940 ]
941 subprocess_cmd.extend(
942 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29943 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33944 subprocess_cmd.extend(filters)
945
946 json_output = json.loads(subprocess.check_output(subprocess_cmd))
947 assert len(json_output['data']) == 1
948 files_coverage_data = json_output['data'][0]['files']
949
950 per_file_coverage_summary = {}
951 for file_coverage_data in files_coverage_data:
952 file_path = file_coverage_data['filename']
953 summary = file_coverage_data['summary']
954
Yuke Liaoea228d02018-01-05 19:10:33955 if summary['lines']['count'] == 0:
956 continue
957
958 per_file_coverage_summary[file_path] = _CoverageSummary(
959 regions_total=summary['regions']['count'],
960 regions_covered=summary['regions']['covered'],
961 functions_total=summary['functions']['count'],
962 functions_covered=summary['functions']['covered'],
963 lines_total=summary['lines']['count'],
964 lines_covered=summary['lines']['covered'])
965
Yuke Liao481d3482018-01-29 19:17:10966 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33967 return per_file_coverage_summary
968
969
Yuke Liaob2926832018-03-02 17:34:29970def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
971 """Appends -arch arguments to the command list if it's ios platform.
972
973 iOS binaries are universal binaries, and require specifying the architecture
974 to use, and one architecture needs to be specified for each binary.
975 """
976 if _IsIOS():
977 cmd_list.extend(['-arch=x86_64'] * num_archs)
978
979
Yuke Liao506e8822017-12-04 16:52:54980def _GetBinaryPath(command):
981 """Returns a relative path to the binary to be run by the command.
982
Yuke Liao545db322018-02-15 17:12:01983 Currently, following types of commands are supported (e.g. url_unittests):
984 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
985 2. Use xvfb.
986 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
987 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liaoa0c8c2f2018-02-28 20:14:10988 3. Use iossim to run tests on iOS platform.
989 3.1. "out/Coverage-iphonesimulator/iossim
990 out/Coverage-iphonesimulator/url_unittests.app <arguments>"
Yuke Liao545db322018-02-15 17:12:01991
Yuke Liao506e8822017-12-04 16:52:54992 Args:
993 command: A command used to run a target.
994
995 Returns:
996 A relative path to the binary.
997 """
Yuke Liao545db322018-02-15 17:12:01998 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
999
Yuke Liaob2926832018-03-02 17:34:291000 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011001 if os.path.basename(command_parts[0]) == 'python':
1002 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1003 'This tool doesn\'t understand the command: "%s"' % command)
1004 return command_parts[2]
1005
1006 if os.path.basename(command_parts[0]) == xvfb_script_name:
1007 return command_parts[1]
1008
Yuke Liaob2926832018-03-02 17:34:291009 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101010 # For a given application bundle, the binary resides in the bundle and has
1011 # the same name with the application without the .app extension.
1012 app_path = command_parts[1]
1013 app_name = os.path.splitext(os.path.basename(app_path))[0]
1014 return os.path.join(app_path, app_name)
1015
Yuke Liaob2926832018-03-02 17:34:291016 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541017
1018
Yuke Liaob2926832018-03-02 17:34:291019def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101020 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291021 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101022
1023
Yuke Liao95d13d72017-12-07 18:18:501024def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1025 """Verifies that the target executables specified in the commands are inside
1026 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541027 for command in commands:
1028 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501029 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1030 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1031 'Target executable "%s" in command: "%s" is outside of '
1032 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541033
1034
1035def _ValidateBuildingWithClangCoverage():
1036 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201037 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541038
1039 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1040 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591041 assert False, ('\'{} = true\' is required in args.gn.'
1042 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541043
1044
Yuke Liaoc60b2d02018-03-02 21:40:431045def _ValidateCurrentPlatformIsSupported():
1046 """Asserts that this script suports running on the current platform"""
1047 target_os = _GetTargetOS()
1048 if target_os:
1049 current_platform = target_os
1050 else:
1051 current_platform = _GetHostPlatform()
1052
1053 assert current_platform in [
1054 'linux', 'mac', 'chromeos', 'ios'
1055 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1056
1057
Yuke Liao80afff32018-03-07 01:26:201058def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541059 """Parses args.gn file and returns results as a dictionary.
1060
1061 Returns:
1062 A dictionary representing the build args.
1063 """
Yuke Liao80afff32018-03-07 01:26:201064 global _BUILD_ARGS
1065 if _BUILD_ARGS is not None:
1066 return _BUILD_ARGS
1067
1068 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541069 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1070 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1071 'missing args.gn file.' % BUILD_DIR)
1072 with open(build_args_path) as build_args_file:
1073 build_args_lines = build_args_file.readlines()
1074
Yuke Liao506e8822017-12-04 16:52:541075 for build_arg_line in build_args_lines:
1076 build_arg_without_comments = build_arg_line.split('#')[0]
1077 key_value_pair = build_arg_without_comments.split('=')
1078 if len(key_value_pair) != 2:
1079 continue
1080
1081 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431082
1083 # Values are wrapped within a pair of double-quotes, so remove the leading
1084 # and trailing double-quotes.
1085 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201086 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541087
Yuke Liao80afff32018-03-07 01:26:201088 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541089
1090
Abhishek Arya16f059a2017-12-07 17:47:321091def _VerifyPathsAndReturnAbsolutes(paths):
1092 """Verifies that the paths specified in |paths| exist and returns absolute
1093 versions.
Yuke Liao66da1732017-12-05 22:19:421094
1095 Args:
1096 paths: A list of files or directories.
1097 """
Abhishek Arya16f059a2017-12-07 17:47:321098 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421099 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321100 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1101 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1102
1103 absolute_paths.append(absolute_path)
1104
1105 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421106
1107
Yuke Liaodd1ec0592018-02-02 01:26:371108def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1109 """Returns a target path relative to the directory of base_path.
1110
1111 This method requires base_path to be a file, otherwise, one should call
1112 os.path.relpath directly.
1113 """
1114 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141115 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371116 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141117 base_dir = os.path.dirname(base_path)
1118 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371119
1120
Yuke Liao506e8822017-12-04 16:52:541121def _ParseCommandArguments():
1122 """Adds and parses relevant arguments for tool comands.
1123
1124 Returns:
1125 A dictionary representing the arguments.
1126 """
1127 arg_parser = argparse.ArgumentParser()
1128 arg_parser.usage = __doc__
1129
Abhishek Arya1ec832c2017-12-05 18:06:591130 arg_parser.add_argument(
1131 '-b',
1132 '--build-dir',
1133 type=str,
1134 required=True,
1135 help='The build directory, the path needs to be relative to the root of '
1136 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541137
Abhishek Arya1ec832c2017-12-05 18:06:591138 arg_parser.add_argument(
1139 '-o',
1140 '--output-dir',
1141 type=str,
1142 required=True,
1143 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541144
Abhishek Arya1ec832c2017-12-05 18:06:591145 arg_parser.add_argument(
1146 '-c',
1147 '--command',
1148 action='append',
1149 required=True,
1150 help='Commands used to run test targets, one test target needs one and '
1151 'only one command, when specifying commands, one should assume the '
1152 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541153
Abhishek Arya1ec832c2017-12-05 18:06:591154 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421155 '-f',
1156 '--filters',
1157 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321158 required=False,
Yuke Liao66da1732017-12-05 22:19:421159 help='Directories or files to get code coverage for, and all files under '
1160 'the directories are included recursively.')
1161
1162 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591163 '-j',
1164 '--jobs',
1165 type=int,
1166 default=None,
1167 help='Run N jobs to build in parallel. If not specified, a default value '
1168 'will be derived based on CPUs availability. Please refer to '
1169 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541170
Abhishek Arya1ec832c2017-12-05 18:06:591171 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101172 '-v',
1173 '--verbose',
1174 action='store_true',
1175 help='Prints additional output for diagnostics.')
1176
1177 arg_parser.add_argument(
1178 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1179
1180 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591181 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541182
1183 args = arg_parser.parse_args()
1184 return args
1185
1186
1187def Main():
1188 """Execute tool commands."""
1189 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1190 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591191 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541192 args = _ParseCommandArguments()
1193 global BUILD_DIR
1194 BUILD_DIR = args.build_dir
1195 global OUTPUT_DIR
1196 OUTPUT_DIR = args.output_dir
1197
1198 assert len(args.targets) == len(args.command), ('Number of targets must be '
1199 'equal to the number of test '
1200 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431201
1202 # logging should be configured before it is used.
1203 log_level = logging.DEBUG if args.verbose else logging.INFO
1204 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1205 log_file = args.log_file if args.log_file else None
1206 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1207
Abhishek Arya1ec832c2017-12-05 18:06:591208 assert os.path.exists(BUILD_DIR), (
1209 'Build directory: {} doesn\'t exist. '
1210 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431211 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541212 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501213 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321214
Yuke Liaoc60b2d02018-03-02 21:40:431215 DownloadCoverageToolsIfNeeded()
1216
Abhishek Arya16f059a2017-12-07 17:47:321217 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421218 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321219 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421220
Yuke Liao506e8822017-12-04 16:52:541221 if not os.path.exists(OUTPUT_DIR):
1222 os.makedirs(OUTPUT_DIR)
1223
Abhishek Arya1ec832c2017-12-05 18:06:591224 profdata_file_path = _CreateCoverageProfileDataForTargets(
1225 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541226 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331227
Yuke Liao481d3482018-01-29 19:17:101228 logging.info('Generating code coverage report in html (this can take a while '
1229 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371230 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1231 binary_paths, profdata_file_path, absolute_filter_paths)
1232 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1233 absolute_filter_paths)
1234 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1235
1236 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1237 per_file_coverage_summary)
1238 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1239 per_file_coverage_summary)
1240 _GenerateDirectoryViewHtmlIndexFile()
1241
1242 component_to_directories = _ExtractComponentToDirectoriesMapping()
1243 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1244 component_to_directories, per_directory_coverage_summary)
1245 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1246 component_to_directories,
1247 per_directory_coverage_summary)
1248 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331249
1250 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371251 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331252 _OverwriteHtmlReportsIndexFile()
1253
Yuke Liao506e8822017-12-04 16:52:541254 html_index_file_path = 'file://' + os.path.abspath(
1255 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101256 logging.info('Index file for html report is generated as: %s',
1257 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541258
Abhishek Arya1ec832c2017-12-05 18:06:591259
Yuke Liao506e8822017-12-04 16:52:541260if __name__ == '__main__':
1261 sys.exit(Main())