blob: c6de66493db0f1b9ff4fcb3ae037768d51a81688 [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
33 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
34 flag as well.
35
36 Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
37
Abhishek Arya16f059a2017-12-07 17:47:3238 python tools/code_coverage/coverage.py pdfium_fuzzer \\
39 -b out/coverage -o out/report \\
40 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
41 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5942
43 where:
44 <corpus_dir> - directory containing samples files for this format.
45 <runs> - number of times to fuzz target function. Should be 0 when you just
46 want to see the coverage on corpus and don't want to fuzz at all.
47
48 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao506e8822017-12-04 16:52:5449"""
50
51from __future__ import print_function
52
53import sys
54
55import argparse
Yuke Liaoea228d02018-01-05 19:10:3356import json
Yuke Liao481d3482018-01-29 19:17:1057import logging
Yuke Liao506e8822017-12-04 16:52:5458import os
59import subprocess
Yuke Liao506e8822017-12-04 16:52:5460import urllib2
61
Abhishek Arya1ec832c2017-12-05 18:06:5962sys.path.append(
63 os.path.join(
64 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
65 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5466import update as clang_update
67
Yuke Liaoea228d02018-01-05 19:10:3368sys.path.append(
69 os.path.join(
70 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
71 'third_party'))
72import jinja2
73from collections import defaultdict
74
Yuke Liao506e8822017-12-04 16:52:5475# Absolute path to the root of the checkout.
Abhishek Arya1ec832c2017-12-05 18:06:5976SRC_ROOT_PATH = os.path.abspath(
77 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Yuke Liao506e8822017-12-04 16:52:5478
79# Absolute path to the code coverage tools binary.
80LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
81LLVM_COV_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-cov')
82LLVM_PROFDATA_PATH = os.path.join(LLVM_BUILD_DIR, 'bin', 'llvm-profdata')
83
84# Build directory, the value is parsed from command line arguments.
85BUILD_DIR = None
86
87# Output directory for generated artifacts, the value is parsed from command
88# line arguemnts.
89OUTPUT_DIR = None
90
91# Default number of jobs used to build when goma is configured and enabled.
92DEFAULT_GOMA_JOBS = 100
93
94# Name of the file extension for profraw data files.
95PROFRAW_FILE_EXTENSION = 'profraw'
96
97# Name of the final profdata file, and this file needs to be passed to
98# "llvm-cov" command in order to call "llvm-cov show" to inspect the
99# line-by-line coverage of specific files.
100PROFDATA_FILE_NAME = 'coverage.profdata'
101
102# Build arg required for generating code coverage data.
103CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
104
Yuke Liaoea228d02018-01-05 19:10:33105# The default name of the html coverage report for a directory.
106DIRECTORY_COVERAGE_HTML_REPORT_NAME = os.extsep.join(['report', 'html'])
107
Yuke Liaodd1ec0592018-02-02 01:26:37108# Name of the html index files for different views.
109DIRECTORY_VIEW_INDEX_FILE = os.extsep.join(['directory_view_index', 'html'])
110COMPONENT_VIEW_INDEX_FILE = os.extsep.join(['component_view_index', 'html'])
111FILE_VIEW_INDEX_FILE = os.extsep.join(['file_view_index', 'html'])
112
113# Used to extract a mapping between directories and components.
114COMPONENT_MAPPING_URL = 'https://storage.googleapis.com/chromium-owners/component_map.json'
115
Yuke Liaoea228d02018-01-05 19:10:33116
117class _CoverageSummary(object):
118 """Encapsulates coverage summary representation."""
119
Yuke Liaodd1ec0592018-02-02 01:26:37120 def __init__(self,
121 regions_total=0,
122 regions_covered=0,
123 functions_total=0,
124 functions_covered=0,
125 lines_total=0,
126 lines_covered=0):
Yuke Liaoea228d02018-01-05 19:10:33127 """Initializes _CoverageSummary object."""
128 self._summary = {
129 'regions': {
130 'total': regions_total,
131 'covered': regions_covered
132 },
133 'functions': {
134 'total': functions_total,
135 'covered': functions_covered
136 },
137 'lines': {
138 'total': lines_total,
139 'covered': lines_covered
140 }
141 }
142
143 def Get(self):
144 """Returns summary as a dictionary."""
145 return self._summary
146
147 def AddSummary(self, other_summary):
148 """Adds another summary to this one element-wise."""
149 for feature in self._summary:
150 self._summary[feature]['total'] += other_summary.Get()[feature]['total']
151 self._summary[feature]['covered'] += other_summary.Get()[feature][
152 'covered']
153
154
Yuke Liaodd1ec0592018-02-02 01:26:37155class _CoverageReportHtmlGenerator(object):
156 """Encapsulates coverage html report generation.
Yuke Liaoea228d02018-01-05 19:10:33157
Yuke Liaodd1ec0592018-02-02 01:26:37158 The generated html has a table that contains links to other coverage reports.
Yuke Liaoea228d02018-01-05 19:10:33159 """
160
Yuke Liaodd1ec0592018-02-02 01:26:37161 def __init__(self, output_path, table_entry_type):
162 """Initializes _CoverageReportHtmlGenerator object.
163
164 Args:
165 output_path: Path to the html report that will be generated.
166 table_entry_type: Type of the table entries to be displayed in the table
167 header. For example: 'Path', 'Component'.
168 """
Yuke Liaoea228d02018-01-05 19:10:33169 css_file_name = os.extsep.join(['style', 'css'])
170 css_absolute_path = os.path.abspath(os.path.join(OUTPUT_DIR, css_file_name))
171 assert os.path.exists(css_absolute_path), (
172 'css file doesn\'t exit. Please make sure "llvm-cov show -format=html" '
173 'is called first, and the css file is generated at: "%s"' %
174 css_absolute_path)
175
176 self._css_absolute_path = css_absolute_path
Yuke Liaodd1ec0592018-02-02 01:26:37177 self._output_path = output_path
178 self._table_entry_type = table_entry_type
179
Yuke Liaoea228d02018-01-05 19:10:33180 self._table_entries = []
Yuke Liaod54030e2018-01-08 17:34:12181 self._total_entry = {}
Yuke Liaoea228d02018-01-05 19:10:33182 template_dir = os.path.join(
183 os.path.dirname(os.path.realpath(__file__)), 'html_templates')
184
185 jinja_env = jinja2.Environment(
186 loader=jinja2.FileSystemLoader(template_dir), trim_blocks=True)
187 self._header_template = jinja_env.get_template('header.html')
188 self._table_template = jinja_env.get_template('table.html')
189 self._footer_template = jinja_env.get_template('footer.html')
190
191 def AddLinkToAnotherReport(self, html_report_path, name, summary):
192 """Adds a link to another html report in this report.
193
194 The link to be added is assumed to be an entry in this directory.
195 """
Yuke Liaodd1ec0592018-02-02 01:26:37196 # Use relative paths instead of absolute paths to make the generated reports
197 # portable.
198 html_report_relative_path = _GetRelativePathToDirectoryOfFile(
199 html_report_path, self._output_path)
200
Yuke Liaod54030e2018-01-08 17:34:12201 table_entry = self._CreateTableEntryFromCoverageSummary(
Yuke Liaodd1ec0592018-02-02 01:26:37202 summary, html_report_relative_path, name,
Yuke Liaod54030e2018-01-08 17:34:12203 os.path.basename(html_report_path) ==
204 DIRECTORY_COVERAGE_HTML_REPORT_NAME)
205 self._table_entries.append(table_entry)
206
207 def CreateTotalsEntry(self, summary):
Yuke Liaoa785f4d32018-02-13 21:41:35208 """Creates an entry corresponds to the 'Totals' row in the html report."""
Yuke Liaod54030e2018-01-08 17:34:12209 self._total_entry = self._CreateTableEntryFromCoverageSummary(summary)
210
211 def _CreateTableEntryFromCoverageSummary(self,
212 summary,
213 href=None,
214 name=None,
215 is_dir=None):
216 """Creates an entry to display in the html report."""
Yuke Liaodd1ec0592018-02-02 01:26:37217 assert (href is None and name is None and is_dir is None) or (
218 href is not None and name is not None and is_dir is not None), (
219 'The only scenario when href or name or is_dir can be None is when '
Yuke Liaoa785f4d32018-02-13 21:41:35220 'creating an entry for the Totals row, and in that case, all three '
Yuke Liaodd1ec0592018-02-02 01:26:37221 'attributes must be None.')
222
Yuke Liaod54030e2018-01-08 17:34:12223 entry = {}
Yuke Liaodd1ec0592018-02-02 01:26:37224 if href is not None:
225 entry['href'] = href
226 if name is not None:
227 entry['name'] = name
228 if is_dir is not None:
229 entry['is_dir'] = is_dir
230
Yuke Liaoea228d02018-01-05 19:10:33231 summary_dict = summary.Get()
Yuke Liaod54030e2018-01-08 17:34:12232 for feature in summary_dict:
Yuke Liaodd1ec0592018-02-02 01:26:37233 if summary_dict[feature]['total'] == 0:
234 percentage = 0.0
235 else:
Yuke Liaoa785f4d32018-02-13 21:41:35236 percentage = float(summary_dict[feature]['covered']) / summary_dict[
237 feature]['total'] * 100
238
Yuke Liaoea228d02018-01-05 19:10:33239 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12240 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33241 'total': summary_dict[feature]['total'],
242 'covered': summary_dict[feature]['covered'],
Yuke Liaoa785f4d32018-02-13 21:41:35243 'percentage': '{:6.2f}'.format(percentage),
Yuke Liaoea228d02018-01-05 19:10:33244 'color_class': color_class
245 }
Yuke Liaod54030e2018-01-08 17:34:12246
Yuke Liaod54030e2018-01-08 17:34:12247 return entry
Yuke Liaoea228d02018-01-05 19:10:33248
249 def _GetColorClass(self, percentage):
250 """Returns the css color class based on coverage percentage."""
251 if percentage >= 0 and percentage < 80:
252 return 'red'
253 if percentage >= 80 and percentage < 100:
254 return 'yellow'
255 if percentage == 100:
256 return 'green'
257
258 assert False, 'Invalid coverage percentage: "%d"' % percentage
259
Yuke Liaodd1ec0592018-02-02 01:26:37260 def WriteHtmlCoverageReport(self):
261 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33262
263 In the report, sub-directories are displayed before files and within each
264 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33265 """
266
267 def EntryCmp(left, right):
268 """Compare function for table entries."""
269 if left['is_dir'] != right['is_dir']:
270 return -1 if left['is_dir'] == True else 1
271
Yuke Liaodd1ec0592018-02-02 01:26:37272 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33273
274 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
275
276 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37277 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
278 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
279 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
280
Yuke Liaoea228d02018-01-05 19:10:33281 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37282 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
283 directory_view_href=_GetRelativePathToDirectoryOfFile(
284 directory_view_path, self._output_path),
285 component_view_href=_GetRelativePathToDirectoryOfFile(
286 component_view_path, self._output_path),
287 file_view_href=_GetRelativePathToDirectoryOfFile(
288 file_view_path, self._output_path))
289
Yuke Liaod54030e2018-01-08 17:34:12290 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37291 entries=self._table_entries,
292 total_entry=self._total_entry,
293 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33294 html_footer = self._footer_template.render()
295
Yuke Liaodd1ec0592018-02-02 01:26:37296 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33297 html_file.write(html_header + html_table + html_footer)
298
Yuke Liao506e8822017-12-04 16:52:54299
Abhishek Arya1ec832c2017-12-05 18:06:59300def _GetPlatform():
301 """Returns current running platform."""
302 if sys.platform == 'win32' or sys.platform == 'cygwin':
303 return 'win'
304 if sys.platform.startswith('linux'):
305 return 'linux'
306 else:
307 assert sys.platform == 'darwin'
308 return 'mac'
309
310
Yuke Liao506e8822017-12-04 16:52:54311# TODO(crbug.com/759794): remove this function once tools get included to
312# Clang bundle:
313# https://chromium-review.googlesource.com/c/chromium/src/+/688221
314def DownloadCoverageToolsIfNeeded():
315 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59316
317 def _GetRevisionFromStampFile(stamp_file_path, platform):
Yuke Liao506e8822017-12-04 16:52:54318 """Returns a pair of revision number by reading the build stamp file.
319
320 Args:
321 stamp_file_path: A path the build stamp file created by
322 tools/clang/scripts/update.py.
323 Returns:
324 A pair of integers represeting the main and sub revision respectively.
325 """
326 if not os.path.exists(stamp_file_path):
327 return 0, 0
328
329 with open(stamp_file_path) as stamp_file:
Abhishek Arya1ec832c2017-12-05 18:06:59330 for stamp_file_line in stamp_file.readlines():
331 if ',' in stamp_file_line:
332 package_version, target_os = stamp_file_line.rstrip().split(',')
333 else:
334 package_version = stamp_file_line.rstrip()
335 target_os = ''
Yuke Liao506e8822017-12-04 16:52:54336
Abhishek Arya1ec832c2017-12-05 18:06:59337 if target_os and platform != target_os:
338 continue
339
340 clang_revision_str, clang_sub_revision_str = package_version.split('-')
341 return int(clang_revision_str), int(clang_sub_revision_str)
342
343 assert False, 'Coverage is only supported on target_os - linux, mac.'
344
345 platform = _GetPlatform()
Yuke Liao506e8822017-12-04 16:52:54346 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59347 clang_update.STAMP_FILE, platform)
Yuke Liao506e8822017-12-04 16:52:54348
349 coverage_revision_stamp_file = os.path.join(
350 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
351 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59352 coverage_revision_stamp_file, platform)
Yuke Liao506e8822017-12-04 16:52:54353
Yuke Liaoea228d02018-01-05 19:10:33354 has_coverage_tools = (
355 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32356
Yuke Liaoea228d02018-01-05 19:10:33357 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54358 coverage_sub_revision == clang_sub_revision):
359 # LLVM coverage tools are up to date, bail out.
360 return clang_revision
361
362 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
363 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
364
365 # The code bellow follows the code from tools/clang/scripts/update.py.
Abhishek Arya1ec832c2017-12-05 18:06:59366 if platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54367 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
368 else:
Abhishek Arya1ec832c2017-12-05 18:06:59369 assert platform == 'linux'
Yuke Liao506e8822017-12-04 16:52:54370 coverage_tools_url = (
371 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
372
373 try:
374 clang_update.DownloadAndUnpack(coverage_tools_url,
375 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10376 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54377 with open(coverage_revision_stamp_file, 'w') as file_handle:
Abhishek Arya1ec832c2017-12-05 18:06:59378 file_handle.write('%s,%s' % (package_version, platform))
Yuke Liao506e8822017-12-04 16:52:54379 file_handle.write('\n')
380 except urllib2.URLError:
381 raise Exception(
382 'Failed to download coverage tools: %s.' % coverage_tools_url)
383
384
Yuke Liaodd1ec0592018-02-02 01:26:37385def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
386 filters):
Yuke Liao506e8822017-12-04 16:52:54387 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
388
389 For a file with absolute path /a/b/x.cc, a html report is generated as:
390 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
391 OUTPUT_DIR/index.html.
392
393 Args:
394 binary_paths: A list of paths to the instrumented binaries.
395 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42396 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54397 """
Yuke Liao506e8822017-12-04 16:52:54398 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
399 # [[-object BIN]] [SOURCES]
400 # NOTE: For object files, the first one is specified as a positional argument,
401 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10402 logging.debug('Generating per file line by line coverage reports using '
403 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59404 subprocess_cmd = [
405 LLVM_COV_PATH, 'show', '-format=html',
406 '-output-dir={}'.format(OUTPUT_DIR),
407 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
408 ]
409 subprocess_cmd.extend(
410 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liao66da1732017-12-05 22:19:42411 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54412 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10413 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54414
415
Yuke Liaodd1ec0592018-02-02 01:26:37416def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
417 """Generates html index file for file view."""
418 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
419 logging.debug('Generating file view html index file as: "%s".',
420 file_view_index_file_path)
421 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
422 'Path')
423 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33424
Yuke Liaodd1ec0592018-02-02 01:26:37425 for file_path in per_file_coverage_summary:
426 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
427
428 html_generator.AddLinkToAnotherReport(
429 _GetCoverageHtmlReportPathForFile(file_path),
430 os.path.relpath(file_path, SRC_ROOT_PATH),
431 per_file_coverage_summary[file_path])
432
433 html_generator.CreateTotalsEntry(totals_coverage_summary)
434 html_generator.WriteHtmlCoverageReport()
435 logging.debug('Finished generating file view html index file.')
436
437
438def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
439 """Calculates per directory coverage summary."""
440 logging.debug('Calculating per-directory coverage summary')
441 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
442
Yuke Liaoea228d02018-01-05 19:10:33443 for file_path in per_file_coverage_summary:
444 summary = per_file_coverage_summary[file_path]
445 parent_dir = os.path.dirname(file_path)
446 while True:
447 per_directory_coverage_summary[parent_dir].AddSummary(summary)
448
449 if parent_dir == SRC_ROOT_PATH:
450 break
451 parent_dir = os.path.dirname(parent_dir)
452
Yuke Liaodd1ec0592018-02-02 01:26:37453 logging.debug('Finished calculating per-directory coverage summary')
454 return per_directory_coverage_summary
455
456
457def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
458 per_file_coverage_summary):
459 """Generates per directory coverage breakdown in html."""
460 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33461 for dir_path in per_directory_coverage_summary:
462 _GenerateCoverageInHtmlForDirectory(
463 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
464
Yuke Liaodd1ec0592018-02-02 01:26:37465 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10466
Yuke Liaoea228d02018-01-05 19:10:33467
468def _GenerateCoverageInHtmlForDirectory(
469 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
470 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37471 html_generator = _CoverageReportHtmlGenerator(
472 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33473
474 for entry_name in os.listdir(dir_path):
475 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33476
Yuke Liaodd1ec0592018-02-02 01:26:37477 if entry_path in per_file_coverage_summary:
478 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
479 entry_coverage_summary = per_file_coverage_summary[entry_path]
480 elif entry_path in per_directory_coverage_summary:
481 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
482 entry_path)
483 entry_coverage_summary = per_directory_coverage_summary[entry_path]
484 else:
Yuke Liaoc7e607142018-02-05 20:26:14485 # Any file without executable lines shouldn't be included into the report.
486 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37487 continue
Yuke Liaoea228d02018-01-05 19:10:33488
Yuke Liaodd1ec0592018-02-02 01:26:37489 html_generator.AddLinkToAnotherReport(entry_html_report_path,
490 os.path.basename(entry_path),
491 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33492
Yuke Liaod54030e2018-01-08 17:34:12493 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37494 html_generator.WriteHtmlCoverageReport()
495
496
497def _GenerateDirectoryViewHtmlIndexFile():
498 """Generates the html index file for directory view.
499
500 Note that the index file is already generated under SRC_ROOT_PATH, so this
501 file simply redirects to it, and the reason of this extra layer is for
502 structural consistency with other views.
503 """
504 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
505 DIRECTORY_VIEW_INDEX_FILE)
506 logging.debug('Generating directory view html index file as: "%s".',
507 directory_view_index_file_path)
508 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
509 SRC_ROOT_PATH)
510 _WriteRedirectHtmlFile(directory_view_index_file_path,
511 src_root_html_report_path)
512 logging.debug('Finished generating directory view html index file.')
513
514
515def _CalculatePerComponentCoverageSummary(component_to_directories,
516 per_directory_coverage_summary):
517 """Calculates per component coverage summary."""
518 logging.debug('Calculating per-component coverage summary')
519 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
520
521 for component in component_to_directories:
522 for directory in component_to_directories[component]:
523 absolute_directory_path = os.path.abspath(directory)
524 if absolute_directory_path in per_directory_coverage_summary:
525 per_component_coverage_summary[component].AddSummary(
526 per_directory_coverage_summary[absolute_directory_path])
527
528 logging.debug('Finished calculating per-component coverage summary')
529 return per_component_coverage_summary
530
531
532def _ExtractComponentToDirectoriesMapping():
533 """Returns a mapping from components to directories."""
534 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
535 directory_to_component = component_mappings['dir-to-component']
536
537 component_to_directories = defaultdict(list)
538 for directory in directory_to_component:
539 component = directory_to_component[directory]
540 component_to_directories[component].append(directory)
541
542 return component_to_directories
543
544
545def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
546 component_to_directories,
547 per_directory_coverage_summary):
548 """Generates per-component coverage reports in html."""
549 logging.debug('Writing per-component coverage html reports.')
550 for component in per_component_coverage_summary:
551 _GenerateCoverageInHtmlForComponent(
552 component, per_component_coverage_summary, component_to_directories,
553 per_directory_coverage_summary)
554
555 logging.debug('Finished writing per-component coverage html reports.')
556
557
558def _GenerateCoverageInHtmlForComponent(
559 component_name, per_component_coverage_summary, component_to_directories,
560 per_directory_coverage_summary):
561 """Generates coverage html report for a component."""
562 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
563 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14564 component_html_report_dir = os.path.dirname(component_html_report_path)
565 if not os.path.exists(component_html_report_dir):
566 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37567
568 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
569 'Path')
570
571 for dir_path in component_to_directories[component_name]:
572 dir_absolute_path = os.path.abspath(dir_path)
573 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14574 # Any directory without an excercised file shouldn't be included into the
575 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37576 continue
577
578 html_generator.AddLinkToAnotherReport(
579 _GetCoverageHtmlReportPathForDirectory(dir_path),
580 os.path.relpath(dir_path, SRC_ROOT_PATH),
581 per_directory_coverage_summary[dir_absolute_path])
582
583 html_generator.CreateTotalsEntry(
584 per_component_coverage_summary[component_name])
585 html_generator.WriteHtmlCoverageReport()
586
587
588def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
589 """Generates the html index file for component view."""
590 component_view_index_file_path = os.path.join(OUTPUT_DIR,
591 COMPONENT_VIEW_INDEX_FILE)
592 logging.debug('Generating component view html index file as: "%s".',
593 component_view_index_file_path)
594 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
595 'Component')
596 totals_coverage_summary = _CoverageSummary()
597
598 for component in per_component_coverage_summary:
599 totals_coverage_summary.AddSummary(
600 per_component_coverage_summary[component])
601
602 html_generator.AddLinkToAnotherReport(
603 _GetCoverageHtmlReportPathForComponent(component), component,
604 per_component_coverage_summary[component])
605
606 html_generator.CreateTotalsEntry(totals_coverage_summary)
607 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14608 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33609
610
611def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37612 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33613 html_index_file_path = os.path.join(OUTPUT_DIR,
614 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37615 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
616 DIRECTORY_VIEW_INDEX_FILE)
617 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
618
619
620def _WriteRedirectHtmlFile(from_html_path, to_html_path):
621 """Writes a html file that redirects to another html file."""
622 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
623 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33624 content = ("""
625 <!DOCTYPE html>
626 <html>
627 <head>
628 <!-- HTML meta refresh URL redirection -->
629 <meta http-equiv="refresh" content="0; url=%s">
630 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37631 </html>""" % to_html_relative_path)
632 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33633 f.write(content)
634
635
Yuke Liaodd1ec0592018-02-02 01:26:37636def _GetCoverageHtmlReportPathForFile(file_path):
637 """Given a file path, returns the corresponding html report path."""
638 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
639 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
640
641 # '+' is used instead of os.path.join because both of them are absolute paths
642 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14643 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37644 return _GetCoverageReportRootDirPath() + html_report_path
645
646
647def _GetCoverageHtmlReportPathForDirectory(dir_path):
648 """Given a directory path, returns the corresponding html report path."""
649 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
650 html_report_path = os.path.join(
651 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
652
653 # '+' is used instead of os.path.join because both of them are absolute paths
654 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14655 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37656 return _GetCoverageReportRootDirPath() + html_report_path
657
658
659def _GetCoverageHtmlReportPathForComponent(component_name):
660 """Given a component, returns the corresponding html report path."""
661 component_file_name = component_name.lower().replace('>', '-')
662 html_report_name = os.extsep.join([component_file_name, 'html'])
663 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
664 html_report_name)
665
666
667def _GetCoverageReportRootDirPath():
668 """The root directory that contains all generated coverage html reports."""
669 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33670
671
Yuke Liao506e8822017-12-04 16:52:54672def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
673 """Builds and runs target to generate the coverage profile data.
674
675 Args:
676 targets: A list of targets to build with coverage instrumentation.
677 commands: A list of commands used to run the targets.
678 jobs_count: Number of jobs to run in parallel for building. If None, a
679 default value is derived based on CPUs availability.
680
681 Returns:
682 A relative path to the generated profdata file.
683 """
684 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59685 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
686 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54687 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
688 profraw_file_paths)
689
Yuke Liaod4a9865202018-01-12 23:17:52690 for profraw_file_path in profraw_file_paths:
691 os.remove(profraw_file_path)
692
Yuke Liao506e8822017-12-04 16:52:54693 return profdata_file_path
694
695
696def _BuildTargets(targets, jobs_count):
697 """Builds target with Clang coverage instrumentation.
698
699 This function requires current working directory to be the root of checkout.
700
701 Args:
702 targets: A list of targets to build with coverage instrumentation.
703 jobs_count: Number of jobs to run in parallel for compilation. If None, a
704 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54705 """
Abhishek Arya1ec832c2017-12-05 18:06:59706
Yuke Liao506e8822017-12-04 16:52:54707 def _IsGomaConfigured():
708 """Returns True if goma is enabled in the gn build args.
709
710 Returns:
711 A boolean indicates whether goma is configured for building or not.
712 """
713 build_args = _ParseArgsGnFile()
714 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
715
Yuke Liao481d3482018-01-29 19:17:10716 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54717 if jobs_count is None and _IsGomaConfigured():
718 jobs_count = DEFAULT_GOMA_JOBS
719
720 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
721 if jobs_count is not None:
722 subprocess_cmd.append('-j' + str(jobs_count))
723
724 subprocess_cmd.extend(targets)
725 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10726 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54727
728
729def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
730 """Runs commands and returns the relative paths to the profraw data files.
731
732 Args:
733 targets: A list of targets built with coverage instrumentation.
734 commands: A list of commands used to run the targets.
735
736 Returns:
737 A list of relative paths to the generated profraw data files.
738 """
Yuke Liao481d3482018-01-29 19:17:10739 logging.debug('Executing the test commands')
740
Yuke Liao506e8822017-12-04 16:52:54741 # Remove existing profraw data files.
742 for file_or_dir in os.listdir(OUTPUT_DIR):
743 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
744 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
745
Yuke Liaod4a9865202018-01-12 23:17:52746 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54747 for target, command in zip(targets, commands):
Yuke Liaod4a9865202018-01-12 23:17:52748 _ExecuteCommand(target, command)
Yuke Liao506e8822017-12-04 16:52:54749
Yuke Liao481d3482018-01-29 19:17:10750 logging.debug('Finished executing the test commands')
751
Yuke Liao506e8822017-12-04 16:52:54752 profraw_file_paths = []
753 for file_or_dir in os.listdir(OUTPUT_DIR):
754 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
755 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
756
757 # Assert one target/command generates at least one profraw data file.
758 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59759 assert any(
760 os.path.basename(profraw_file).startswith(target)
761 for profraw_file in profraw_file_paths), (
762 'Running target: %s failed to generate any profraw data file, '
763 'please make sure the binary exists and is properly instrumented.' %
764 target)
Yuke Liao506e8822017-12-04 16:52:54765
766 return profraw_file_paths
767
768
769def _ExecuteCommand(target, command):
770 """Runs a single command and generates a profraw data file.
771
772 Args:
773 target: A target built with coverage instrumentation.
774 command: A command used to run the target.
775 """
Yuke Liaod4a9865202018-01-12 23:17:52776 # Per Clang "Source-based Code Coverage" doc:
777 # "%Nm" expands out to the instrumented binary's signature. When this pattern
778 # is specified, the runtime creates a pool of N raw profiles which are used
779 # for on-line profile merging. The runtime takes care of selecting a raw
780 # profile from the pool, locking it, and updating it before the program exits.
781 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
782 # N must be between 1 and 9. The merge pool specifier can only occur once per
783 # filename pattern.
784 #
785 # 4 is chosen because it creates some level of parallelism, but it's not too
786 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59787 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52788 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54789 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
790 expected_profraw_file_name)
791 output_file_name = os.extsep.join([target + '_output', 'txt'])
792 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
793
Yuke Liao481d3482018-01-29 19:17:10794 logging.info('Running command: "%s", the output is redirected to "%s"',
795 command, output_file_path)
Abhishek Arya1ec832c2017-12-05 18:06:59796 output = subprocess.check_output(
Yuke Liaodd1ec0592018-02-02 01:26:37797 command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liao506e8822017-12-04 16:52:54798 with open(output_file_path, 'w') as output_file:
799 output_file.write(output)
800
801
802def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
803 """Returns a relative path to the profdata file by merging profraw data files.
804
805 Args:
806 profraw_file_paths: A list of relative paths to the profraw data files that
807 are to be merged.
808
809 Returns:
810 A relative path to the generated profdata file.
811
812 Raises:
813 CalledProcessError: An error occurred merging profraw data files.
814 """
Yuke Liao481d3482018-01-29 19:17:10815 logging.info('Creating the coverage profile data file')
816 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54817 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54818 try:
Abhishek Arya1ec832c2017-12-05 18:06:59819 subprocess_cmd = [
820 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
821 ]
Yuke Liao506e8822017-12-04 16:52:54822 subprocess_cmd.extend(profraw_file_paths)
823 subprocess.check_call(subprocess_cmd)
824 except subprocess.CalledProcessError as error:
825 print('Failed to merge profraw files to create profdata file')
826 raise error
827
Yuke Liao481d3482018-01-29 19:17:10828 logging.debug('Finished merging profraw files')
829 logging.info('Code coverage profile data is created as: %s',
830 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54831 return profdata_file_path
832
833
Yuke Liaoea228d02018-01-05 19:10:33834def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
835 """Generates per file coverage summary using "llvm-cov export" command."""
836 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
837 # [[-object BIN]] [SOURCES].
838 # NOTE: For object files, the first one is specified as a positional argument,
839 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10840 logging.debug('Generating per-file code coverage summary using "llvm-cov '
841 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33842 subprocess_cmd = [
843 LLVM_COV_PATH, 'export', '-summary-only',
844 '-instr-profile=' + profdata_file_path, binary_paths[0]
845 ]
846 subprocess_cmd.extend(
847 ['-object=' + binary_path for binary_path in binary_paths[1:]])
848 subprocess_cmd.extend(filters)
849
850 json_output = json.loads(subprocess.check_output(subprocess_cmd))
851 assert len(json_output['data']) == 1
852 files_coverage_data = json_output['data'][0]['files']
853
854 per_file_coverage_summary = {}
855 for file_coverage_data in files_coverage_data:
856 file_path = file_coverage_data['filename']
857 summary = file_coverage_data['summary']
858
Yuke Liaoea228d02018-01-05 19:10:33859 if summary['lines']['count'] == 0:
860 continue
861
862 per_file_coverage_summary[file_path] = _CoverageSummary(
863 regions_total=summary['regions']['count'],
864 regions_covered=summary['regions']['covered'],
865 functions_total=summary['functions']['count'],
866 functions_covered=summary['functions']['covered'],
867 lines_total=summary['lines']['count'],
868 lines_covered=summary['lines']['covered'])
869
Yuke Liao481d3482018-01-29 19:17:10870 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33871 return per_file_coverage_summary
872
873
Yuke Liao506e8822017-12-04 16:52:54874def _GetBinaryPath(command):
875 """Returns a relative path to the binary to be run by the command.
876
877 Args:
878 command: A command used to run a target.
879
880 Returns:
881 A relative path to the binary.
882 """
883 return command.split()[0]
884
885
Yuke Liao95d13d72017-12-07 18:18:50886def _VerifyTargetExecutablesAreInBuildDirectory(commands):
887 """Verifies that the target executables specified in the commands are inside
888 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:54889 for command in commands:
890 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:50891 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
892 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
893 'Target executable "%s" in command: "%s" is outside of '
894 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:54895
896
897def _ValidateBuildingWithClangCoverage():
898 """Asserts that targets are built with Clang coverage enabled."""
899 build_args = _ParseArgsGnFile()
900
901 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
902 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:59903 assert False, ('\'{} = true\' is required in args.gn.'
904 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:54905
906
907def _ParseArgsGnFile():
908 """Parses args.gn file and returns results as a dictionary.
909
910 Returns:
911 A dictionary representing the build args.
912 """
913 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
914 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
915 'missing args.gn file.' % BUILD_DIR)
916 with open(build_args_path) as build_args_file:
917 build_args_lines = build_args_file.readlines()
918
919 build_args = {}
920 for build_arg_line in build_args_lines:
921 build_arg_without_comments = build_arg_line.split('#')[0]
922 key_value_pair = build_arg_without_comments.split('=')
923 if len(key_value_pair) != 2:
924 continue
925
926 key = key_value_pair[0].strip()
927 value = key_value_pair[1].strip()
928 build_args[key] = value
929
930 return build_args
931
932
Abhishek Arya16f059a2017-12-07 17:47:32933def _VerifyPathsAndReturnAbsolutes(paths):
934 """Verifies that the paths specified in |paths| exist and returns absolute
935 versions.
Yuke Liao66da1732017-12-05 22:19:42936
937 Args:
938 paths: A list of files or directories.
939 """
Abhishek Arya16f059a2017-12-07 17:47:32940 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:42941 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:32942 absolute_path = os.path.join(SRC_ROOT_PATH, path)
943 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
944
945 absolute_paths.append(absolute_path)
946
947 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:42948
949
Yuke Liaodd1ec0592018-02-02 01:26:37950def _GetRelativePathToDirectoryOfFile(target_path, base_path):
951 """Returns a target path relative to the directory of base_path.
952
953 This method requires base_path to be a file, otherwise, one should call
954 os.path.relpath directly.
955 """
956 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:14957 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:37958 base_path)
Yuke Liaoc7e607142018-02-05 20:26:14959 base_dir = os.path.dirname(base_path)
960 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37961
962
Yuke Liao506e8822017-12-04 16:52:54963def _ParseCommandArguments():
964 """Adds and parses relevant arguments for tool comands.
965
966 Returns:
967 A dictionary representing the arguments.
968 """
969 arg_parser = argparse.ArgumentParser()
970 arg_parser.usage = __doc__
971
Abhishek Arya1ec832c2017-12-05 18:06:59972 arg_parser.add_argument(
973 '-b',
974 '--build-dir',
975 type=str,
976 required=True,
977 help='The build directory, the path needs to be relative to the root of '
978 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:54979
Abhishek Arya1ec832c2017-12-05 18:06:59980 arg_parser.add_argument(
981 '-o',
982 '--output-dir',
983 type=str,
984 required=True,
985 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:54986
Abhishek Arya1ec832c2017-12-05 18:06:59987 arg_parser.add_argument(
988 '-c',
989 '--command',
990 action='append',
991 required=True,
992 help='Commands used to run test targets, one test target needs one and '
993 'only one command, when specifying commands, one should assume the '
994 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:54995
Abhishek Arya1ec832c2017-12-05 18:06:59996 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:42997 '-f',
998 '--filters',
999 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321000 required=False,
Yuke Liao66da1732017-12-05 22:19:421001 help='Directories or files to get code coverage for, and all files under '
1002 'the directories are included recursively.')
1003
1004 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591005 '-j',
1006 '--jobs',
1007 type=int,
1008 default=None,
1009 help='Run N jobs to build in parallel. If not specified, a default value '
1010 'will be derived based on CPUs availability. Please refer to '
1011 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541012
Abhishek Arya1ec832c2017-12-05 18:06:591013 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101014 '-v',
1015 '--verbose',
1016 action='store_true',
1017 help='Prints additional output for diagnostics.')
1018
1019 arg_parser.add_argument(
1020 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1021
1022 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591023 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541024
1025 args = arg_parser.parse_args()
1026 return args
1027
1028
1029def Main():
1030 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371031 assert _GetPlatform() in [
1032 'linux', 'mac'
1033 ], ('Coverage is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541034 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1035 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591036 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541037 DownloadCoverageToolsIfNeeded()
1038
1039 args = _ParseCommandArguments()
1040 global BUILD_DIR
1041 BUILD_DIR = args.build_dir
1042 global OUTPUT_DIR
1043 OUTPUT_DIR = args.output_dir
1044
Yuke Liao481d3482018-01-29 19:17:101045 log_level = logging.DEBUG if args.verbose else logging.INFO
1046 log_format = '[%(asctime)s] %(message)s'
1047 log_file = args.log_file if args.log_file else None
1048 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1049
Yuke Liao506e8822017-12-04 16:52:541050 assert len(args.targets) == len(args.command), ('Number of targets must be '
1051 'equal to the number of test '
1052 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591053 assert os.path.exists(BUILD_DIR), (
1054 'Build directory: {} doesn\'t exist. '
1055 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541056 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501057 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321058
1059 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421060 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321061 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421062
Yuke Liao506e8822017-12-04 16:52:541063 if not os.path.exists(OUTPUT_DIR):
1064 os.makedirs(OUTPUT_DIR)
1065
Abhishek Arya1ec832c2017-12-05 18:06:591066 profdata_file_path = _CreateCoverageProfileDataForTargets(
1067 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541068 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331069
Yuke Liao481d3482018-01-29 19:17:101070 logging.info('Generating code coverage report in html (this can take a while '
1071 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371072 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1073 binary_paths, profdata_file_path, absolute_filter_paths)
1074 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1075 absolute_filter_paths)
1076 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1077
1078 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1079 per_file_coverage_summary)
1080 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1081 per_file_coverage_summary)
1082 _GenerateDirectoryViewHtmlIndexFile()
1083
1084 component_to_directories = _ExtractComponentToDirectoriesMapping()
1085 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1086 component_to_directories, per_directory_coverage_summary)
1087 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1088 component_to_directories,
1089 per_directory_coverage_summary)
1090 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331091
1092 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371093 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331094 _OverwriteHtmlReportsIndexFile()
1095
Yuke Liao506e8822017-12-04 16:52:541096 html_index_file_path = 'file://' + os.path.abspath(
1097 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101098 logging.info('Index file for html report is generated as: %s',
1099 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541100
Abhishek Arya1ec832c2017-12-05 18:06:591101
Yuke Liao506e8822017-12-04 16:52:541102if __name__ == '__main__':
1103 sys.exit(Main())