blob: 8bfdafe99d7c18296ce98693f9cdf2770cf4d0dd [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Yuke Liaod3b46272018-03-14 18:25:1414 Existing implementation requires "is_component_build=false" flag because
15 coverage info for dynamic libraries may be missing and "is_component_build"
16 is set to true by "is_debug" unless it is explicitly set to false.
Yuke Liao506e8822017-12-04 16:52:5417
Abhishek Arya1ec832c2017-12-05 18:06:5918 Example usage:
19
Abhishek Arya16f059a2017-12-07 17:47:3220 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
21 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5922 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3223 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
24 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
25 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5926
Abhishek Arya16f059a2017-12-07 17:47:3227 The command above builds crypto_unittests and url_unittests targets and then
28 runs them with specified command line arguments. For url_unittests, it only
29 runs the test URLParser.PathURL. The coverage report is filtered to include
30 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5931
Yuke Liao545db322018-02-15 17:12:0132 If you want to run tests that try to draw to the screen but don't have a
33 display connected, you can run tests in headless mode with xvfb.
34
35 Sample flow for running a test target with xvfb (e.g. unit_tests):
36
37 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
38 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
39
Abhishek Arya1ec832c2017-12-05 18:06:5940 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
41 flag as well.
42
43 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
44
Abhishek Arya16f059a2017-12-07 17:47:3245 python tools/code_coverage/coverage.py pdfium_fuzzer \\
46 -b out/coverage -o out/report \\
47 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
48 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5949
50 where:
51 <corpus_dir> - directory containing samples files for this format.
52 <runs> - number of times to fuzz target function. Should be 0 when you just
53 want to see the coverage on corpus and don't want to fuzz at all.
54
55 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao506e8822017-12-04 16:52:5456"""
57
58from __future__ import print_function
59
60import sys
61
62import argparse
Yuke Liaoea228d02018-01-05 19:10:3363import json
Yuke Liao481d3482018-01-29 19:17:1064import logging
Yuke Liao506e8822017-12-04 16:52:5465import os
Yuke Liaob2926832018-03-02 17:34:2966import re
67import shlex
Yuke Liao506e8822017-12-04 16:52:5468import subprocess
Yuke Liao506e8822017-12-04 16:52:5469import urllib2
70
Abhishek Arya1ec832c2017-12-05 18:06:5971sys.path.append(
72 os.path.join(
73 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
74 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5475import update as clang_update
76
Yuke Liaoea228d02018-01-05 19:10:3377sys.path.append(
78 os.path.join(
79 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
80 'third_party'))
81import jinja2
82from collections import defaultdict
83
Yuke Liao506e8822017-12-04 16:52:5484# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5985SRC_ROOT_PATH = os.path.abspath(
86 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5487
88# Absolute path to the code coverage tools binary.
89LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
90LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
91LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
92
93# Build directory, the value is parsed from command line arguments.
94BUILD_DIR = None
95
96# Output directory for generated artifacts, the value is parsed from command
97# line arguemnts.
98OUTPUT_DIR = None
99
100# Default number of jobs used to build when goma is configured and enabled.
101DEFAULT_GOMA_JOBS = 100
102
103# Name of the file extension for profraw data files.
104PROFRAW_FILE_EXTENSION = 'profraw'
105
106# Name of the final profdata file, and this file needs to be passed to
107# "llvm-cov" command in order to call "llvm-cov show" to inspect the
108# line-by-line coverage of specific files.
109PROFDATA_FILE_NAME = 'coverage.profdata'
110
111# Build arg required for generating code coverage data.
112CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
113
Yuke Liaoea228d02018-01-05 19:10:33114# The default name of the html coverage report for a directory.
115DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
116
Yuke Liaodd1ec0592018-02-02 01:26:37117# Name of the html index files for different views.
118DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
119COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
120FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
121
122# Used to extract a mapping between directories and components.
123COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
124
Yuke Liao80afff32018-03-07 01:26:20125# Caches the results returned by _GetBuildArgs, don't use this variable
126# directly, call _GetBuildArgs instead.
127_BUILD_ARGS = None
128
Yuke Liaoea228d02018-01-05 19:10:33129
130class _CoverageSummary(object):
131 """Encapsulates coverage summary representation."""
132
Yuke Liaodd1ec0592018-02-02 01:26:37133 def __init__(self,
134 regions_total=0,
135 regions_covered=0,
136 functions_total=0,
137 functions_covered=0,
138 lines_total=0,
139 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33140 """Initializes _CoverageSummary object."""
141 self._summary = {
142 'regions': {
143 'total': regions_total,
144 'covered': regions_covered
145 },
146 'functions': {
147 'total': functions_total,
148 'covered': functions_covered
149 },
150 'lines': {
151 'total': lines_total,
152 'covered': lines_covered
153 }
154 }
155
156 def Get(self):
157 """Returns summary as a dictionary."""
158 return self._summary
159
160 def AddSummary(self, other_summary):
161 """Adds another summary to this one element-wise."""
162 for feature in self._summary:
163 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
164 self._summary[feature]['covered'] += other_summary.Get()[feature][
165 'covered']
166
167
Yuke Liaodd1ec0592018-02-02 01:26:37168class _CoverageReportHtmlGenerator(object):
169 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33170
Yuke Liaodd1ec0592018-02-02 01:26:37171 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33172 """
173
Yuke Liaodd1ec0592018-02-02 01:26:37174 def __init__(self, output_path, table_entry_type):
175 """Initializes _CoverageReportHtmlGenerator object.
176
177 Args:
178 output_path: Path to the html report that will be generated.
179 table_entry_type: Type of the table entries to be displayed in the table
180 header. For example: 'Path', 'Component'.
181 """
Yuke Liaoea228d02018-01-05 19:10:33182 css_file_name = os.extsep.join(['style', 'css'])
183 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
184 assert os.path.exists(css_absolute_path), (
185 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
186 'is called first, and the css file is generated at: "%s"' %
187 css_absolute_path)
188
189 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37190 self._output_path = output_path
191 self._table_entry_type = table_entry_type
192
Yuke Liaoea228d02018-01-05 19:10:33193 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12194 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33195 template_dir = os.path.join(
196 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
197
198 jinja_env = jinja2.Environment(
199 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
200 self._header_template = jinja_env.get_template('header.html')
201 self._table_template = jinja_env.get_template('table.html')
202 self._footer_template = jinja_env.get_template('footer.html')
203
204 def AddLinkToAnotherReport(self, html_report_path, name, summary):
205 """Adds a link to another html report in this report.
206
207 The link to be added is assumed to be an entry in this directory.
208 """
Yuke Liaodd1ec0592018-02-02 01:26:37209 # Use relative paths instead of absolute paths to make the generated reports
210 # portable.
211 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
212 html_report_path, self._output_path)
213
Yuke Liaod54030e2018-01-08 17:34:12214 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37215 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12216 os.path.basename(html_report_path) ==
217 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
218 self._table_entries.append(table_entry)
219
220 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35221 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12222 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
223
224 def _CreateTableEntryFromCoverageSummary(self,
225 summary,
226 href=None,
227 name=None,
228 is_dir=None):
229 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37230 assert (href is None and name is None and is_dir is None) or (
231 href is not None and name is not None and is_dir is not None), (
232 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35233 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37234 'attributes must be None.')
235
Yuke Liaod54030e2018-01-08 17:34:12236 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37237 if href is not None:
238 entry['href'] = href
239 if name is not None:
240 entry['name'] = name
241 if is_dir is not None:
242 entry['is_dir'] = is_dir
243
Yuke Liaoea228d02018-01-05 19:10:33244 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12245 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37246 if summary_dict[feature]['total'] == 0:
247 percentage = 0.0
248 else:
Yuke Liaoa785f4d32018-02-13 21:41:35249 percentage = float(summary_dict[feature]['covered']) / summary_dict[
250 feature]['total'] * 100
251
Yuke Liaoea228d02018-01-05 19:10:33252 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12253 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33254 'total': summary_dict[feature]['total'],
255 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35256 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33257 'color_class': color_class
258 }
Yuke Liaod54030e2018-01-08 17:34:12259
Yuke Liaod54030e2018-01-08 17:34:12260 return entry
Yuke Liaoea228d02018-01-05 19:10:33261
262 def _GetColorClass(self, percentage):
263 """Returns the css color class based on coverage percentage."""
264 if percentage >= 0 and percentage < 80:
265 return 'red'
266 if percentage >= 80 and percentage < 100:
267 return 'yellow'
268 if percentage == 100:
269 return 'green'
270
271 assert False, 'Invalid coverage percentage: "%d"' % percentage
272
Yuke Liaodd1ec0592018-02-02 01:26:37273 def WriteHtmlCoverageReport(self):
274 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33275
276 In the report, sub-directories are displayed before files and within each
277 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33278 """
279
280 def EntryCmp(left, right):
281 """Compare function for table entries."""
282 if left['is_dir'] != right['is_dir']:
283 return -1 if left['is_dir'] == True else 1
284
Yuke Liaodd1ec0592018-02-02 01:26:37285 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33286
287 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
288
289 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37290 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
291 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
292 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
293
Yuke Liaoea228d02018-01-05 19:10:33294 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37295 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
296 directory_view_href=_GetRelativePathToDirectoryOfFile(
297 directory_view_path, self._output_path),
298 component_view_href=_GetRelativePathToDirectoryOfFile(
299 component_view_path, self._output_path),
300 file_view_href=_GetRelativePathToDirectoryOfFile(
301 file_view_path, self._output_path))
302
Yuke Liaod54030e2018-01-08 17:34:12303 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37304 entries=self._table_entries,
305 total_entry=self._total_entry,
306 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33307 html_footer = self._footer_template.render()
308
Yuke Liaodd1ec0592018-02-02 01:26:37309 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33310 html_file.write(html_header + html_table + html_footer)
311
Yuke Liao506e8822017-12-04 16:52:54312
Yuke Liaoc60b2d02018-03-02 21:40:43313def _GetHostPlatform():
314 """Returns the host platform.
315
316 This is separate from the target platform/os that coverage is running for.
317 """
Abhishek Arya1ec832c2017-12-05 18:06:59318 if sys.platform == 'win32' or sys.platform == 'cygwin':
319 return 'win'
320 if sys.platform.startswith('linux'):
321 return 'linux'
322 else:
323 assert sys.platform == 'darwin'
324 return 'mac'
325
326
Yuke Liaoc60b2d02018-03-02 21:40:43327def _GetTargetOS():
328 """Returns the target os specified in args.gn file.
329
330 Returns an empty string is target_os is not specified.
331 """
Yuke Liao80afff32018-03-07 01:26:20332 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43333 return build_args['target_os'] if 'target_os' in build_args else ''
334
335
Yuke Liaob2926832018-03-02 17:34:29336def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10337 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43338 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10339
340
Yuke Liao506e8822017-12-04 16:52:54341# TODO(crbug.com/759794): remove this function once tools get included to
342# Clang bundle:
343# https://chromium-review.googlesource.com/c/chromium/src/+/688221
344def DownloadCoverageToolsIfNeeded():
345 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59346
Yuke Liaoc60b2d02018-03-02 21:40:43347 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54348 """Returns a pair of revision number by reading the build stamp file.
349
350 Args:
351 stamp_file_path: A path the build stamp file created by
352 tools/clang/scripts/update.py.
353 Returns:
354 A pair of integers represeting the main and sub revision respectively.
355 """
356 if not os.path.exists(stamp_file_path):
357 return 0, 0
358
359 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43360 stamp_file_line = stamp_file.readline()
361 if ',' in stamp_file_line:
362 package_version = stamp_file_line.rstrip().split(',')[0]
363 else:
364 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54365
Yuke Liaoc60b2d02018-03-02 21:40:43366 clang_revision_str, clang_sub_revision_str = package_version.split('-')
367 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59368
Yuke Liaoc60b2d02018-03-02 21:40:43369 host_platform = _GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54370 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43371 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54372
373 coverage_revision_stamp_file = os.path.join(
374 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
375 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43376 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54377
Yuke Liaoea228d02018-01-05 19:10:33378 has_coverage_tools = (
379 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32380
Yuke Liaoea228d02018-01-05 19:10:33381 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54382 coverage_sub_revision == clang_sub_revision):
383 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43384 return
Yuke Liao506e8822017-12-04 16:52:54385
386 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
387 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
388
389 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43390 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54391 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43392 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54393 coverage_tools_url = (
394 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43395 else:
396 assert host_platform == 'win'
397 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54398
399 try:
400 clang_update.DownloadAndUnpack(coverage_tools_url,
401 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10402 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54403 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43404 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54405 file_handle.write('\n')
406 except urllib2.URLError:
407 raise Exception(
408 'Failed to download coverage tools: %s.' % coverage_tools_url)
409
410
Yuke Liaodd1ec0592018-02-02 01:26:37411def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
412 filters):
Yuke Liao506e8822017-12-04 16:52:54413 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
414
415 For a file with absolute path /a/b/x.cc, a html report is generated as:
416 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
417 OUTPUT_DIR/index.html.
418
419 Args:
420 binary_paths: A list of paths to the instrumented binaries.
421 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42422 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54423 """
Yuke Liao506e8822017-12-04 16:52:54424 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
425 # [[-object BIN]] [SOURCES]
426 # NOTE: For object files, the first one is specified as a positional argument,
427 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10428 logging.debug('Generating per file line by line coverage reports using '
429 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59430 subprocess_cmd = [
431 LLVM_COV_PATH, 'show', '-format=html',
432 '-output-dir={}'.format(OUTPUT_DIR),
433 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
434 ]
435 subprocess_cmd.extend(
436 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29437 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liao66da1732017-12-05 22:19:42438 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54439 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10440 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54441
442
Yuke Liaodd1ec0592018-02-02 01:26:37443def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
444 """Generates html index file for file view."""
445 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
446 logging.debug('Generating file view html index file as: "%s".',
447 file_view_index_file_path)
448 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
449 'Path')
450 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33451
Yuke Liaodd1ec0592018-02-02 01:26:37452 for file_path in per_file_coverage_summary:
453 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
454
455 html_generator.AddLinkToAnotherReport(
456 _GetCoverageHtmlReportPathForFile(file_path),
457 os.path.relpath(file_path, SRC_ROOT_PATH),
458 per_file_coverage_summary[file_path])
459
460 html_generator.CreateTotalsEntry(totals_coverage_summary)
461 html_generator.WriteHtmlCoverageReport()
462 logging.debug('Finished generating file view html index file.')
463
464
465def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
466 """Calculates per directory coverage summary."""
467 logging.debug('Calculating per-directory coverage summary')
468 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
469
Yuke Liaoea228d02018-01-05 19:10:33470 for file_path in per_file_coverage_summary:
471 summary = per_file_coverage_summary[file_path]
472 parent_dir = os.path.dirname(file_path)
473 while True:
474 per_directory_coverage_summary[parent_dir].AddSummary(summary)
475
476 if parent_dir == SRC_ROOT_PATH:
477 break
478 parent_dir = os.path.dirname(parent_dir)
479
Yuke Liaodd1ec0592018-02-02 01:26:37480 logging.debug('Finished calculating per-directory coverage summary')
481 return per_directory_coverage_summary
482
483
484def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
485 per_file_coverage_summary):
486 """Generates per directory coverage breakdown in html."""
487 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33488 for dir_path in per_directory_coverage_summary:
489 _GenerateCoverageInHtmlForDirectory(
490 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
491
Yuke Liaodd1ec0592018-02-02 01:26:37492 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10493
Yuke Liaoea228d02018-01-05 19:10:33494
495def _GenerateCoverageInHtmlForDirectory(
496 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
497 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37498 html_generator = _CoverageReportHtmlGenerator(
499 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33500
501 for entry_name in os.listdir(dir_path):
502 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33503
Yuke Liaodd1ec0592018-02-02 01:26:37504 if entry_path in per_file_coverage_summary:
505 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
506 entry_coverage_summary = per_file_coverage_summary[entry_path]
507 elif entry_path in per_directory_coverage_summary:
508 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
509 entry_path)
510 entry_coverage_summary = per_directory_coverage_summary[entry_path]
511 else:
Yuke Liaoc7e607142018-02-05 20:26:14512 # Any file without executable lines shouldn't be included into the report.
513 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37514 continue
Yuke Liaoea228d02018-01-05 19:10:33515
Yuke Liaodd1ec0592018-02-02 01:26:37516 html_generator.AddLinkToAnotherReport(entry_html_report_path,
517 os.path.basename(entry_path),
518 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33519
Yuke Liaod54030e2018-01-08 17:34:12520 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37521 html_generator.WriteHtmlCoverageReport()
522
523
524def _GenerateDirectoryViewHtmlIndexFile():
525 """Generates the html index file for directory view.
526
527 Note that the index file is already generated under SRC_ROOT_PATH, so this
528 file simply redirects to it, and the reason of this extra layer is for
529 structural consistency with other views.
530 """
531 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
532 DIRECTORY_VIEW_INDEX_FILE)
533 logging.debug('Generating directory view html index file as: "%s".',
534 directory_view_index_file_path)
535 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
536 SRC_ROOT_PATH)
537 _WriteRedirectHtmlFile(directory_view_index_file_path,
538 src_root_html_report_path)
539 logging.debug('Finished generating directory view html index file.')
540
541
542def _CalculatePerComponentCoverageSummary(component_to_directories,
543 per_directory_coverage_summary):
544 """Calculates per component coverage summary."""
545 logging.debug('Calculating per-component coverage summary')
546 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
547
548 for component in component_to_directories:
549 for directory in component_to_directories[component]:
550 absolute_directory_path = os.path.abspath(directory)
551 if absolute_directory_path in per_directory_coverage_summary:
552 per_component_coverage_summary[component].AddSummary(
553 per_directory_coverage_summary[absolute_directory_path])
554
555 logging.debug('Finished calculating per-component coverage summary')
556 return per_component_coverage_summary
557
558
559def _ExtractComponentToDirectoriesMapping():
560 """Returns a mapping from components to directories."""
561 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
562 directory_to_component = component_mappings['dir-to-component']
563
564 component_to_directories = defaultdict(list)
565 for directory in directory_to_component:
566 component = directory_to_component[directory]
567 component_to_directories[component].append(directory)
568
569 return component_to_directories
570
571
572def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
573 component_to_directories,
574 per_directory_coverage_summary):
575 """Generates per-component coverage reports in html."""
576 logging.debug('Writing per-component coverage html reports.')
577 for component in per_component_coverage_summary:
578 _GenerateCoverageInHtmlForComponent(
579 component, per_component_coverage_summary, component_to_directories,
580 per_directory_coverage_summary)
581
582 logging.debug('Finished writing per-component coverage html reports.')
583
584
585def _GenerateCoverageInHtmlForComponent(
586 component_name, per_component_coverage_summary, component_to_directories,
587 per_directory_coverage_summary):
588 """Generates coverage html report for a component."""
589 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
590 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14591 component_html_report_dir = os.path.dirname(component_html_report_path)
592 if not os.path.exists(component_html_report_dir):
593 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37594
595 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
596 'Path')
597
598 for dir_path in component_to_directories[component_name]:
599 dir_absolute_path = os.path.abspath(dir_path)
600 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14601 # Any directory without an excercised file shouldn't be included into the
602 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37603 continue
604
605 html_generator.AddLinkToAnotherReport(
606 _GetCoverageHtmlReportPathForDirectory(dir_path),
607 os.path.relpath(dir_path, SRC_ROOT_PATH),
608 per_directory_coverage_summary[dir_absolute_path])
609
610 html_generator.CreateTotalsEntry(
611 per_component_coverage_summary[component_name])
612 html_generator.WriteHtmlCoverageReport()
613
614
615def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
616 """Generates the html index file for component view."""
617 component_view_index_file_path = os.path.join(OUTPUT_DIR,
618 COMPONENT_VIEW_INDEX_FILE)
619 logging.debug('Generating component view html index file as: "%s".',
620 component_view_index_file_path)
621 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
622 'Component')
623 totals_coverage_summary = _CoverageSummary()
624
625 for component in per_component_coverage_summary:
626 totals_coverage_summary.AddSummary(
627 per_component_coverage_summary[component])
628
629 html_generator.AddLinkToAnotherReport(
630 _GetCoverageHtmlReportPathForComponent(component), component,
631 per_component_coverage_summary[component])
632
633 html_generator.CreateTotalsEntry(totals_coverage_summary)
634 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14635 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33636
637
638def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37639 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33640 html_index_file_path = os.path.join(OUTPUT_DIR,
641 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37642 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
643 DIRECTORY_VIEW_INDEX_FILE)
644 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
645
646
647def _WriteRedirectHtmlFile(from_html_path, to_html_path):
648 """Writes a html file that redirects to another html file."""
649 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
650 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33651 content = ("""
652 <!DOCTYPE html>
653 <html>
654 <head>
655 <!-- HTML meta refresh URL redirection -->
656 <meta http-equiv="refresh" content="0; url=%s">
657 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37658 </html>""" % to_html_relative_path)
659 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33660 f.write(content)
661
662
Yuke Liaodd1ec0592018-02-02 01:26:37663def _GetCoverageHtmlReportPathForFile(file_path):
664 """Given a file path, returns the corresponding html report path."""
665 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
666 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
667
668 # '+' is used instead of os.path.join because both of them are absolute paths
669 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14670 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37671 return _GetCoverageReportRootDirPath() + html_report_path
672
673
674def _GetCoverageHtmlReportPathForDirectory(dir_path):
675 """Given a directory path, returns the corresponding html report path."""
676 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
677 html_report_path = os.path.join(
678 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
679
680 # '+' is used instead of os.path.join because both of them are absolute paths
681 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14682 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37683 return _GetCoverageReportRootDirPath() + html_report_path
684
685
686def _GetCoverageHtmlReportPathForComponent(component_name):
687 """Given a component, returns the corresponding html report path."""
688 component_file_name = component_name.lower().replace('>', '-')
689 html_report_name = os.extsep.join([component_file_name, 'html'])
690 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
691 html_report_name)
692
693
694def _GetCoverageReportRootDirPath():
695 """The root directory that contains all generated coverage html reports."""
696 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33697
698
Yuke Liao506e8822017-12-04 16:52:54699def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
700 """Builds and runs target to generate the coverage profile data.
701
702 Args:
703 targets: A list of targets to build with coverage instrumentation.
704 commands: A list of commands used to run the targets.
705 jobs_count: Number of jobs to run in parallel for building. If None, a
706 default value is derived based on CPUs availability.
707
708 Returns:
709 A relative path to the generated profdata file.
710 """
711 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59712 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
713 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54714 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
715 profraw_file_paths)
716
Yuke Liaod4a9865202018-01-12 23:17:52717 for profraw_file_path in profraw_file_paths:
718 os.remove(profraw_file_path)
719
Yuke Liao506e8822017-12-04 16:52:54720 return profdata_file_path
721
722
723def _BuildTargets(targets, jobs_count):
724 """Builds target with Clang coverage instrumentation.
725
726 This function requires current working directory to be the root of checkout.
727
728 Args:
729 targets: A list of targets to build with coverage instrumentation.
730 jobs_count: Number of jobs to run in parallel for compilation. If None, a
731 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54732 """
Abhishek Arya1ec832c2017-12-05 18:06:59733
Yuke Liao506e8822017-12-04 16:52:54734 def _IsGomaConfigured():
735 """Returns True if goma is enabled in the gn build args.
736
737 Returns:
738 A boolean indicates whether goma is configured for building or not.
739 """
Yuke Liao80afff32018-03-07 01:26:20740 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54741 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
742
Yuke Liao481d3482018-01-29 19:17:10743 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54744 if jobs_count is None and _IsGomaConfigured():
745 jobs_count = DEFAULT_GOMA_JOBS
746
747 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
748 if jobs_count is not None:
749 subprocess_cmd.append('-j' + str(jobs_count))
750
751 subprocess_cmd.extend(targets)
752 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10753 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54754
755
756def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
757 """Runs commands and returns the relative paths to the profraw data files.
758
759 Args:
760 targets: A list of targets built with coverage instrumentation.
761 commands: A list of commands used to run the targets.
762
763 Returns:
764 A list of relative paths to the generated profraw data files.
765 """
Yuke Liao481d3482018-01-29 19:17:10766 logging.debug('Executing the test commands')
767
Yuke Liao506e8822017-12-04 16:52:54768 # Remove existing profraw data files.
769 for file_or_dir in os.listdir(OUTPUT_DIR):
770 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
771 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
772
Yuke Liaoa0c8c2f2018-02-28 20:14:10773 profraw_file_paths = []
774
Yuke Liaod4a9865202018-01-12 23:17:52775 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54776 for target, command in zip(targets, commands):
Yuke Liaoa0c8c2f2018-02-28 20:14:10777 output_file_name = os.extsep.join([target + '_output', 'txt'])
778 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
779 logging.info('Running command: "%s", the output is redirected to "%s"',
780 command, output_file_path)
781
Yuke Liaob2926832018-03-02 17:34:29782 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10783 # On iOS platform, due to lack of write permissions, profraw files are
784 # generated outside of the OUTPUT_DIR, and the exact paths are contained
785 # in the output of the command execution.
Yuke Liaob2926832018-03-02 17:34:29786 output = _ExecuteIOSCommand(target, command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10787 profraw_file_paths.append(_GetProfrawDataFileByParsingOutput(output))
788 else:
789 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
790 output = _ExecuteCommand(target, command)
791
792 with open(output_file_path, 'w') as output_file:
793 output_file.write(output)
Yuke Liao506e8822017-12-04 16:52:54794
Yuke Liao481d3482018-01-29 19:17:10795 logging.debug('Finished executing the test commands')
796
Yuke Liaob2926832018-03-02 17:34:29797 if _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10798 return profraw_file_paths
799
Yuke Liao506e8822017-12-04 16:52:54800 for file_or_dir in os.listdir(OUTPUT_DIR):
801 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
802 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
803
804 # Assert one target/command generates at least one profraw data file.
805 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59806 assert any(
807 os.path.basename(profraw_file).startswith(target)
808 for profraw_file in profraw_file_paths), (
809 'Running target: %s failed to generate any profraw data file, '
810 'please make sure the binary exists and is properly instrumented.' %
811 target)
Yuke Liao506e8822017-12-04 16:52:54812
813 return profraw_file_paths
814
815
816def _ExecuteCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10817 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52818 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01819 #
820 # "%p" expands out to the process ID.
821 #
Yuke Liaod4a9865202018-01-12 23:17:52822 # "%Nm" expands out to the instrumented binary's signature. When this pattern
823 # is specified, the runtime creates a pool of N raw profiles which are used
824 # for on-line profile merging. The runtime takes care of selecting a raw
825 # profile from the pool, locking it, and updating it before the program exits.
826 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
827 # N must be between 1 and 9. The merge pool specifier can only occur once per
828 # filename pattern.
829 #
Yuke Liao27349c92018-03-22 21:10:01830 # "%p" is used when tests run in single process, however, it can't be used for
831 # multi-process because each process produces an intermediate dump, which may
832 # consume hundreds of gigabytes of disk space.
833 #
834 # For "%Nm", 4 is chosen because it creates some level of parallelism, but
835 # it's not too big to consume too much computing resource or disk space.
836 profile_pattern_string = '%p' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59837 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01838 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54839 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
840 expected_profraw_file_name)
Yuke Liao506e8822017-12-04 16:52:54841
Yuke Liaoa0c8c2f2018-02-28 20:14:10842 try:
843 output = subprocess.check_output(
Yuke Liaob2926832018-03-02 17:34:29844 shlex.split(command),
845 env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10846 except subprocess.CalledProcessError as e:
847 output = e.output
848 logging.warning('Command: "%s" exited with non-zero return code', command)
849
850 return output
851
852
Yuke Liao27349c92018-03-22 21:10:01853def _IsFuzzerTarget(target):
854 """Returns true if the target is a fuzzer target."""
855 build_args = _GetBuildArgs()
856 use_libfuzzer = ('use_libfuzzer' in build_args and
857 build_args['use_libfuzzer'] == 'true')
858 return use_libfuzzer and target.endswith('_fuzzer')
859
860
Yuke Liaob2926832018-03-02 17:34:29861def _ExecuteIOSCommand(target, command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10862 """Runs a single iOS command and generates a profraw data file.
863
864 iOS application doesn't have write access to folders outside of the app, so
865 it's impossible to instruct the app to flush the profraw data file to the
866 desired location. The profraw data file will be generated somewhere within the
867 application's Documents folder, and the full path can be obtained by parsing
868 the output.
869 """
Yuke Liaob2926832018-03-02 17:34:29870 assert _IsIOSCommand(command)
871
872 # After running tests, iossim generates a profraw data file, it won't be
873 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
874 # checkout.
875 iossim_profraw_file_path = os.path.join(
876 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Yuke Liaoa0c8c2f2018-02-28 20:14:10877
878 try:
Yuke Liaob2926832018-03-02 17:34:29879 output = subprocess.check_output(
880 shlex.split(command),
881 env={'LLVM_PROFILE_FILE': iossim_profraw_file_path})
Yuke Liaoa0c8c2f2018-02-28 20:14:10882 except subprocess.CalledProcessError as e:
883 # iossim emits non-zero return code even if tests run successfully, so
884 # ignore the return code.
885 output = e.output
886
887 return output
888
889
890def _GetProfrawDataFileByParsingOutput(output):
891 """Returns the path to the profraw data file obtained by parsing the output.
892
893 The output of running the test target has no format, but it is guaranteed to
894 have a single line containing the path to the generated profraw data file.
895 NOTE: This should only be called when target os is iOS.
896 """
Yuke Liaob2926832018-03-02 17:34:29897 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10898
Yuke Liaob2926832018-03-02 17:34:29899 output_by_lines = ''.join(output).splitlines()
900 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10901
902 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29903 result = profraw_file_pattern.match(line)
904 if result:
905 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10906
907 assert False, ('No profraw data file was generated, did you call '
908 'coverage_util::ConfigureCoverageReportPath() in test setup? '
909 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54910
911
912def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
913 """Returns a relative path to the profdata file by merging profraw data files.
914
915 Args:
916 profraw_file_paths: A list of relative paths to the profraw data files that
917 are to be merged.
918
919 Returns:
920 A relative path to the generated profdata file.
921
922 Raises:
923 CalledProcessError: An error occurred merging profraw data files.
924 """
Yuke Liao481d3482018-01-29 19:17:10925 logging.info('Creating the coverage profile data file')
926 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54927 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
928 try:
Abhishek Arya1ec832c2017-12-05 18:06:59929 subprocess_cmd = [
930 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
931 ]
Yuke Liao506e8822017-12-04 16:52:54932 subprocess_cmd.extend(profraw_file_paths)
933 subprocess.check_call(subprocess_cmd)
934 except subprocess.CalledProcessError as error:
935 print('Failed to merge profraw files to create profdata file')
936 raise error
937
Yuke Liao481d3482018-01-29 19:17:10938 logging.debug('Finished merging profraw files')
939 logging.info('Code coverage profile data is created as: %s',
940 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54941 return profdata_file_path
942
943
Yuke Liaoea228d02018-01-05 19:10:33944def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
945 """Generates per file coverage summary using "llvm-cov export" command."""
946 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
947 # [[-object BIN]] [SOURCES].
948 # NOTE: For object files, the first one is specified as a positional argument,
949 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10950 logging.debug('Generating per-file code coverage summary using "llvm-cov '
951 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33952 subprocess_cmd = [
953 LLVM_COV_PATH, 'export', '-summary-only',
954 '-instr-profile=' + profdata_file_path, binary_paths[0]
955 ]
956 subprocess_cmd.extend(
957 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29958 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33959 subprocess_cmd.extend(filters)
960
961 json_output = json.loads(subprocess.check_output(subprocess_cmd))
962 assert len(json_output['data']) == 1
963 files_coverage_data = json_output['data'][0]['files']
964
965 per_file_coverage_summary = {}
966 for file_coverage_data in files_coverage_data:
967 file_path = file_coverage_data['filename']
968 summary = file_coverage_data['summary']
969
Yuke Liaoea228d02018-01-05 19:10:33970 if summary['lines']['count'] == 0:
971 continue
972
973 per_file_coverage_summary[file_path] = _CoverageSummary(
974 regions_total=summary['regions']['count'],
975 regions_covered=summary['regions']['covered'],
976 functions_total=summary['functions']['count'],
977 functions_covered=summary['functions']['covered'],
978 lines_total=summary['lines']['count'],
979 lines_covered=summary['lines']['covered'])
980
Yuke Liao481d3482018-01-29 19:17:10981 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33982 return per_file_coverage_summary
983
984
Yuke Liaob2926832018-03-02 17:34:29985def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
986 """Appends -arch arguments to the command list if it's ios platform.
987
988 iOS binaries are universal binaries, and require specifying the architecture
989 to use, and one architecture needs to be specified for each binary.
990 """
991 if _IsIOS():
992 cmd_list.extend(['-arch=x86_64'] * num_archs)
993
994
Yuke Liao506e8822017-12-04 16:52:54995def _GetBinaryPath(command):
996 """Returns a relative path to the binary to be run by the command.
997
Yuke Liao545db322018-02-15 17:12:01998 Currently, following types of commands are supported (e.g. url_unittests):
999 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
1000 2. Use xvfb.
1001 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
1002 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:371003 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
1004 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:101005 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:371006 <iossim_arguments> -c <app_arguments>
1007 out/Coverage-iphonesimulator/url_unittests.app"
1008
Yuke Liao545db322018-02-15 17:12:011009
Yuke Liao506e8822017-12-04 16:52:541010 Args:
1011 command: A command used to run a target.
1012
1013 Returns:
1014 A relative path to the binary.
1015 """
Yuke Liao545db322018-02-15 17:12:011016 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
1017
Yuke Liaob2926832018-03-02 17:34:291018 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:011019 if os.path.basename(command_parts[0]) == 'python':
1020 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
1021 'This tool doesn\'t understand the command: "%s"' % command)
1022 return command_parts[2]
1023
1024 if os.path.basename(command_parts[0]) == xvfb_script_name:
1025 return command_parts[1]
1026
Yuke Liaob2926832018-03-02 17:34:291027 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101028 # For a given application bundle, the binary resides in the bundle and has
1029 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:371030 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:101031 app_name = os.path.splitext(os.path.basename(app_path))[0]
1032 return os.path.join(app_path, app_name)
1033
Yuke Liaob2926832018-03-02 17:34:291034 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:541035
1036
Yuke Liaob2926832018-03-02 17:34:291037def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:101038 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:291039 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:101040
1041
Yuke Liao95d13d72017-12-07 18:18:501042def _VerifyTargetExecutablesAreInBuildDirectory(commands):
1043 """Verifies that the target executables specified in the commands are inside
1044 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:541045 for command in commands:
1046 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:501047 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
1048 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
1049 'Target executable "%s" in command: "%s" is outside of '
1050 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:541051
1052
1053def _ValidateBuildingWithClangCoverage():
1054 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:201055 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:541056
1057 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
1058 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:591059 assert False, ('\'{} = true\' is required in args.gn.'
1060 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:541061
1062
Yuke Liaoc60b2d02018-03-02 21:40:431063def _ValidateCurrentPlatformIsSupported():
1064 """Asserts that this script suports running on the current platform"""
1065 target_os = _GetTargetOS()
1066 if target_os:
1067 current_platform = target_os
1068 else:
1069 current_platform = _GetHostPlatform()
1070
1071 assert current_platform in [
1072 'linux', 'mac', 'chromeos', 'ios'
1073 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
1074
1075
Yuke Liao80afff32018-03-07 01:26:201076def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:541077 """Parses args.gn file and returns results as a dictionary.
1078
1079 Returns:
1080 A dictionary representing the build args.
1081 """
Yuke Liao80afff32018-03-07 01:26:201082 global _BUILD_ARGS
1083 if _BUILD_ARGS is not None:
1084 return _BUILD_ARGS
1085
1086 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:541087 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
1088 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
1089 'missing args.gn file.' % BUILD_DIR)
1090 with open(build_args_path) as build_args_file:
1091 build_args_lines = build_args_file.readlines()
1092
Yuke Liao506e8822017-12-04 16:52:541093 for build_arg_line in build_args_lines:
1094 build_arg_without_comments = build_arg_line.split('#')[0]
1095 key_value_pair = build_arg_without_comments.split('=')
1096 if len(key_value_pair) != 2:
1097 continue
1098
1099 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:431100
1101 # Values are wrapped within a pair of double-quotes, so remove the leading
1102 # and trailing double-quotes.
1103 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:201104 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:541105
Yuke Liao80afff32018-03-07 01:26:201106 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:541107
1108
Abhishek Arya16f059a2017-12-07 17:47:321109def _VerifyPathsAndReturnAbsolutes(paths):
1110 """Verifies that the paths specified in |paths| exist and returns absolute
1111 versions.
Yuke Liao66da1732017-12-05 22:19:421112
1113 Args:
1114 paths: A list of files or directories.
1115 """
Abhishek Arya16f059a2017-12-07 17:47:321116 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:421117 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:321118 absolute_path = os.path.join(SRC_ROOT_PATH, path)
1119 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
1120
1121 absolute_paths.append(absolute_path)
1122
1123 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:421124
1125
Yuke Liaodd1ec0592018-02-02 01:26:371126def _GetRelativePathToDirectoryOfFile(target_path, base_path):
1127 """Returns a target path relative to the directory of base_path.
1128
1129 This method requires base_path to be a file, otherwise, one should call
1130 os.path.relpath directly.
1131 """
1132 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:141133 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:371134 base_path)
Yuke Liaoc7e607142018-02-05 20:26:141135 base_dir = os.path.dirname(base_path)
1136 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:371137
1138
Yuke Liao506e8822017-12-04 16:52:541139def _ParseCommandArguments():
1140 """Adds and parses relevant arguments for tool comands.
1141
1142 Returns:
1143 A dictionary representing the arguments.
1144 """
1145 arg_parser = argparse.ArgumentParser()
1146 arg_parser.usage = __doc__
1147
Abhishek Arya1ec832c2017-12-05 18:06:591148 arg_parser.add_argument(
1149 '-b',
1150 '--build-dir',
1151 type=str,
1152 required=True,
1153 help='The build directory, the path needs to be relative to the root of '
1154 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541155
Abhishek Arya1ec832c2017-12-05 18:06:591156 arg_parser.add_argument(
1157 '-o',
1158 '--output-dir',
1159 type=str,
1160 required=True,
1161 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541162
Abhishek Arya1ec832c2017-12-05 18:06:591163 arg_parser.add_argument(
1164 '-c',
1165 '--command',
1166 action='append',
1167 required=True,
1168 help='Commands used to run test targets, one test target needs one and '
1169 'only one command, when specifying commands, one should assume the '
1170 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541171
Abhishek Arya1ec832c2017-12-05 18:06:591172 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421173 '-f',
1174 '--filters',
1175 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321176 required=False,
Yuke Liao66da1732017-12-05 22:19:421177 help='Directories or files to get code coverage for, and all files under '
1178 'the directories are included recursively.')
1179
1180 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591181 '-j',
1182 '--jobs',
1183 type=int,
1184 default=None,
1185 help='Run N jobs to build in parallel. If not specified, a default value '
1186 'will be derived based on CPUs availability. Please refer to '
1187 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541188
Abhishek Arya1ec832c2017-12-05 18:06:591189 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101190 '-v',
1191 '--verbose',
1192 action='store_true',
1193 help='Prints additional output for diagnostics.')
1194
1195 arg_parser.add_argument(
1196 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1197
1198 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591199 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541200
1201 args = arg_parser.parse_args()
1202 return args
1203
1204
1205def Main():
1206 """Execute tool commands."""
1207 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1208 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591209 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541210 args = _ParseCommandArguments()
1211 global BUILD_DIR
1212 BUILD_DIR = args.build_dir
1213 global OUTPUT_DIR
1214 OUTPUT_DIR = args.output_dir
1215
1216 assert len(args.targets) == len(args.command), ('Number of targets must be '
1217 'equal to the number of test '
1218 'commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431219
1220 # logging should be configured before it is used.
1221 log_level = logging.DEBUG if args.verbose else logging.INFO
1222 log_format = '[%(asctime)s %(levelname)s] %(message)s'
1223 log_file = args.log_file if args.log_file else None
1224 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1225
Abhishek Arya1ec832c2017-12-05 18:06:591226 assert os.path.exists(BUILD_DIR), (
1227 'Build directory: {} doesn\'t exist. '
1228 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liaoc60b2d02018-03-02 21:40:431229 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541230 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501231 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321232
Yuke Liaoc60b2d02018-03-02 21:40:431233 DownloadCoverageToolsIfNeeded()
1234
Abhishek Arya16f059a2017-12-07 17:47:321235 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421236 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321237 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421238
Yuke Liao506e8822017-12-04 16:52:541239 if not os.path.exists(OUTPUT_DIR):
1240 os.makedirs(OUTPUT_DIR)
1241
Abhishek Arya1ec832c2017-12-05 18:06:591242 profdata_file_path = _CreateCoverageProfileDataForTargets(
1243 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541244 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331245
Yuke Liao481d3482018-01-29 19:17:101246 logging.info('Generating code coverage report in html (this can take a while '
1247 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371248 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1249 binary_paths, profdata_file_path, absolute_filter_paths)
1250 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1251 absolute_filter_paths)
1252 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1253
1254 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1255 per_file_coverage_summary)
1256 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1257 per_file_coverage_summary)
1258 _GenerateDirectoryViewHtmlIndexFile()
1259
1260 component_to_directories = _ExtractComponentToDirectoriesMapping()
1261 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1262 component_to_directories, per_directory_coverage_summary)
1263 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1264 component_to_directories,
1265 per_directory_coverage_summary)
1266 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331267
1268 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371269 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331270 _OverwriteHtmlReportsIndexFile()
1271
Yuke Liao506e8822017-12-04 16:52:541272 html_index_file_path = 'file://' + os.path.abspath(
1273 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101274 logging.info('Index file for html report is generated as: %s',
1275 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541276
Abhishek Arya1ec832c2017-12-05 18:06:591277
Yuke Liao506e8822017-12-04 16:52:541278if __name__ == '__main__':
1279 sys.exit(Main())