blob: ecd9fb0399503a817fb3204e4a66d411a51632a8 [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):
208 """Creates an entry corresponds to the 'TOTALS' row in the html report."""
209 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 '
220 'creating an entry for the TOTALS row, and in that case, all three '
221 '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:
236 percentage = round((float(summary_dict[feature]['covered']
237 ) / summary_dict[feature]['total']) * 100, 2)
Yuke Liaoea228d02018-01-05 19:10:33238 color_class = self._GetColorClass(percentage)
Yuke Liaod54030e2018-01-08 17:34:12239 entry[feature] = {
Yuke Liaoea228d02018-01-05 19:10:33240 'total': summary_dict[feature]['total'],
241 'covered': summary_dict[feature]['covered'],
242 'percentage': percentage,
243 'color_class': color_class
244 }
Yuke Liaod54030e2018-01-08 17:34:12245
Yuke Liaod54030e2018-01-08 17:34:12246 return entry
Yuke Liaoea228d02018-01-05 19:10:33247
248 def _GetColorClass(self, percentage):
249 """Returns the css color class based on coverage percentage."""
250 if percentage >= 0 and percentage < 80:
251 return 'red'
252 if percentage >= 80 and percentage < 100:
253 return 'yellow'
254 if percentage == 100:
255 return 'green'
256
257 assert False, 'Invalid coverage percentage: "%d"' % percentage
258
Yuke Liaodd1ec0592018-02-02 01:26:37259 def WriteHtmlCoverageReport(self):
260 """Writes html coverage report.
Yuke Liaoea228d02018-01-05 19:10:33261
262 In the report, sub-directories are displayed before files and within each
263 category, entries are sorted alphabetically.
Yuke Liaoea228d02018-01-05 19:10:33264 """
265
266 def EntryCmp(left, right):
267 """Compare function for table entries."""
268 if left['is_dir'] != right['is_dir']:
269 return -1 if left['is_dir'] == True else 1
270
Yuke Liaodd1ec0592018-02-02 01:26:37271 return -1 if left['name'] < right['name'] else 1
Yuke Liaoea228d02018-01-05 19:10:33272
273 self._table_entries = sorted(self._table_entries, cmp=EntryCmp)
274
275 css_path = os.path.join(OUTPUT_DIR, os.extsep.join(['style', 'css']))
Yuke Liaodd1ec0592018-02-02 01:26:37276 directory_view_path = os.path.join(OUTPUT_DIR, DIRECTORY_VIEW_INDEX_FILE)
277 component_view_path = os.path.join(OUTPUT_DIR, COMPONENT_VIEW_INDEX_FILE)
278 file_view_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
279
Yuke Liaoea228d02018-01-05 19:10:33280 html_header = self._header_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37281 css_path=_GetRelativePathToDirectoryOfFile(css_path, self._output_path),
282 directory_view_href=_GetRelativePathToDirectoryOfFile(
283 directory_view_path, self._output_path),
284 component_view_href=_GetRelativePathToDirectoryOfFile(
285 component_view_path, self._output_path),
286 file_view_href=_GetRelativePathToDirectoryOfFile(
287 file_view_path, self._output_path))
288
Yuke Liaod54030e2018-01-08 17:34:12289 html_table = self._table_template.render(
Yuke Liaodd1ec0592018-02-02 01:26:37290 entries=self._table_entries,
291 total_entry=self._total_entry,
292 table_entry_type=self._table_entry_type)
Yuke Liaoea228d02018-01-05 19:10:33293 html_footer = self._footer_template.render()
294
Yuke Liaodd1ec0592018-02-02 01:26:37295 with open(self._output_path, 'w') as html_file:
Yuke Liaoea228d02018-01-05 19:10:33296 html_file.write(html_header + html_table + html_footer)
297
Yuke Liao506e8822017-12-04 16:52:54298
Abhishek Arya1ec832c2017-12-05 18:06:59299def _GetPlatform():
300 """Returns current running platform."""
301 if sys.platform == 'win32' or sys.platform == 'cygwin':
302 return 'win'
303 if sys.platform.startswith('linux'):
304 return 'linux'
305 else:
306 assert sys.platform == 'darwin'
307 return 'mac'
308
309
Yuke Liao506e8822017-12-04 16:52:54310# TODO(crbug.com/759794): remove this function once tools get included to
311# Clang bundle:
312# https://chromium-review.googlesource.com/c/chromium/src/+/688221
313def DownloadCoverageToolsIfNeeded():
314 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59315
316 def _GetRevisionFromStampFile(stamp_file_path, platform):
Yuke Liao506e8822017-12-04 16:52:54317 """Returns a pair of revision number by reading the build stamp file.
318
319 Args:
320 stamp_file_path: A path the build stamp file created by
321 tools/clang/scripts/update.py.
322 Returns:
323 A pair of integers represeting the main and sub revision respectively.
324 """
325 if not os.path.exists(stamp_file_path):
326 return 0, 0
327
328 with open(stamp_file_path) as stamp_file:
Abhishek Arya1ec832c2017-12-05 18:06:59329 for stamp_file_line in stamp_file.readlines():
330 if ',' in stamp_file_line:
331 package_version, target_os = stamp_file_line.rstrip().split(',')
332 else:
333 package_version = stamp_file_line.rstrip()
334 target_os = ''
Yuke Liao506e8822017-12-04 16:52:54335
Abhishek Arya1ec832c2017-12-05 18:06:59336 if target_os and platform != target_os:
337 continue
338
339 clang_revision_str, clang_sub_revision_str = package_version.split('-')
340 return int(clang_revision_str), int(clang_sub_revision_str)
341
342 assert False, 'Coverage is only supported on target_os - linux, mac.'
343
344 platform = _GetPlatform()
Yuke Liao506e8822017-12-04 16:52:54345 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59346 clang_update.STAMP_FILE, platform)
Yuke Liao506e8822017-12-04 16:52:54347
348 coverage_revision_stamp_file = os.path.join(
349 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
350 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Abhishek Arya1ec832c2017-12-05 18:06:59351 coverage_revision_stamp_file, platform)
Yuke Liao506e8822017-12-04 16:52:54352
Yuke Liaoea228d02018-01-05 19:10:33353 has_coverage_tools = (
354 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32355
Yuke Liaoea228d02018-01-05 19:10:33356 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54357 coverage_sub_revision == clang_sub_revision):
358 # LLVM coverage tools are up to date, bail out.
359 return clang_revision
360
361 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
362 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
363
364 # The code bellow follows the code from tools/clang/scripts/update.py.
Abhishek Arya1ec832c2017-12-05 18:06:59365 if platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54366 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
367 else:
Abhishek Arya1ec832c2017-12-05 18:06:59368 assert platform == 'linux'
Yuke Liao506e8822017-12-04 16:52:54369 coverage_tools_url = (
370 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
371
372 try:
373 clang_update.DownloadAndUnpack(coverage_tools_url,
374 clang_update.LLVM_BUILD_DIR)
Yuke Liao481d3482018-01-29 19:17:10375 logging.info('Coverage tools %s unpacked', package_version)
Yuke Liao506e8822017-12-04 16:52:54376 with open(coverage_revision_stamp_file, 'w') as file_handle:
Abhishek Arya1ec832c2017-12-05 18:06:59377 file_handle.write('%s,%s' % (package_version, platform))
Yuke Liao506e8822017-12-04 16:52:54378 file_handle.write('\n')
379 except urllib2.URLError:
380 raise Exception(
381 'Failed to download coverage tools: %s.' % coverage_tools_url)
382
383
Yuke Liaodd1ec0592018-02-02 01:26:37384def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
385 filters):
Yuke Liao506e8822017-12-04 16:52:54386 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
387
388 For a file with absolute path /a/b/x.cc, a html report is generated as:
389 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
390 OUTPUT_DIR/index.html.
391
392 Args:
393 binary_paths: A list of paths to the instrumented binaries.
394 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42395 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54396 """
Yuke Liao506e8822017-12-04 16:52:54397 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
398 # [[-object BIN]] [SOURCES]
399 # NOTE: For object files, the first one is specified as a positional argument,
400 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10401 logging.debug('Generating per file line by line coverage reports using '
402 '"llvm-cov show" command')
Abhishek Arya1ec832c2017-12-05 18:06:59403 subprocess_cmd = [
404 LLVM_COV_PATH, 'show', '-format=html',
405 '-output-dir={}'.format(OUTPUT_DIR),
406 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
407 ]
408 subprocess_cmd.extend(
409 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liao66da1732017-12-05 22:19:42410 subprocess_cmd.extend(filters)
Yuke Liao506e8822017-12-04 16:52:54411 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10412 logging.debug('Finished running "llvm-cov show" command')
Yuke Liao506e8822017-12-04 16:52:54413
414
Yuke Liaodd1ec0592018-02-02 01:26:37415def _GenerateFileViewHtmlIndexFile(per_file_coverage_summary):
416 """Generates html index file for file view."""
417 file_view_index_file_path = os.path.join(OUTPUT_DIR, FILE_VIEW_INDEX_FILE)
418 logging.debug('Generating file view html index file as: "%s".',
419 file_view_index_file_path)
420 html_generator = _CoverageReportHtmlGenerator(file_view_index_file_path,
421 'Path')
422 totals_coverage_summary = _CoverageSummary()
Yuke Liaoea228d02018-01-05 19:10:33423
Yuke Liaodd1ec0592018-02-02 01:26:37424 for file_path in per_file_coverage_summary:
425 totals_coverage_summary.AddSummary(per_file_coverage_summary[file_path])
426
427 html_generator.AddLinkToAnotherReport(
428 _GetCoverageHtmlReportPathForFile(file_path),
429 os.path.relpath(file_path, SRC_ROOT_PATH),
430 per_file_coverage_summary[file_path])
431
432 html_generator.CreateTotalsEntry(totals_coverage_summary)
433 html_generator.WriteHtmlCoverageReport()
434 logging.debug('Finished generating file view html index file.')
435
436
437def _CalculatePerDirectoryCoverageSummary(per_file_coverage_summary):
438 """Calculates per directory coverage summary."""
439 logging.debug('Calculating per-directory coverage summary')
440 per_directory_coverage_summary = defaultdict(lambda: _CoverageSummary())
441
Yuke Liaoea228d02018-01-05 19:10:33442 for file_path in per_file_coverage_summary:
443 summary = per_file_coverage_summary[file_path]
444 parent_dir = os.path.dirname(file_path)
445 while True:
446 per_directory_coverage_summary[parent_dir].AddSummary(summary)
447
448 if parent_dir == SRC_ROOT_PATH:
449 break
450 parent_dir = os.path.dirname(parent_dir)
451
Yuke Liaodd1ec0592018-02-02 01:26:37452 logging.debug('Finished calculating per-directory coverage summary')
453 return per_directory_coverage_summary
454
455
456def _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
457 per_file_coverage_summary):
458 """Generates per directory coverage breakdown in html."""
459 logging.debug('Writing per-directory coverage html reports')
Yuke Liaoea228d02018-01-05 19:10:33460 for dir_path in per_directory_coverage_summary:
461 _GenerateCoverageInHtmlForDirectory(
462 dir_path, per_directory_coverage_summary, per_file_coverage_summary)
463
Yuke Liaodd1ec0592018-02-02 01:26:37464 logging.debug('Finished writing per-directory coverage html reports')
Yuke Liao481d3482018-01-29 19:17:10465
Yuke Liaoea228d02018-01-05 19:10:33466
467def _GenerateCoverageInHtmlForDirectory(
468 dir_path, per_directory_coverage_summary, per_file_coverage_summary):
469 """Generates coverage html report for a single directory."""
Yuke Liaodd1ec0592018-02-02 01:26:37470 html_generator = _CoverageReportHtmlGenerator(
471 _GetCoverageHtmlReportPathForDirectory(dir_path), 'Path')
Yuke Liaoea228d02018-01-05 19:10:33472
473 for entry_name in os.listdir(dir_path):
474 entry_path = os.path.normpath(os.path.join(dir_path, entry_name))
Yuke Liaoea228d02018-01-05 19:10:33475
Yuke Liaodd1ec0592018-02-02 01:26:37476 if entry_path in per_file_coverage_summary:
477 entry_html_report_path = _GetCoverageHtmlReportPathForFile(entry_path)
478 entry_coverage_summary = per_file_coverage_summary[entry_path]
479 elif entry_path in per_directory_coverage_summary:
480 entry_html_report_path = _GetCoverageHtmlReportPathForDirectory(
481 entry_path)
482 entry_coverage_summary = per_directory_coverage_summary[entry_path]
483 else:
Yuke Liaoc7e607142018-02-05 20:26:14484 # Any file without executable lines shouldn't be included into the report.
485 # For example, OWNER and README.md files.
Yuke Liaodd1ec0592018-02-02 01:26:37486 continue
Yuke Liaoea228d02018-01-05 19:10:33487
Yuke Liaodd1ec0592018-02-02 01:26:37488 html_generator.AddLinkToAnotherReport(entry_html_report_path,
489 os.path.basename(entry_path),
490 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33491
Yuke Liaod54030e2018-01-08 17:34:12492 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37493 html_generator.WriteHtmlCoverageReport()
494
495
496def _GenerateDirectoryViewHtmlIndexFile():
497 """Generates the html index file for directory view.
498
499 Note that the index file is already generated under SRC_ROOT_PATH, so this
500 file simply redirects to it, and the reason of this extra layer is for
501 structural consistency with other views.
502 """
503 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
504 DIRECTORY_VIEW_INDEX_FILE)
505 logging.debug('Generating directory view html index file as: "%s".',
506 directory_view_index_file_path)
507 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
508 SRC_ROOT_PATH)
509 _WriteRedirectHtmlFile(directory_view_index_file_path,
510 src_root_html_report_path)
511 logging.debug('Finished generating directory view html index file.')
512
513
514def _CalculatePerComponentCoverageSummary(component_to_directories,
515 per_directory_coverage_summary):
516 """Calculates per component coverage summary."""
517 logging.debug('Calculating per-component coverage summary')
518 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
519
520 for component in component_to_directories:
521 for directory in component_to_directories[component]:
522 absolute_directory_path = os.path.abspath(directory)
523 if absolute_directory_path in per_directory_coverage_summary:
524 per_component_coverage_summary[component].AddSummary(
525 per_directory_coverage_summary[absolute_directory_path])
526
527 logging.debug('Finished calculating per-component coverage summary')
528 return per_component_coverage_summary
529
530
531def _ExtractComponentToDirectoriesMapping():
532 """Returns a mapping from components to directories."""
533 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
534 directory_to_component = component_mappings['dir-to-component']
535
536 component_to_directories = defaultdict(list)
537 for directory in directory_to_component:
538 component = directory_to_component[directory]
539 component_to_directories[component].append(directory)
540
541 return component_to_directories
542
543
544def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
545 component_to_directories,
546 per_directory_coverage_summary):
547 """Generates per-component coverage reports in html."""
548 logging.debug('Writing per-component coverage html reports.')
549 for component in per_component_coverage_summary:
550 _GenerateCoverageInHtmlForComponent(
551 component, per_component_coverage_summary, component_to_directories,
552 per_directory_coverage_summary)
553
554 logging.debug('Finished writing per-component coverage html reports.')
555
556
557def _GenerateCoverageInHtmlForComponent(
558 component_name, per_component_coverage_summary, component_to_directories,
559 per_directory_coverage_summary):
560 """Generates coverage html report for a component."""
561 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
562 component_name)
Yuke Liaoc7e607142018-02-05 20:26:14563 component_html_report_dir = os.path.dirname(component_html_report_path)
564 if not os.path.exists(component_html_report_dir):
565 os.makedirs(component_html_report_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37566
567 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
568 'Path')
569
570 for dir_path in component_to_directories[component_name]:
571 dir_absolute_path = os.path.abspath(dir_path)
572 if dir_absolute_path not in per_directory_coverage_summary:
Yuke Liaoc7e607142018-02-05 20:26:14573 # Any directory without an excercised file shouldn't be included into the
574 # report.
Yuke Liaodd1ec0592018-02-02 01:26:37575 continue
576
577 html_generator.AddLinkToAnotherReport(
578 _GetCoverageHtmlReportPathForDirectory(dir_path),
579 os.path.relpath(dir_path, SRC_ROOT_PATH),
580 per_directory_coverage_summary[dir_absolute_path])
581
582 html_generator.CreateTotalsEntry(
583 per_component_coverage_summary[component_name])
584 html_generator.WriteHtmlCoverageReport()
585
586
587def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
588 """Generates the html index file for component view."""
589 component_view_index_file_path = os.path.join(OUTPUT_DIR,
590 COMPONENT_VIEW_INDEX_FILE)
591 logging.debug('Generating component view html index file as: "%s".',
592 component_view_index_file_path)
593 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
594 'Component')
595 totals_coverage_summary = _CoverageSummary()
596
597 for component in per_component_coverage_summary:
598 totals_coverage_summary.AddSummary(
599 per_component_coverage_summary[component])
600
601 html_generator.AddLinkToAnotherReport(
602 _GetCoverageHtmlReportPathForComponent(component), component,
603 per_component_coverage_summary[component])
604
605 html_generator.CreateTotalsEntry(totals_coverage_summary)
606 html_generator.WriteHtmlCoverageReport()
Yuke Liaoc7e607142018-02-05 20:26:14607 logging.debug('Finished generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33608
609
610def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37611 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33612 html_index_file_path = os.path.join(OUTPUT_DIR,
613 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37614 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
615 DIRECTORY_VIEW_INDEX_FILE)
616 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
617
618
619def _WriteRedirectHtmlFile(from_html_path, to_html_path):
620 """Writes a html file that redirects to another html file."""
621 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
622 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33623 content = ("""
624 <!DOCTYPE html>
625 <html>
626 <head>
627 <!-- HTML meta refresh URL redirection -->
628 <meta http-equiv="refresh" content="0; url=%s">
629 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37630 </html>""" % to_html_relative_path)
631 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33632 f.write(content)
633
634
Yuke Liaodd1ec0592018-02-02 01:26:37635def _GetCoverageHtmlReportPathForFile(file_path):
636 """Given a file path, returns the corresponding html report path."""
637 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
638 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
639
640 # '+' is used instead of os.path.join because both of them are absolute paths
641 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14642 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37643 return _GetCoverageReportRootDirPath() + html_report_path
644
645
646def _GetCoverageHtmlReportPathForDirectory(dir_path):
647 """Given a directory path, returns the corresponding html report path."""
648 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
649 html_report_path = os.path.join(
650 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
651
652 # '+' is used instead of os.path.join because both of them are absolute paths
653 # and os.path.join ignores the first path.
Yuke Liaoc7e607142018-02-05 20:26:14654 # TODO(crbug.com/809150): Think of a generic cross platform fix (Windows).
Yuke Liaodd1ec0592018-02-02 01:26:37655 return _GetCoverageReportRootDirPath() + html_report_path
656
657
658def _GetCoverageHtmlReportPathForComponent(component_name):
659 """Given a component, returns the corresponding html report path."""
660 component_file_name = component_name.lower().replace('>', '-')
661 html_report_name = os.extsep.join([component_file_name, 'html'])
662 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
663 html_report_name)
664
665
666def _GetCoverageReportRootDirPath():
667 """The root directory that contains all generated coverage html reports."""
668 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33669
670
Yuke Liao506e8822017-12-04 16:52:54671def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
672 """Builds and runs target to generate the coverage profile data.
673
674 Args:
675 targets: A list of targets to build with coverage instrumentation.
676 commands: A list of commands used to run the targets.
677 jobs_count: Number of jobs to run in parallel for building. If None, a
678 default value is derived based on CPUs availability.
679
680 Returns:
681 A relative path to the generated profdata file.
682 """
683 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59684 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
685 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54686 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
687 profraw_file_paths)
688
Yuke Liaod4a9865202018-01-12 23:17:52689 for profraw_file_path in profraw_file_paths:
690 os.remove(profraw_file_path)
691
Yuke Liao506e8822017-12-04 16:52:54692 return profdata_file_path
693
694
695def _BuildTargets(targets, jobs_count):
696 """Builds target with Clang coverage instrumentation.
697
698 This function requires current working directory to be the root of checkout.
699
700 Args:
701 targets: A list of targets to build with coverage instrumentation.
702 jobs_count: Number of jobs to run in parallel for compilation. If None, a
703 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54704 """
Abhishek Arya1ec832c2017-12-05 18:06:59705
Yuke Liao506e8822017-12-04 16:52:54706 def _IsGomaConfigured():
707 """Returns True if goma is enabled in the gn build args.
708
709 Returns:
710 A boolean indicates whether goma is configured for building or not.
711 """
712 build_args = _ParseArgsGnFile()
713 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
714
Yuke Liao481d3482018-01-29 19:17:10715 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54716 if jobs_count is None and _IsGomaConfigured():
717 jobs_count = DEFAULT_GOMA_JOBS
718
719 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
720 if jobs_count is not None:
721 subprocess_cmd.append('-j' + str(jobs_count))
722
723 subprocess_cmd.extend(targets)
724 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10725 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54726
727
728def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
729 """Runs commands and returns the relative paths to the profraw data files.
730
731 Args:
732 targets: A list of targets built with coverage instrumentation.
733 commands: A list of commands used to run the targets.
734
735 Returns:
736 A list of relative paths to the generated profraw data files.
737 """
Yuke Liao481d3482018-01-29 19:17:10738 logging.debug('Executing the test commands')
739
Yuke Liao506e8822017-12-04 16:52:54740 # Remove existing profraw data files.
741 for file_or_dir in os.listdir(OUTPUT_DIR):
742 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
743 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
744
Yuke Liaod4a9865202018-01-12 23:17:52745 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54746 for target, command in zip(targets, commands):
Yuke Liaod4a9865202018-01-12 23:17:52747 _ExecuteCommand(target, command)
Yuke Liao506e8822017-12-04 16:52:54748
Yuke Liao481d3482018-01-29 19:17:10749 logging.debug('Finished executing the test commands')
750
Yuke Liao506e8822017-12-04 16:52:54751 profraw_file_paths = []
752 for file_or_dir in os.listdir(OUTPUT_DIR):
753 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
754 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
755
756 # Assert one target/command generates at least one profraw data file.
757 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59758 assert any(
759 os.path.basename(profraw_file).startswith(target)
760 for profraw_file in profraw_file_paths), (
761 'Running target: %s failed to generate any profraw data file, '
762 'please make sure the binary exists and is properly instrumented.' %
763 target)
Yuke Liao506e8822017-12-04 16:52:54764
765 return profraw_file_paths
766
767
768def _ExecuteCommand(target, command):
769 """Runs a single command and generates a profraw data file.
770
771 Args:
772 target: A target built with coverage instrumentation.
773 command: A command used to run the target.
774 """
Yuke Liaod4a9865202018-01-12 23:17:52775 # Per Clang "Source-based Code Coverage" doc:
776 # "%Nm" expands out to the instrumented binary's signature. When this pattern
777 # is specified, the runtime creates a pool of N raw profiles which are used
778 # for on-line profile merging. The runtime takes care of selecting a raw
779 # profile from the pool, locking it, and updating it before the program exits.
780 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
781 # N must be between 1 and 9. The merge pool specifier can only occur once per
782 # filename pattern.
783 #
784 # 4 is chosen because it creates some level of parallelism, but it's not too
785 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59786 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52787 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54788 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
789 expected_profraw_file_name)
790 output_file_name = os.extsep.join([target + '_output', 'txt'])
791 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
792
Yuke Liao481d3482018-01-29 19:17:10793 logging.info('Running command: "%s", the output is redirected to "%s"',
794 command, output_file_path)
Abhishek Arya1ec832c2017-12-05 18:06:59795 output = subprocess.check_output(
Yuke Liaodd1ec0592018-02-02 01:26:37796 command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liao506e8822017-12-04 16:52:54797 with open(output_file_path, 'w') as output_file:
798 output_file.write(output)
799
800
801def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
802 """Returns a relative path to the profdata file by merging profraw data files.
803
804 Args:
805 profraw_file_paths: A list of relative paths to the profraw data files that
806 are to be merged.
807
808 Returns:
809 A relative path to the generated profdata file.
810
811 Raises:
812 CalledProcessError: An error occurred merging profraw data files.
813 """
Yuke Liao481d3482018-01-29 19:17:10814 logging.info('Creating the coverage profile data file')
815 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54816 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54817 try:
Abhishek Arya1ec832c2017-12-05 18:06:59818 subprocess_cmd = [
819 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
820 ]
Yuke Liao506e8822017-12-04 16:52:54821 subprocess_cmd.extend(profraw_file_paths)
822 subprocess.check_call(subprocess_cmd)
823 except subprocess.CalledProcessError as error:
824 print('Failed to merge profraw files to create profdata file')
825 raise error
826
Yuke Liao481d3482018-01-29 19:17:10827 logging.debug('Finished merging profraw files')
828 logging.info('Code coverage profile data is created as: %s',
829 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54830 return profdata_file_path
831
832
Yuke Liaoea228d02018-01-05 19:10:33833def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
834 """Generates per file coverage summary using "llvm-cov export" command."""
835 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
836 # [[-object BIN]] [SOURCES].
837 # NOTE: For object files, the first one is specified as a positional argument,
838 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10839 logging.debug('Generating per-file code coverage summary using "llvm-cov '
840 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33841 subprocess_cmd = [
842 LLVM_COV_PATH, 'export', '-summary-only',
843 '-instr-profile=' + profdata_file_path, binary_paths[0]
844 ]
845 subprocess_cmd.extend(
846 ['-object=' + binary_path for binary_path in binary_paths[1:]])
847 subprocess_cmd.extend(filters)
848
849 json_output = json.loads(subprocess.check_output(subprocess_cmd))
850 assert len(json_output['data']) == 1
851 files_coverage_data = json_output['data'][0]['files']
852
853 per_file_coverage_summary = {}
854 for file_coverage_data in files_coverage_data:
855 file_path = file_coverage_data['filename']
856 summary = file_coverage_data['summary']
857
858 # TODO(crbug.com/797345): Currently, [SOURCES] parameter doesn't apply to
859 # llvm-cov export command, so work it around by manually filter the paths.
860 # Remove this logic once the bug is fixed and clang has rolled past it.
861 if filters and not any(
862 os.path.abspath(file_path).startswith(os.path.abspath(filter))
863 for filter in filters):
864 continue
865
866 if summary['lines']['count'] == 0:
867 continue
868
869 per_file_coverage_summary[file_path] = _CoverageSummary(
870 regions_total=summary['regions']['count'],
871 regions_covered=summary['regions']['covered'],
872 functions_total=summary['functions']['count'],
873 functions_covered=summary['functions']['covered'],
874 lines_total=summary['lines']['count'],
875 lines_covered=summary['lines']['covered'])
876
Yuke Liao481d3482018-01-29 19:17:10877 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33878 return per_file_coverage_summary
879
880
Yuke Liao506e8822017-12-04 16:52:54881def _GetBinaryPath(command):
882 """Returns a relative path to the binary to be run by the command.
883
884 Args:
885 command: A command used to run a target.
886
887 Returns:
888 A relative path to the binary.
889 """
890 return command.split()[0]
891
892
Yuke Liao95d13d72017-12-07 18:18:50893def _VerifyTargetExecutablesAreInBuildDirectory(commands):
894 """Verifies that the target executables specified in the commands are inside
895 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:54896 for command in commands:
897 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:50898 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
899 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
900 'Target executable "%s" in command: "%s" is outside of '
901 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:54902
903
904def _ValidateBuildingWithClangCoverage():
905 """Asserts that targets are built with Clang coverage enabled."""
906 build_args = _ParseArgsGnFile()
907
908 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
909 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:59910 assert False, ('\'{} = true\' is required in args.gn.'
911 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:54912
913
914def _ParseArgsGnFile():
915 """Parses args.gn file and returns results as a dictionary.
916
917 Returns:
918 A dictionary representing the build args.
919 """
920 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
921 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
922 'missing args.gn file.' % BUILD_DIR)
923 with open(build_args_path) as build_args_file:
924 build_args_lines = build_args_file.readlines()
925
926 build_args = {}
927 for build_arg_line in build_args_lines:
928 build_arg_without_comments = build_arg_line.split('#')[0]
929 key_value_pair = build_arg_without_comments.split('=')
930 if len(key_value_pair) != 2:
931 continue
932
933 key = key_value_pair[0].strip()
934 value = key_value_pair[1].strip()
935 build_args[key] = value
936
937 return build_args
938
939
Abhishek Arya16f059a2017-12-07 17:47:32940def _VerifyPathsAndReturnAbsolutes(paths):
941 """Verifies that the paths specified in |paths| exist and returns absolute
942 versions.
Yuke Liao66da1732017-12-05 22:19:42943
944 Args:
945 paths: A list of files or directories.
946 """
Abhishek Arya16f059a2017-12-07 17:47:32947 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:42948 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:32949 absolute_path = os.path.join(SRC_ROOT_PATH, path)
950 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
951
952 absolute_paths.append(absolute_path)
953
954 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:42955
956
Yuke Liaodd1ec0592018-02-02 01:26:37957def _GetRelativePathToDirectoryOfFile(target_path, base_path):
958 """Returns a target path relative to the directory of base_path.
959
960 This method requires base_path to be a file, otherwise, one should call
961 os.path.relpath directly.
962 """
963 assert os.path.dirname(base_path) != base_path, (
Yuke Liaoc7e607142018-02-05 20:26:14964 'Base path: "%s" is a directory, please call os.path.relpath directly.' %
Yuke Liaodd1ec0592018-02-02 01:26:37965 base_path)
Yuke Liaoc7e607142018-02-05 20:26:14966 base_dir = os.path.dirname(base_path)
967 return os.path.relpath(target_path, base_dir)
Yuke Liaodd1ec0592018-02-02 01:26:37968
969
Yuke Liao506e8822017-12-04 16:52:54970def _ParseCommandArguments():
971 """Adds and parses relevant arguments for tool comands.
972
973 Returns:
974 A dictionary representing the arguments.
975 """
976 arg_parser = argparse.ArgumentParser()
977 arg_parser.usage = __doc__
978
Abhishek Arya1ec832c2017-12-05 18:06:59979 arg_parser.add_argument(
980 '-b',
981 '--build-dir',
982 type=str,
983 required=True,
984 help='The build directory, the path needs to be relative to the root of '
985 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:54986
Abhishek Arya1ec832c2017-12-05 18:06:59987 arg_parser.add_argument(
988 '-o',
989 '--output-dir',
990 type=str,
991 required=True,
992 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:54993
Abhishek Arya1ec832c2017-12-05 18:06:59994 arg_parser.add_argument(
995 '-c',
996 '--command',
997 action='append',
998 required=True,
999 help='Commands used to run test targets, one test target needs one and '
1000 'only one command, when specifying commands, one should assume the '
1001 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:541002
Abhishek Arya1ec832c2017-12-05 18:06:591003 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:421004 '-f',
1005 '--filters',
1006 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321007 required=False,
Yuke Liao66da1732017-12-05 22:19:421008 help='Directories or files to get code coverage for, and all files under '
1009 'the directories are included recursively.')
1010
1011 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591012 '-j',
1013 '--jobs',
1014 type=int,
1015 default=None,
1016 help='Run N jobs to build in parallel. If not specified, a default value '
1017 'will be derived based on CPUs availability. Please refer to '
1018 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541019
Abhishek Arya1ec832c2017-12-05 18:06:591020 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101021 '-v',
1022 '--verbose',
1023 action='store_true',
1024 help='Prints additional output for diagnostics.')
1025
1026 arg_parser.add_argument(
1027 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1028
1029 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591030 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541031
1032 args = arg_parser.parse_args()
1033 return args
1034
1035
1036def Main():
1037 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371038 assert _GetPlatform() in [
1039 'linux', 'mac'
1040 ], ('Coverage is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541041 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1042 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591043 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541044 DownloadCoverageToolsIfNeeded()
1045
1046 args = _ParseCommandArguments()
1047 global BUILD_DIR
1048 BUILD_DIR = args.build_dir
1049 global OUTPUT_DIR
1050 OUTPUT_DIR = args.output_dir
1051
Yuke Liao481d3482018-01-29 19:17:101052 log_level = logging.DEBUG if args.verbose else logging.INFO
1053 log_format = '[%(asctime)s] %(message)s'
1054 log_file = args.log_file if args.log_file else None
1055 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1056
Yuke Liao506e8822017-12-04 16:52:541057 assert len(args.targets) == len(args.command), ('Number of targets must be '
1058 'equal to the number of test '
1059 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591060 assert os.path.exists(BUILD_DIR), (
1061 'Build directory: {} doesn\'t exist. '
1062 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541063 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501064 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321065
1066 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421067 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321068 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421069
Yuke Liao506e8822017-12-04 16:52:541070 if not os.path.exists(OUTPUT_DIR):
1071 os.makedirs(OUTPUT_DIR)
1072
Abhishek Arya1ec832c2017-12-05 18:06:591073 profdata_file_path = _CreateCoverageProfileDataForTargets(
1074 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541075 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331076
Yuke Liao481d3482018-01-29 19:17:101077 logging.info('Generating code coverage report in html (this can take a while '
1078 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371079 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1080 binary_paths, profdata_file_path, absolute_filter_paths)
1081 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1082 absolute_filter_paths)
1083 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1084
1085 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1086 per_file_coverage_summary)
1087 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1088 per_file_coverage_summary)
1089 _GenerateDirectoryViewHtmlIndexFile()
1090
1091 component_to_directories = _ExtractComponentToDirectoriesMapping()
1092 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1093 component_to_directories, per_directory_coverage_summary)
1094 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1095 component_to_directories,
1096 per_directory_coverage_summary)
1097 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331098
1099 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371100 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331101 _OverwriteHtmlReportsIndexFile()
1102
Yuke Liao506e8822017-12-04 16:52:541103 html_index_file_path = 'file://' + os.path.abspath(
1104 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101105 logging.info('Index file for html report is generated as: %s',
1106 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541107
Abhishek Arya1ec832c2017-12-05 18:06:591108
Yuke Liao506e8822017-12-04 16:52:541109if __name__ == '__main__':
1110 sys.exit(Main())