blob: 98e41979d7039a0f7e0e5ee5efa20f9a73ed99bf [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
11 "use_clang_coverage=true" GN flag to args.gn file in your build
12 output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Abhishek Arya16f059a2017-12-07 17:47:3214 It is recommended to add "is_component_build=false" flag as well because:
Abhishek Arya1ec832c2017-12-05 18:06:5915 1. It is incompatible with other sanitizer flags (like "is_asan", "is_msan")
16 and others like "optimize_for_fuzzing".
17 2. If it is not set explicitly, "is_debug" overrides it to true.
Yuke Liao506e8822017-12-04 16:52:5418
Abhishek Arya1ec832c2017-12-05 18:06:5919 Example usage:
20
Abhishek Arya16f059a2017-12-07 17:47:3221 gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
22 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5923 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3224 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
25 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
26 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5927
Abhishek Arya16f059a2017-12-07 17:47:3228 The command above builds crypto_unittests and url_unittests targets and then
29 runs them with specified command line arguments. For url_unittests, it only
30 runs the test URLParser.PathURL. The coverage report is filtered to include
31 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5932
Yuke Liao545db322018-02-15 17:12:0133 If you want to run tests that try to draw to the screen but don't have a
34 display connected, you can run tests in headless mode with xvfb.
35
36 Sample flow for running a test target with xvfb (e.g. unit_tests):
37
38 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
39 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
40
Abhishek Arya1ec832c2017-12-05 18:06:5941 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
42 flag as well.
43
44 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
45
Abhishek Arya16f059a2017-12-07 17:47:3246 python tools/code_coverage/coverage.py pdfium_fuzzer \\
47 -b out/coverage -o out/report \\
48 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
49 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5950
51 where:
52 <corpus_dir> - directory containing samples files for this format.
53 <runs> - number of times to fuzz target function. Should be 0 when you just
54 want to see the coverage on corpus and don't want to fuzz at all.
55
56 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao506e8822017-12-04 16:52:5457"""
58
59from __future__ import print_function
60
61import sys
62
63import argparse
Yuke Liaoea228d02018-01-05 19:10:3364import json
Yuke Liao481d3482018-01-29 19:17:1065import logging
Yuke Liao506e8822017-12-04 16:52:5466import os
67import subprocess
Yuke Liao506e8822017-12-04 16:52:5468import urllib2
69
Abhishek Arya1ec832c2017-12-05 18:06:5970sys.path.append(
71 os.path.join(
72 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
73 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5474import update as clang_update
75
Yuke Liaoea228d02018-01-05 19:10:3376sys.path.append(
77 os.path.join(
78 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
79 'third_party'))
80import jinja2
81from collections import defaultdict
82
Yuke Liao506e8822017-12-04 16:52:5483# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5984SRC_ROOT_PATH = os.path.abspath(
85 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5486
87# Absolute path to the code coverage tools binary.
88LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
89LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
90LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
91
92# Build directory, the value is parsed from command line arguments.
93BUILD_DIR = None
94
95# Output directory for generated artifacts, the value is parsed from command
96# line arguemnts.
97OUTPUT_DIR = None
98
99# Default number of jobs used to build when goma is configured and enabled.
100DEFAULT_GOMA_JOBS = 100
101
102# Name of the file extension for profraw data files.
103PROFRAW_FILE_EXTENSION = 'profraw'
104
105# Name of the final profdata file, and this file needs to be passed to
106# "llvm-cov" command in order to call "llvm-cov show" to inspect the
107# line-by-line coverage of specific files.
108PROFDATA_FILE_NAME = 'coverage.profdata'
109
110# Build arg required for generating code coverage data.
111CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
112
Yuke Liaoea228d02018-01-05 19:10:33113# The default name of the html coverage report for a directory.
114DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
115
Yuke Liaodd1ec0592018-02-02 01:26:37116# Name of the html index files for different views.
117DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
118COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
119FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
120
121# Used to extract a mapping between directories and components.
122COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
123
Yuke Liaoea228d02018-01-05 19:10:33124
125class _CoverageSummary(object):
126 """Encapsulates coverage summary representation."""
127
Yuke Liaodd1ec0592018-02-02 01:26:37128 def __init__(self,
129 regions_total=0,
130 regions_covered=0,
131 functions_total=0,
132 functions_covered=0,
133 lines_total=0,
134 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33135 """Initializes _CoverageSummary object."""
136 self._summary = {
137 'regions': {
138 'total': regions_total,
139 'covered': regions_covered
140 },
141 'functions': {
142 'total': functions_total,
143 'covered': functions_covered
144 },
145 'lines': {
146 'total': lines_total,
147 'covered': lines_covered
148 }
149 }
150
151 def Get(self):
152 """Returns summary as a dictionary."""
153 return self._summary
154
155 def AddSummary(self, other_summary):
156 """Adds another summary to this one element-wise."""
157 for feature in self._summary:
158 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
159 self._summary[feature]['covered'] += other_summary.Get()[feature][
160 'covered']
161
162
Yuke Liaodd1ec0592018-02-02 01:26:37163class _CoverageReportHtmlGenerator(object):
164 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33165
Yuke Liaodd1ec0592018-02-02 01:26:37166 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33167 """
168
Yuke Liaodd1ec0592018-02-02 01:26:37169 def __init__(self, output_path, table_entry_type):
170 """Initializes _CoverageReportHtmlGenerator object.
171
172 Args:
173 output_path: Path to the html report that will be generated.
174 table_entry_type: Type of the table entries to be displayed in the table
175 header. For example: 'Path', 'Component'.
176 """
Yuke Liaoea228d02018-01-05 19:10:33177 css_file_name = os.extsep.join(['style', 'css'])
178 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
179 assert os.path.exists(css_absolute_path), (
180 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
181 'is called first, and the css file is generated at: "%s"' %
182 css_absolute_path)
183
184 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37185 self._output_path = output_path
186 self._table_entry_type = table_entry_type
187
Yuke Liaoea228d02018-01-05 19:10:33188 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12189 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33190 template_dir = os.path.join(
191 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
192
193 jinja_env = jinja2.Environment(
194 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
195 self._header_template = jinja_env.get_template('header.html')
196 self._table_template = jinja_env.get_template('table.html')
197 self._footer_template = jinja_env.get_template('footer.html')
198
199 def AddLinkToAnotherReport(self, html_report_path, name, summary):
200 """Adds a link to another html report in this report.
201
202 The link to be added is assumed to be an entry in this directory.
203 """
Yuke Liaodd1ec0592018-02-02 01:26:37204 # Use relative paths instead of absolute paths to make the generated reports
205 # portable.
206 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
207 html_report_path, self._output_path)
208
Yuke Liaod54030e2018-01-08 17:34:12209 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37210 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12211 os.path.basename(html_report_path) ==
212 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
213 self._table_entries.append(table_entry)
214
215 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35216 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12217 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
218
219 def _CreateTableEntryFromCoverageSummary(self,
220 summary,
221 href=None,
222 name=None,
223 is_dir=None):
224 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37225 assert (href is None and name is None and is_dir is None) or (
226 href is not None and name is not None and is_dir is not None), (
227 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35228 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37229 'attributes must be None.')
230
Yuke Liaod54030e2018-01-08 17:34:12231 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37232 if href is not None:
233 entry['href'] = href
234 if name is not None:
235 entry['name'] = name
236 if is_dir is not None:
237 entry['is_dir'] = is_dir
238
Yuke Liaoea228d02018-01-05 19:10:33239 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12240 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37241 if summary_dict[feature]['total'] == 0:
242 percentage = 0.0
243 else:
Yuke Liaoa785f4d32018-02-13 21:41:35244 percentage = float(summary_dict[feature]['covered']) / summary_dict[
245 feature]['total'] * 100
246
Yuke Liaoea228d02018-01-05 19:10:33247 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12248 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33249 'total': summary_dict[feature]['total'],
250 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35251 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33252 'color_class': color_class
253 }
Yuke Liaod54030e2018-01-08 17:34:12254
Yuke Liaod54030e2018-01-08 17:34:12255 return entry
Yuke Liaoea228d02018-01-05 19:10:33256
257 def _GetColorClass(self, percentage):
258 """Returns the css color class based on coverage percentage."""
259 if percentage >= 0 and percentage < 80:
260 return 'red'
261 if percentage >= 80 and percentage < 100:
262 return 'yellow'
263 if percentage == 100:
264 return 'green'
265
266 assert False, 'Invalid coverage percentage: "%d"' % percentage
267
Yuke Liaodd1ec0592018-02-02 01:26:37268 def WriteHtmlCoverageReport(self):
269 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33270
271 In the report, sub-directories are displayed before files and within each
272 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33273 """
274
275 def EntryCmp(left, right):
276 """Compare function for table entries."""
277 if left['is_dir'] != right['is_dir']:
278 return -1 if left['is_dir'] == True else 1
279
Yuke Liaodd1ec0592018-02-02 01:26:37280 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33281
282 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
283
284 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37285 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
286 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
287 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
288
Yuke Liaoea228d02018-01-05 19:10:33289 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37290 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
291 directory_view_href=_GetRelativePathToDirectoryOfFile(
292 directory_view_path, self._output_path),
293 component_view_href=_GetRelativePathToDirectoryOfFile(
294 component_view_path, self._output_path),
295 file_view_href=_GetRelativePathToDirectoryOfFile(
296 file_view_path, self._output_path))
297
Yuke Liaod54030e2018-01-08 17:34:12298 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37299 entries=self._table_entries,
300 total_entry=self._total_entry,
301 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33302 html_footer = self._footer_template.render()
303
Yuke Liaodd1ec0592018-02-02 01:26:37304 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33305 html_file.write(html_header + html_table + html_footer)
306
Yuke Liao506e8822017-12-04 16:52:54307
Abhishek Arya1ec832c2017-12-05 18:06:59308def _GetPlatform():
309 """Returns current running platform."""
310 if sys.platform == 'win32' or sys.platform == 'cygwin':
311 return 'win'
312 if sys.platform.startswith('linux'):
313 return 'linux'
314 else:
315 assert sys.platform == 'darwin'
316 return 'mac'
317
318
Yuke Liao506e8822017-12-04 16:52:54319# TODO(crbug.com/759794): remove this function once tools get included to
320# Clang bundle:
321# https://chromium-review.googlesource.com/c/chromium/src/+/688221
322def DownloadCoverageToolsIfNeeded():
323 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59324
325 def _GetRevisionFromStampFile(stamp_file_path, platform):
Yuke Liao506e8822017-12-04 16:52:54326 """Returns a pair of revision number by reading the build stamp file.
327
328 Args:
329 stamp_file_path: A path the build stamp file created by
330 tools/clang/scripts/update.py.
331 Returns:
332 A pair of integers represeting the main and sub revision respectively.
333 """
334 if not os.path.exists(stamp_file_path):
335 return 0, 0
336
337 with open(stamp_file_path) as stamp_file:
Abhishek Arya1ec832c2017-12-05 18:06:59338 for stamp_file_line in stamp_file.readlines():
339 if ',' in stamp_file_line:
340 package_version, target_os = stamp_file_line.rstrip().split(',')
341 else:
342 package_version = stamp_file_line.rstrip()
343 target_os = ''
Yuke Liao506e8822017-12-04 16:52:54344
Abhishek Arya1ec832c2017-12-05 18:06:59345 if target_os and platform != target_os:
346 continue
347
348 clang_revision_str, clang_sub_revision_str = package_version.split('-')
349 return int(clang_revision_str), int(clang_sub_revision_str)
350
351 assert False, 'Coverage is only supported on target_os - linux, mac.'
352
353 platform = _GetPlatform()
Yuke Liao506e8822017-12-04 16:52:54354 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59355 clang_update.STAMP_FILE, platform)
Yuke Liao506e8822017-12-04 16:52:54356
357 coverage_revision_stamp_file = os.path.join(
358 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
359 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59360 coverage_revision_stamp_file, platform)
Yuke Liao506e8822017-12-04 16:52:54361
Yuke Liaoea228d02018-01-05 19:10:33362 has_coverage_tools = (
363 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32364
Yuke Liaoea228d02018-01-05 19:10:33365 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54366 coverage_sub_revision == clang_sub_revision):
367 # LLVM coverage tools are up to date, bail out.
368 return clang_revision
369
370 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
371 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
372
373 # The code bellow follows the code from tools/clang/scripts/update.py.
Abhishek Arya1ec832c2017-12-05 18:06:59374 if platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54375 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
376 else:
Abhishek Arya1ec832c2017-12-05 18:06:59377 assert platform == 'linux'
Yuke Liao506e8822017-12-04 16:52:54378 coverage_tools_url = (
379 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
380
381 try:
382 clang_update.DownloadAndUnpack(coverage_tools_url,
383 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10384 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54385 with open(coverage_revision_stamp_file, 'w') as file_handle:
Abhishek Arya1ec832c2017-12-05 18:06:59386 file_handle.write('%s,%s' % (package_version, platform))
Yuke Liao506e8822017-12-04 16:52:54387 file_handle.write('\n')
388 except urllib2.URLError:
389 raise Exception(
390 'Failed to download coverage tools: %s.' % coverage_tools_url)
391
392
Yuke Liaodd1ec0592018-02-02 01:26:37393def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
394 filters):
Yuke Liao506e8822017-12-04 16:52:54395 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
396
397 For a file with absolute path /a/b/x.cc, a html report is generated as:
398 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
399 OUTPUT_DIR/index.html.
400
401 Args:
402 binary_paths: A list of paths to the instrumented binaries.
403 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42404 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54405 """
Yuke Liao506e8822017-12-04 16:52:54406 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
407 # [[-object BIN]] [SOURCES]
408 # NOTE: For object files, the first one is specified as a positional argument,
409 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10410 logging.debug('Generating per file line by line coverage reports using '
411 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59412 subprocess_cmd = [
413 LLVM_COV_PATH, 'show', '-format=html',
414 '-output-dir={}'.format(OUTPUT_DIR),
415 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
416 ]
417 subprocess_cmd.extend(
418 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liao66da1732017-12-05 22:19:42419 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54420 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10421 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54422
423
Yuke Liaodd1ec0592018-02-02 01:26:37424def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
425 """Generates html index file for file view."""
426 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
427 logging.debug('Generating file view html index file as: "%s".',
428 file_view_index_file_path)
429 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
430 'Path')
431 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33432
Yuke Liaodd1ec0592018-02-02 01:26:37433 for file_path in per_file_coverage_summary:
434 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
435
436 html_generator.AddLinkToAnotherReport(
437 _GetCoverageHtmlReportPathForFile(file_path),
438 os.path.relpath(file_path, SRC_ROOT_PATH),
439 per_file_coverage_summary[file_path])
440
441 html_generator.CreateTotalsEntry(totals_coverage_summary)
442 html_generator.WriteHtmlCoverageReport()
443 logging.debug('Finished generating file view html index file.')
444
445
446def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
447 """Calculates per directory coverage summary."""
448 logging.debug('Calculating per-directory coverage summary')
449 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
450
Yuke Liaoea228d02018-01-05 19:10:33451 for file_path in per_file_coverage_summary:
452 summary = per_file_coverage_summary[file_path]
453 parent_dir = os.path.dirname(file_path)
454 while True:
455 per_directory_coverage_summary[parent_dir].AddSummary(summary)
456
457 if parent_dir == SRC_ROOT_PATH:
458 break
459 parent_dir = os.path.dirname(parent_dir)
460
Yuke Liaodd1ec0592018-02-02 01:26:37461 logging.debug('Finished calculating per-directory coverage summary')
462 return per_directory_coverage_summary
463
464
465def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
466 per_file_coverage_summary):
467 """Generates per directory coverage breakdown in html."""
468 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33469 for dir_path in per_directory_coverage_summary:
470 _GenerateCoverageInHtmlForDirectory(
471 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
472
Yuke Liaodd1ec0592018-02-02 01:26:37473 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10474
Yuke Liaoea228d02018-01-05 19:10:33475
476def _GenerateCoverageInHtmlForDirectory(
477 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
478 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37479 html_generator = _CoverageReportHtmlGenerator(
480 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33481
482 for entry_name in os.listdir(dir_path):
483 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33484
Yuke Liaodd1ec0592018-02-02 01:26:37485 if entry_path in per_file_coverage_summary:
486 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
487 entry_coverage_summary = per_file_coverage_summary[entry_path]
488 elif entry_path in per_directory_coverage_summary:
489 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
490 entry_path)
491 entry_coverage_summary = per_directory_coverage_summary[entry_path]
492 else:
Yuke Liaoc7e607142018-02-05 20:26:14493 # Any file without executable lines shouldn't be included into the report.
494 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37495 continue
Yuke Liaoea228d02018-01-05 19:10:33496
Yuke Liaodd1ec0592018-02-02 01:26:37497 html_generator.AddLinkToAnotherReport(entry_html_report_path,
498 os.path.basename(entry_path),
499 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33500
Yuke Liaod54030e2018-01-08 17:34:12501 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37502 html_generator.WriteHtmlCoverageReport()
503
504
505def _GenerateDirectoryViewHtmlIndexFile():
506 """Generates the html index file for directory view.
507
508 Note that the index file is already generated under SRC_ROOT_PATH, so this
509 file simply redirects to it, and the reason of this extra layer is for
510 structural consistency with other views.
511 """
512 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
513 DIRECTORY_VIEW_INDEX_FILE)
514 logging.debug('Generating directory view html index file as: "%s".',
515 directory_view_index_file_path)
516 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
517 SRC_ROOT_PATH)
518 _WriteRedirectHtmlFile(directory_view_index_file_path,
519 src_root_html_report_path)
520 logging.debug('Finished generating directory view html index file.')
521
522
523def _CalculatePerComponentCoverageSummary(component_to_directories,
524 per_directory_coverage_summary):
525 """Calculates per component coverage summary."""
526 logging.debug('Calculating per-component coverage summary')
527 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
528
529 for component in component_to_directories:
530 for directory in component_to_directories[component]:
531 absolute_directory_path = os.path.abspath(directory)
532 if absolute_directory_path in per_directory_coverage_summary:
533 per_component_coverage_summary[component].AddSummary(
534 per_directory_coverage_summary[absolute_directory_path])
535
536 logging.debug('Finished calculating per-component coverage summary')
537 return per_component_coverage_summary
538
539
540def _ExtractComponentToDirectoriesMapping():
541 """Returns a mapping from components to directories."""
542 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
543 directory_to_component = component_mappings['dir-to-component']
544
545 component_to_directories = defaultdict(list)
546 for directory in directory_to_component:
547 component = directory_to_component[directory]
548 component_to_directories[component].append(directory)
549
550 return component_to_directories
551
552
553def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
554 component_to_directories,
555 per_directory_coverage_summary):
556 """Generates per-component coverage reports in html."""
557 logging.debug('Writing per-component coverage html reports.')
558 for component in per_component_coverage_summary:
559 _GenerateCoverageInHtmlForComponent(
560 component, per_component_coverage_summary, component_to_directories,
561 per_directory_coverage_summary)
562
563 logging.debug('Finished writing per-component coverage html reports.')
564
565
566def _GenerateCoverageInHtmlForComponent(
567 component_name, per_component_coverage_summary, component_to_directories,
568 per_directory_coverage_summary):
569 """Generates coverage html report for a component."""
570 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
571 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14572 component_html_report_dir = os.path.dirname(component_html_report_path)
573 if not os.path.exists(component_html_report_dir):
574 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37575
576 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
577 'Path')
578
579 for dir_path in component_to_directories[component_name]:
580 dir_absolute_path = os.path.abspath(dir_path)
581 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14582 # Any directory without an excercised file shouldn't be included into the
583 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37584 continue
585
586 html_generator.AddLinkToAnotherReport(
587 _GetCoverageHtmlReportPathForDirectory(dir_path),
588 os.path.relpath(dir_path, SRC_ROOT_PATH),
589 per_directory_coverage_summary[dir_absolute_path])
590
591 html_generator.CreateTotalsEntry(
592 per_component_coverage_summary[component_name])
593 html_generator.WriteHtmlCoverageReport()
594
595
596def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
597 """Generates the html index file for component view."""
598 component_view_index_file_path = os.path.join(OUTPUT_DIR,
599 COMPONENT_VIEW_INDEX_FILE)
600 logging.debug('Generating component view html index file as: "%s".',
601 component_view_index_file_path)
602 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
603 'Component')
604 totals_coverage_summary = _CoverageSummary()
605
606 for component in per_component_coverage_summary:
607 totals_coverage_summary.AddSummary(
608 per_component_coverage_summary[component])
609
610 html_generator.AddLinkToAnotherReport(
611 _GetCoverageHtmlReportPathForComponent(component), component,
612 per_component_coverage_summary[component])
613
614 html_generator.CreateTotalsEntry(totals_coverage_summary)
615 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14616 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33617
618
619def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37620 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33621 html_index_file_path = os.path.join(OUTPUT_DIR,
622 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37623 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
624 DIRECTORY_VIEW_INDEX_FILE)
625 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
626
627
628def _WriteRedirectHtmlFile(from_html_path, to_html_path):
629 """Writes a html file that redirects to another html file."""
630 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
631 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33632 content = ("""
633 <!DOCTYPE html>
634 <html>
635 <head>
636 <!-- HTML meta refresh URL redirection -->
637 <meta http-equiv="refresh" content="0; url=%s">
638 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37639 </html>""" % to_html_relative_path)
640 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33641 f.write(content)
642
643
Yuke Liaodd1ec0592018-02-02 01:26:37644def _GetCoverageHtmlReportPathForFile(file_path):
645 """Given a file path, returns the corresponding html report path."""
646 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
647 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
648
649 # '+' is used instead of os.path.join because both of them are absolute paths
650 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14651 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37652 return _GetCoverageReportRootDirPath() + html_report_path
653
654
655def _GetCoverageHtmlReportPathForDirectory(dir_path):
656 """Given a directory path, returns the corresponding html report path."""
657 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
658 html_report_path = os.path.join(
659 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
660
661 # '+' is used instead of os.path.join because both of them are absolute paths
662 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14663 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37664 return _GetCoverageReportRootDirPath() + html_report_path
665
666
667def _GetCoverageHtmlReportPathForComponent(component_name):
668 """Given a component, returns the corresponding html report path."""
669 component_file_name = component_name.lower().replace('>', '-')
670 html_report_name = os.extsep.join([component_file_name, 'html'])
671 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
672 html_report_name)
673
674
675def _GetCoverageReportRootDirPath():
676 """The root directory that contains all generated coverage html reports."""
677 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33678
679
Yuke Liao506e8822017-12-04 16:52:54680def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
681 """Builds and runs target to generate the coverage profile data.
682
683 Args:
684 targets: A list of targets to build with coverage instrumentation.
685 commands: A list of commands used to run the targets.
686 jobs_count: Number of jobs to run in parallel for building. If None, a
687 default value is derived based on CPUs availability.
688
689 Returns:
690 A relative path to the generated profdata file.
691 """
692 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59693 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
694 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54695 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
696 profraw_file_paths)
697
Yuke Liaod4a9865202018-01-12 23:17:52698 for profraw_file_path in profraw_file_paths:
699 os.remove(profraw_file_path)
700
Yuke Liao506e8822017-12-04 16:52:54701 return profdata_file_path
702
703
704def _BuildTargets(targets, jobs_count):
705 """Builds target with Clang coverage instrumentation.
706
707 This function requires current working directory to be the root of checkout.
708
709 Args:
710 targets: A list of targets to build with coverage instrumentation.
711 jobs_count: Number of jobs to run in parallel for compilation. If None, a
712 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54713 """
Abhishek Arya1ec832c2017-12-05 18:06:59714
Yuke Liao506e8822017-12-04 16:52:54715 def _IsGomaConfigured():
716 """Returns True if goma is enabled in the gn build args.
717
718 Returns:
719 A boolean indicates whether goma is configured for building or not.
720 """
721 build_args = _ParseArgsGnFile()
722 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
723
Yuke Liao481d3482018-01-29 19:17:10724 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54725 if jobs_count is None and _IsGomaConfigured():
726 jobs_count = DEFAULT_GOMA_JOBS
727
728 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
729 if jobs_count is not None:
730 subprocess_cmd.append('-j' + str(jobs_count))
731
732 subprocess_cmd.extend(targets)
733 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10734 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54735
736
737def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
738 """Runs commands and returns the relative paths to the profraw data files.
739
740 Args:
741 targets: A list of targets built with coverage instrumentation.
742 commands: A list of commands used to run the targets.
743
744 Returns:
745 A list of relative paths to the generated profraw data files.
746 """
Yuke Liao481d3482018-01-29 19:17:10747 logging.debug('Executing the test commands')
748
Yuke Liao506e8822017-12-04 16:52:54749 # Remove existing profraw data files.
750 for file_or_dir in os.listdir(OUTPUT_DIR):
751 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
752 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
753
Yuke Liaod4a9865202018-01-12 23:17:52754 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54755 for target, command in zip(targets, commands):
Yuke Liaod4a9865202018-01-12 23:17:52756 _ExecuteCommand(target, command)
Yuke Liao506e8822017-12-04 16:52:54757
Yuke Liao481d3482018-01-29 19:17:10758 logging.debug('Finished executing the test commands')
759
Yuke Liao506e8822017-12-04 16:52:54760 profraw_file_paths = []
761 for file_or_dir in os.listdir(OUTPUT_DIR):
762 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
763 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
764
765 # Assert one target/command generates at least one profraw data file.
766 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59767 assert any(
768 os.path.basename(profraw_file).startswith(target)
769 for profraw_file in profraw_file_paths), (
770 'Running target: %s failed to generate any profraw data file, '
771 'please make sure the binary exists and is properly instrumented.' %
772 target)
Yuke Liao506e8822017-12-04 16:52:54773
774 return profraw_file_paths
775
776
777def _ExecuteCommand(target, command):
778 """Runs a single command and generates a profraw data file.
779
780 Args:
781 target: A target built with coverage instrumentation.
782 command: A command used to run the target.
783 """
Yuke Liaod4a9865202018-01-12 23:17:52784 # Per Clang "Source-based Code Coverage" doc:
785 # "%Nm" expands out to the instrumented binary's signature. When this pattern
786 # is specified, the runtime creates a pool of N raw profiles which are used
787 # for on-line profile merging. The runtime takes care of selecting a raw
788 # profile from the pool, locking it, and updating it before the program exits.
789 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
790 # N must be between 1 and 9. The merge pool specifier can only occur once per
791 # filename pattern.
792 #
793 # 4 is chosen because it creates some level of parallelism, but it's not too
794 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59795 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52796 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54797 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
798 expected_profraw_file_name)
799 output_file_name = os.extsep.join([target + '_output', 'txt'])
800 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
801
Yuke Liao481d3482018-01-29 19:17:10802 logging.info('Running command: "%s", the output is redirected to "%s"',
803 command, output_file_path)
Abhishek Arya1ec832c2017-12-05 18:06:59804 output = subprocess.check_output(
Yuke Liaodd1ec0592018-02-02 01:26:37805 command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liao506e8822017-12-04 16:52:54806 with open(output_file_path, 'w') as output_file:
807 output_file.write(output)
808
809
810def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
811 """Returns a relative path to the profdata file by merging profraw data files.
812
813 Args:
814 profraw_file_paths: A list of relative paths to the profraw data files that
815 are to be merged.
816
817 Returns:
818 A relative path to the generated profdata file.
819
820 Raises:
821 CalledProcessError: An error occurred merging profraw data files.
822 """
Yuke Liao481d3482018-01-29 19:17:10823 logging.info('Creating the coverage profile data file')
824 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54825 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54826 try:
Abhishek Arya1ec832c2017-12-05 18:06:59827 subprocess_cmd = [
828 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
829 ]
Yuke Liao506e8822017-12-04 16:52:54830 subprocess_cmd.extend(profraw_file_paths)
831 subprocess.check_call(subprocess_cmd)
832 except subprocess.CalledProcessError as error:
833 print('Failed to merge profraw files to create profdata file')
834 raise error
835
Yuke Liao481d3482018-01-29 19:17:10836 logging.debug('Finished merging profraw files')
837 logging.info('Code coverage profile data is created as: %s',
838 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54839 return profdata_file_path
840
841
Yuke Liaoea228d02018-01-05 19:10:33842def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
843 """Generates per file coverage summary using "llvm-cov export" command."""
844 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
845 # [[-object BIN]] [SOURCES].
846 # NOTE: For object files, the first one is specified as a positional argument,
847 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10848 logging.debug('Generating per-file code coverage summary using "llvm-cov '
849 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33850 subprocess_cmd = [
851 LLVM_COV_PATH, 'export', '-summary-only',
852 '-instr-profile=' + profdata_file_path, binary_paths[0]
853 ]
854 subprocess_cmd.extend(
855 ['-object=' + binary_path for binary_path in binary_paths[1:]])
856 subprocess_cmd.extend(filters)
857
858 json_output = json.loads(subprocess.check_output(subprocess_cmd))
859 assert len(json_output['data']) == 1
860 files_coverage_data = json_output['data'][0]['files']
861
862 per_file_coverage_summary = {}
863 for file_coverage_data in files_coverage_data:
864 file_path = file_coverage_data['filename']
865 summary = file_coverage_data['summary']
866
Yuke Liaoea228d02018-01-05 19:10:33867 if summary['lines']['count'] == 0:
868 continue
869
870 per_file_coverage_summary[file_path] = _CoverageSummary(
871 regions_total=summary['regions']['count'],
872 regions_covered=summary['regions']['covered'],
873 functions_total=summary['functions']['count'],
874 functions_covered=summary['functions']['covered'],
875 lines_total=summary['lines']['count'],
876 lines_covered=summary['lines']['covered'])
877
Yuke Liao481d3482018-01-29 19:17:10878 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33879 return per_file_coverage_summary
880
881
Yuke Liao506e8822017-12-04 16:52:54882def _GetBinaryPath(command):
883 """Returns a relative path to the binary to be run by the command.
884
Yuke Liao545db322018-02-15 17:12:01885 Currently, following types of commands are supported (e.g. url_unittests):
886 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
887 2. Use xvfb.
888 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
889 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
890
Yuke Liao506e8822017-12-04 16:52:54891 Args:
892 command: A command used to run a target.
893
894 Returns:
895 A relative path to the binary.
896 """
Yuke Liao545db322018-02-15 17:12:01897 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
898
899 command_parts = command.split()
900 if os.path.basename(command_parts[0]) == 'python':
901 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
902 'This tool doesn\'t understand the command: "%s"' % command)
903 return command_parts[2]
904
905 if os.path.basename(command_parts[0]) == xvfb_script_name:
906 return command_parts[1]
907
Yuke Liao506e8822017-12-04 16:52:54908 return command.split()[0]
909
910
Yuke Liao95d13d72017-12-07 18:18:50911def _VerifyTargetExecutablesAreInBuildDirectory(commands):
912 """Verifies that the target executables specified in the commands are inside
913 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:54914 for command in commands:
915 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:50916 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
917 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
918 'Target executable "%s" in command: "%s" is outside of '
919 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:54920
921
922def _ValidateBuildingWithClangCoverage():
923 """Asserts that targets are built with Clang coverage enabled."""
924 build_args = _ParseArgsGnFile()
925
926 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
927 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:59928 assert False, ('\'{} = true\' is required in args.gn.'
929 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:54930
931
932def _ParseArgsGnFile():
933 """Parses args.gn file and returns results as a dictionary.
934
935 Returns:
936 A dictionary representing the build args.
937 """
938 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
939 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
940 'missing args.gn file.' % BUILD_DIR)
941 with open(build_args_path) as build_args_file:
942 build_args_lines = build_args_file.readlines()
943
944 build_args = {}
945 for build_arg_line in build_args_lines:
946 build_arg_without_comments = build_arg_line.split('#')[0]
947 key_value_pair = build_arg_without_comments.split('=')
948 if len(key_value_pair) != 2:
949 continue
950
951 key = key_value_pair[0].strip()
952 value = key_value_pair[1].strip()
953 build_args[key] = value
954
955 return build_args
956
957
Abhishek Arya16f059a2017-12-07 17:47:32958def _VerifyPathsAndReturnAbsolutes(paths):
959 """Verifies that the paths specified in |paths| exist and returns absolute
960 versions.
Yuke Liao66da1732017-12-05 22:19:42961
962 Args:
963 paths: A list of files or directories.
964 """
Abhishek Arya16f059a2017-12-07 17:47:32965 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:42966 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:32967 absolute_path = os.path.join(SRC_ROOT_PATH, path)
968 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
969
970 absolute_paths.append(absolute_path)
971
972 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:42973
974
Yuke Liaodd1ec0592018-02-02 01:26:37975def _GetRelativePathToDirectoryOfFile(target_path, base_path):
976 """Returns a target path relative to the directory of base_path.
977
978 This method requires base_path to be a file, otherwise, one should call
979 os.path.relpath directly.
980 """
981 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:14982 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:37983 base_path)
Yuke Liaoc7e607142018-02-05 20:26:14984 base_dir = os.path.dirname(base_path)
985 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37986
987
Yuke Liao506e8822017-12-04 16:52:54988def _ParseCommandArguments():
989 """Adds and parses relevant arguments for tool comands.
990
991 Returns:
992 A dictionary representing the arguments.
993 """
994 arg_parser = argparse.ArgumentParser()
995 arg_parser.usage = __doc__
996
Abhishek Arya1ec832c2017-12-05 18:06:59997 arg_parser.add_argument(
998 '-b',
999 '--build-dir',
1000 type=str,
1001 required=True,
1002 help='The build directory, the path needs to be relative to the root of '
1003 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:541004
Abhishek Arya1ec832c2017-12-05 18:06:591005 arg_parser.add_argument(
1006 '-o',
1007 '--output-dir',
1008 type=str,
1009 required=True,
1010 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:541011
Abhishek Arya1ec832c2017-12-05 18:06:591012 arg_parser.add_argument(
1013 '-c',
1014 '--command',
1015 action='append',
1016 required=True,
1017 help='Commands used to run test targets, one test target needs one and '
1018 'only one command, when specifying commands, one should assume the '
1019 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541020
Abhishek Arya1ec832c2017-12-05 18:06:591021 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421022 '-f',
1023 '--filters',
1024 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321025 required=False,
Yuke Liao66da1732017-12-05 22:19:421026 help='Directories or files to get code coverage for, and all files under '
1027 'the directories are included recursively.')
1028
1029 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591030 '-j',
1031 '--jobs',
1032 type=int,
1033 default=None,
1034 help='Run N jobs to build in parallel. If not specified, a default value '
1035 'will be derived based on CPUs availability. Please refer to '
1036 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541037
Abhishek Arya1ec832c2017-12-05 18:06:591038 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101039 '-v',
1040 '--verbose',
1041 action='store_true',
1042 help='Prints additional output for diagnostics.')
1043
1044 arg_parser.add_argument(
1045 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1046
1047 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591048 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541049
1050 args = arg_parser.parse_args()
1051 return args
1052
1053
1054def Main():
1055 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371056 assert _GetPlatform() in [
1057 'linux', 'mac'
1058 ], ('Coverage is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541059 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1060 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591061 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541062 DownloadCoverageToolsIfNeeded()
1063
1064 args = _ParseCommandArguments()
1065 global BUILD_DIR
1066 BUILD_DIR = args.build_dir
1067 global OUTPUT_DIR
1068 OUTPUT_DIR = args.output_dir
1069
Yuke Liao481d3482018-01-29 19:17:101070 log_level = logging.DEBUG if args.verbose else logging.INFO
1071 log_format = '[%(asctime)s] %(message)s'
1072 log_file = args.log_file if args.log_file else None
1073 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1074
Yuke Liao506e8822017-12-04 16:52:541075 assert len(args.targets) == len(args.command), ('Number of targets must be '
1076 'equal to the number of test '
1077 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591078 assert os.path.exists(BUILD_DIR), (
1079 'Build directory: {} doesn\'t exist. '
1080 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541081 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501082 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321083
1084 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421085 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321086 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421087
Yuke Liao506e8822017-12-04 16:52:541088 if not os.path.exists(OUTPUT_DIR):
1089 os.makedirs(OUTPUT_DIR)
1090
Abhishek Arya1ec832c2017-12-05 18:06:591091 profdata_file_path = _CreateCoverageProfileDataForTargets(
1092 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541093 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331094
Yuke Liao481d3482018-01-29 19:17:101095 logging.info('Generating code coverage report in html (this can take a while '
1096 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371097 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1098 binary_paths, profdata_file_path, absolute_filter_paths)
1099 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1100 absolute_filter_paths)
1101 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1102
1103 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1104 per_file_coverage_summary)
1105 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1106 per_file_coverage_summary)
1107 _GenerateDirectoryViewHtmlIndexFile()
1108
1109 component_to_directories = _ExtractComponentToDirectoriesMapping()
1110 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1111 component_to_directories, per_directory_coverage_summary)
1112 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1113 component_to_directories,
1114 per_directory_coverage_summary)
1115 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331116
1117 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371118 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331119 _OverwriteHtmlReportsIndexFile()
1120
Yuke Liao506e8822017-12-04 16:52:541121 html_index_file_path = 'file://' + os.path.abspath(
1122 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101123 logging.info('Index file for html report is generated as: %s',
1124 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541125
Abhishek Arya1ec832c2017-12-05 18:06:591126
Yuke Liao506e8822017-12-04 16:52:541127if __name__ == '__main__':
1128 sys.exit(Main())