blob: f1797b98590fc8408de0615db0221a0f236dbced [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:
484 continue
Yuke Liaoea228d02018-01-05 19:10:33485
Yuke Liaodd1ec0592018-02-02 01:26:37486 html_generator.AddLinkToAnotherReport(entry_html_report_path,
487 os.path.basename(entry_path),
488 entry_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:33489
Yuke Liaod54030e2018-01-08 17:34:12490 html_generator.CreateTotalsEntry(per_directory_coverage_summary[dir_path])
Yuke Liaodd1ec0592018-02-02 01:26:37491 html_generator.WriteHtmlCoverageReport()
492
493
494def _GenerateDirectoryViewHtmlIndexFile():
495 """Generates the html index file for directory view.
496
497 Note that the index file is already generated under SRC_ROOT_PATH, so this
498 file simply redirects to it, and the reason of this extra layer is for
499 structural consistency with other views.
500 """
501 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
502 DIRECTORY_VIEW_INDEX_FILE)
503 logging.debug('Generating directory view html index file as: "%s".',
504 directory_view_index_file_path)
505 src_root_html_report_path = _GetCoverageHtmlReportPathForDirectory(
506 SRC_ROOT_PATH)
507 _WriteRedirectHtmlFile(directory_view_index_file_path,
508 src_root_html_report_path)
509 logging.debug('Finished generating directory view html index file.')
510
511
512def _CalculatePerComponentCoverageSummary(component_to_directories,
513 per_directory_coverage_summary):
514 """Calculates per component coverage summary."""
515 logging.debug('Calculating per-component coverage summary')
516 per_component_coverage_summary = defaultdict(lambda: _CoverageSummary())
517
518 for component in component_to_directories:
519 for directory in component_to_directories[component]:
520 absolute_directory_path = os.path.abspath(directory)
521 if absolute_directory_path in per_directory_coverage_summary:
522 per_component_coverage_summary[component].AddSummary(
523 per_directory_coverage_summary[absolute_directory_path])
524
525 logging.debug('Finished calculating per-component coverage summary')
526 return per_component_coverage_summary
527
528
529def _ExtractComponentToDirectoriesMapping():
530 """Returns a mapping from components to directories."""
531 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
532 directory_to_component = component_mappings['dir-to-component']
533
534 component_to_directories = defaultdict(list)
535 for directory in directory_to_component:
536 component = directory_to_component[directory]
537 component_to_directories[component].append(directory)
538
539 return component_to_directories
540
541
542def _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
543 component_to_directories,
544 per_directory_coverage_summary):
545 """Generates per-component coverage reports in html."""
546 logging.debug('Writing per-component coverage html reports.')
547 for component in per_component_coverage_summary:
548 _GenerateCoverageInHtmlForComponent(
549 component, per_component_coverage_summary, component_to_directories,
550 per_directory_coverage_summary)
551
552 logging.debug('Finished writing per-component coverage html reports.')
553
554
555def _GenerateCoverageInHtmlForComponent(
556 component_name, per_component_coverage_summary, component_to_directories,
557 per_directory_coverage_summary):
558 """Generates coverage html report for a component."""
559 component_html_report_path = _GetCoverageHtmlReportPathForComponent(
560 component_name)
561 component_html_report_dirname = os.path.dirname(component_html_report_path)
562 if not os.path.exists(component_html_report_dirname):
563 os.makedirs(component_html_report_dirname)
564
565 html_generator = _CoverageReportHtmlGenerator(component_html_report_path,
566 'Path')
567
568 for dir_path in component_to_directories[component_name]:
569 dir_absolute_path = os.path.abspath(dir_path)
570 if dir_absolute_path not in per_directory_coverage_summary:
571 continue
572
573 html_generator.AddLinkToAnotherReport(
574 _GetCoverageHtmlReportPathForDirectory(dir_path),
575 os.path.relpath(dir_path, SRC_ROOT_PATH),
576 per_directory_coverage_summary[dir_absolute_path])
577
578 html_generator.CreateTotalsEntry(
579 per_component_coverage_summary[component_name])
580 html_generator.WriteHtmlCoverageReport()
581
582
583def _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary):
584 """Generates the html index file for component view."""
585 component_view_index_file_path = os.path.join(OUTPUT_DIR,
586 COMPONENT_VIEW_INDEX_FILE)
587 logging.debug('Generating component view html index file as: "%s".',
588 component_view_index_file_path)
589 html_generator = _CoverageReportHtmlGenerator(component_view_index_file_path,
590 'Component')
591 totals_coverage_summary = _CoverageSummary()
592
593 for component in per_component_coverage_summary:
594 totals_coverage_summary.AddSummary(
595 per_component_coverage_summary[component])
596
597 html_generator.AddLinkToAnotherReport(
598 _GetCoverageHtmlReportPathForComponent(component), component,
599 per_component_coverage_summary[component])
600
601 html_generator.CreateTotalsEntry(totals_coverage_summary)
602 html_generator.WriteHtmlCoverageReport()
603 logging.debug('Generating component view html index file.')
Yuke Liaoea228d02018-01-05 19:10:33604
605
606def _OverwriteHtmlReportsIndexFile():
Yuke Liaodd1ec0592018-02-02 01:26:37607 """Overwrites the root index file to redirect to the default view."""
Yuke Liaoea228d02018-01-05 19:10:33608 html_index_file_path = os.path.join(OUTPUT_DIR,
609 os.extsep.join(['index', 'html']))
Yuke Liaodd1ec0592018-02-02 01:26:37610 directory_view_index_file_path = os.path.join(OUTPUT_DIR,
611 DIRECTORY_VIEW_INDEX_FILE)
612 _WriteRedirectHtmlFile(html_index_file_path, directory_view_index_file_path)
613
614
615def _WriteRedirectHtmlFile(from_html_path, to_html_path):
616 """Writes a html file that redirects to another html file."""
617 to_html_relative_path = _GetRelativePathToDirectoryOfFile(
618 to_html_path, from_html_path)
Yuke Liaoea228d02018-01-05 19:10:33619 content = ("""
620 <!DOCTYPE html>
621 <html>
622 <head>
623 <!-- HTML meta refresh URL redirection -->
624 <meta http-equiv="refresh" content="0; url=%s">
625 </head>
Yuke Liaodd1ec0592018-02-02 01:26:37626 </html>""" % to_html_relative_path)
627 with open(from_html_path, 'w') as f:
Yuke Liaoea228d02018-01-05 19:10:33628 f.write(content)
629
630
Yuke Liaodd1ec0592018-02-02 01:26:37631def _GetCoverageHtmlReportPathForFile(file_path):
632 """Given a file path, returns the corresponding html report path."""
633 assert os.path.isfile(file_path), '"%s" is not a file' % file_path
634 html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
635
636 # '+' is used instead of os.path.join because both of them are absolute paths
637 # and os.path.join ignores the first path.
638 return _GetCoverageReportRootDirPath() + html_report_path
639
640
641def _GetCoverageHtmlReportPathForDirectory(dir_path):
642 """Given a directory path, returns the corresponding html report path."""
643 assert os.path.isdir(dir_path), '"%s" is not a directory' % dir_path
644 html_report_path = os.path.join(
645 os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
646
647 # '+' is used instead of os.path.join because both of them are absolute paths
648 # and os.path.join ignores the first path.
649 return _GetCoverageReportRootDirPath() + html_report_path
650
651
652def _GetCoverageHtmlReportPathForComponent(component_name):
653 """Given a component, returns the corresponding html report path."""
654 component_file_name = component_name.lower().replace('>', '-')
655 html_report_name = os.extsep.join([component_file_name, 'html'])
656 return os.path.join(_GetCoverageReportRootDirPath(), 'components',
657 html_report_name)
658
659
660def _GetCoverageReportRootDirPath():
661 """The root directory that contains all generated coverage html reports."""
662 return os.path.join(os.path.abspath(OUTPUT_DIR), 'coverage')
Yuke Liaoea228d02018-01-05 19:10:33663
664
Yuke Liao506e8822017-12-04 16:52:54665def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
666 """Builds and runs target to generate the coverage profile data.
667
668 Args:
669 targets: A list of targets to build with coverage instrumentation.
670 commands: A list of commands used to run the targets.
671 jobs_count: Number of jobs to run in parallel for building. If None, a
672 default value is derived based on CPUs availability.
673
674 Returns:
675 A relative path to the generated profdata file.
676 """
677 _BuildTargets(targets, jobs_count)
Abhishek Arya1ec832c2017-12-05 18:06:59678 profraw_file_paths = _GetProfileRawDataPathsByExecutingCommands(
679 targets, commands)
Yuke Liao506e8822017-12-04 16:52:54680 profdata_file_path = _CreateCoverageProfileDataFromProfRawData(
681 profraw_file_paths)
682
Yuke Liaod4a9865202018-01-12 23:17:52683 for profraw_file_path in profraw_file_paths:
684 os.remove(profraw_file_path)
685
Yuke Liao506e8822017-12-04 16:52:54686 return profdata_file_path
687
688
689def _BuildTargets(targets, jobs_count):
690 """Builds target with Clang coverage instrumentation.
691
692 This function requires current working directory to be the root of checkout.
693
694 Args:
695 targets: A list of targets to build with coverage instrumentation.
696 jobs_count: Number of jobs to run in parallel for compilation. If None, a
697 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54698 """
Abhishek Arya1ec832c2017-12-05 18:06:59699
Yuke Liao506e8822017-12-04 16:52:54700 def _IsGomaConfigured():
701 """Returns True if goma is enabled in the gn build args.
702
703 Returns:
704 A boolean indicates whether goma is configured for building or not.
705 """
706 build_args = _ParseArgsGnFile()
707 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
708
Yuke Liao481d3482018-01-29 19:17:10709 logging.info('Building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54710 if jobs_count is None and _IsGomaConfigured():
711 jobs_count = DEFAULT_GOMA_JOBS
712
713 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
714 if jobs_count is not None:
715 subprocess_cmd.append('-j' + str(jobs_count))
716
717 subprocess_cmd.extend(targets)
718 subprocess.check_call(subprocess_cmd)
Yuke Liao481d3482018-01-29 19:17:10719 logging.debug('Finished building %s', str(targets))
Yuke Liao506e8822017-12-04 16:52:54720
721
722def _GetProfileRawDataPathsByExecutingCommands(targets, commands):
723 """Runs commands and returns the relative paths to the profraw data files.
724
725 Args:
726 targets: A list of targets built with coverage instrumentation.
727 commands: A list of commands used to run the targets.
728
729 Returns:
730 A list of relative paths to the generated profraw data files.
731 """
Yuke Liao481d3482018-01-29 19:17:10732 logging.debug('Executing the test commands')
733
Yuke Liao506e8822017-12-04 16:52:54734 # Remove existing profraw data files.
735 for file_or_dir in os.listdir(OUTPUT_DIR):
736 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
737 os.remove(os.path.join(OUTPUT_DIR, file_or_dir))
738
Yuke Liaod4a9865202018-01-12 23:17:52739 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54740 for target, command in zip(targets, commands):
Yuke Liaod4a9865202018-01-12 23:17:52741 _ExecuteCommand(target, command)
Yuke Liao506e8822017-12-04 16:52:54742
Yuke Liao481d3482018-01-29 19:17:10743 logging.debug('Finished executing the test commands')
744
Yuke Liao506e8822017-12-04 16:52:54745 profraw_file_paths = []
746 for file_or_dir in os.listdir(OUTPUT_DIR):
747 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
748 profraw_file_paths.append(os.path.join(OUTPUT_DIR, file_or_dir))
749
750 # Assert one target/command generates at least one profraw data file.
751 for target in targets:
Abhishek Arya1ec832c2017-12-05 18:06:59752 assert any(
753 os.path.basename(profraw_file).startswith(target)
754 for profraw_file in profraw_file_paths), (
755 'Running target: %s failed to generate any profraw data file, '
756 'please make sure the binary exists and is properly instrumented.' %
757 target)
Yuke Liao506e8822017-12-04 16:52:54758
759 return profraw_file_paths
760
761
762def _ExecuteCommand(target, command):
763 """Runs a single command and generates a profraw data file.
764
765 Args:
766 target: A target built with coverage instrumentation.
767 command: A command used to run the target.
768 """
Yuke Liaod4a9865202018-01-12 23:17:52769 # Per Clang "Source-based Code Coverage" doc:
770 # "%Nm" expands out to the instrumented binary's signature. When this pattern
771 # is specified, the runtime creates a pool of N raw profiles which are used
772 # for on-line profile merging. The runtime takes care of selecting a raw
773 # profile from the pool, locking it, and updating it before the program exits.
774 # If N is not specified (i.e the pattern is "%m"), it's assumed that N = 1.
775 # N must be between 1 and 9. The merge pool specifier can only occur once per
776 # filename pattern.
777 #
778 # 4 is chosen because it creates some level of parallelism, but it's not too
779 # big to consume too much computing resource or disk space.
Abhishek Arya1ec832c2017-12-05 18:06:59780 expected_profraw_file_name = os.extsep.join(
Yuke Liaod4a9865202018-01-12 23:17:52781 [target, '%4m', PROFRAW_FILE_EXTENSION])
Yuke Liao506e8822017-12-04 16:52:54782 expected_profraw_file_path = os.path.join(OUTPUT_DIR,
783 expected_profraw_file_name)
784 output_file_name = os.extsep.join([target + '_output', 'txt'])
785 output_file_path = os.path.join(OUTPUT_DIR, output_file_name)
786
Yuke Liao481d3482018-01-29 19:17:10787 logging.info('Running command: "%s", the output is redirected to "%s"',
788 command, output_file_path)
Abhishek Arya1ec832c2017-12-05 18:06:59789 output = subprocess.check_output(
Yuke Liaodd1ec0592018-02-02 01:26:37790 command.split(), env={'LLVM_PROFILE_FILE': expected_profraw_file_path})
Yuke Liao506e8822017-12-04 16:52:54791 with open(output_file_path, 'w') as output_file:
792 output_file.write(output)
793
794
795def _CreateCoverageProfileDataFromProfRawData(profraw_file_paths):
796 """Returns a relative path to the profdata file by merging profraw data files.
797
798 Args:
799 profraw_file_paths: A list of relative paths to the profraw data files that
800 are to be merged.
801
802 Returns:
803 A relative path to the generated profdata file.
804
805 Raises:
806 CalledProcessError: An error occurred merging profraw data files.
807 """
Yuke Liao481d3482018-01-29 19:17:10808 logging.info('Creating the coverage profile data file')
809 logging.debug('Merging profraw files to create profdata file')
Yuke Liao506e8822017-12-04 16:52:54810 profdata_file_path = os.path.join(OUTPUT_DIR, PROFDATA_FILE_NAME)
Yuke Liao506e8822017-12-04 16:52:54811 try:
Abhishek Arya1ec832c2017-12-05 18:06:59812 subprocess_cmd = [
813 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
814 ]
Yuke Liao506e8822017-12-04 16:52:54815 subprocess_cmd.extend(profraw_file_paths)
816 subprocess.check_call(subprocess_cmd)
817 except subprocess.CalledProcessError as error:
818 print('Failed to merge profraw files to create profdata file')
819 raise error
820
Yuke Liao481d3482018-01-29 19:17:10821 logging.debug('Finished merging profraw files')
822 logging.info('Code coverage profile data is created as: %s',
823 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54824 return profdata_file_path
825
826
Yuke Liaoea228d02018-01-05 19:10:33827def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters):
828 """Generates per file coverage summary using "llvm-cov export" command."""
829 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
830 # [[-object BIN]] [SOURCES].
831 # NOTE: For object files, the first one is specified as a positional argument,
832 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10833 logging.debug('Generating per-file code coverage summary using "llvm-cov '
834 'export -summary-only" command')
Yuke Liaoea228d02018-01-05 19:10:33835 subprocess_cmd = [
836 LLVM_COV_PATH, 'export', '-summary-only',
837 '-instr-profile=' + profdata_file_path, binary_paths[0]
838 ]
839 subprocess_cmd.extend(
840 ['-object=' + binary_path for binary_path in binary_paths[1:]])
841 subprocess_cmd.extend(filters)
842
843 json_output = json.loads(subprocess.check_output(subprocess_cmd))
844 assert len(json_output['data']) == 1
845 files_coverage_data = json_output['data'][0]['files']
846
847 per_file_coverage_summary = {}
848 for file_coverage_data in files_coverage_data:
849 file_path = file_coverage_data['filename']
850 summary = file_coverage_data['summary']
851
852 # TODO(crbug.com/797345): Currently, [SOURCES] parameter doesn't apply to
853 # llvm-cov export command, so work it around by manually filter the paths.
854 # Remove this logic once the bug is fixed and clang has rolled past it.
855 if filters and not any(
856 os.path.abspath(file_path).startswith(os.path.abspath(filter))
857 for filter in filters):
858 continue
859
860 if summary['lines']['count'] == 0:
861 continue
862
863 per_file_coverage_summary[file_path] = _CoverageSummary(
864 regions_total=summary['regions']['count'],
865 regions_covered=summary['regions']['covered'],
866 functions_total=summary['functions']['count'],
867 functions_covered=summary['functions']['covered'],
868 lines_total=summary['lines']['count'],
869 lines_covered=summary['lines']['covered'])
870
Yuke Liao481d3482018-01-29 19:17:10871 logging.debug('Finished generating per-file code coverage summary')
Yuke Liaoea228d02018-01-05 19:10:33872 return per_file_coverage_summary
873
874
Yuke Liao506e8822017-12-04 16:52:54875def _GetBinaryPath(command):
876 """Returns a relative path to the binary to be run by the command.
877
878 Args:
879 command: A command used to run a target.
880
881 Returns:
882 A relative path to the binary.
883 """
884 return command.split()[0]
885
886
Yuke Liao95d13d72017-12-07 18:18:50887def _VerifyTargetExecutablesAreInBuildDirectory(commands):
888 """Verifies that the target executables specified in the commands are inside
889 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:54890 for command in commands:
891 binary_path = _GetBinaryPath(command)
Yuke Liao95d13d72017-12-07 18:18:50892 binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
893 assert binary_absolute_path.startswith(os.path.abspath(BUILD_DIR)), (
894 'Target executable "%s" in command: "%s" is outside of '
895 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:54896
897
898def _ValidateBuildingWithClangCoverage():
899 """Asserts that targets are built with Clang coverage enabled."""
900 build_args = _ParseArgsGnFile()
901
902 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
903 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:59904 assert False, ('\'{} = true\' is required in args.gn.'
905 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:54906
907
908def _ParseArgsGnFile():
909 """Parses args.gn file and returns results as a dictionary.
910
911 Returns:
912 A dictionary representing the build args.
913 """
914 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
915 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
916 'missing args.gn file.' % BUILD_DIR)
917 with open(build_args_path) as build_args_file:
918 build_args_lines = build_args_file.readlines()
919
920 build_args = {}
921 for build_arg_line in build_args_lines:
922 build_arg_without_comments = build_arg_line.split('#')[0]
923 key_value_pair = build_arg_without_comments.split('=')
924 if len(key_value_pair) != 2:
925 continue
926
927 key = key_value_pair[0].strip()
928 value = key_value_pair[1].strip()
929 build_args[key] = value
930
931 return build_args
932
933
Abhishek Arya16f059a2017-12-07 17:47:32934def _VerifyPathsAndReturnAbsolutes(paths):
935 """Verifies that the paths specified in |paths| exist and returns absolute
936 versions.
Yuke Liao66da1732017-12-05 22:19:42937
938 Args:
939 paths: A list of files or directories.
940 """
Abhishek Arya16f059a2017-12-07 17:47:32941 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:42942 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:32943 absolute_path = os.path.join(SRC_ROOT_PATH, path)
944 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
945
946 absolute_paths.append(absolute_path)
947
948 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:42949
950
Yuke Liaodd1ec0592018-02-02 01:26:37951def _GetRelativePathToDirectoryOfFile(target_path, base_path):
952 """Returns a target path relative to the directory of base_path.
953
954 This method requires base_path to be a file, otherwise, one should call
955 os.path.relpath directly.
956 """
957 assert os.path.dirname(base_path) != base_path, (
958 'Base path: "%s" is a directly, please call os.path.relpath directly.' %
959 base_path)
960 base_dirname = os.path.dirname(base_path)
961 return os.path.relpath(target_path, base_dirname)
962
963
Yuke Liao506e8822017-12-04 16:52:54964def _ParseCommandArguments():
965 """Adds and parses relevant arguments for tool comands.
966
967 Returns:
968 A dictionary representing the arguments.
969 """
970 arg_parser = argparse.ArgumentParser()
971 arg_parser.usage = __doc__
972
Abhishek Arya1ec832c2017-12-05 18:06:59973 arg_parser.add_argument(
974 '-b',
975 '--build-dir',
976 type=str,
977 required=True,
978 help='The build directory, the path needs to be relative to the root of '
979 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:54980
Abhishek Arya1ec832c2017-12-05 18:06:59981 arg_parser.add_argument(
982 '-o',
983 '--output-dir',
984 type=str,
985 required=True,
986 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:54987
Abhishek Arya1ec832c2017-12-05 18:06:59988 arg_parser.add_argument(
989 '-c',
990 '--command',
991 action='append',
992 required=True,
993 help='Commands used to run test targets, one test target needs one and '
994 'only one command, when specifying commands, one should assume the '
995 'current working directory is the root of the checkout.')
Yuke Liao506e8822017-12-04 16:52:54996
Abhishek Arya1ec832c2017-12-05 18:06:59997 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:42998 '-f',
999 '--filters',
1000 action='append',
Abhishek Arya16f059a2017-12-07 17:47:321001 required=False,
Yuke Liao66da1732017-12-05 22:19:421002 help='Directories or files to get code coverage for, and all files under '
1003 'the directories are included recursively.')
1004
1005 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591006 '-j',
1007 '--jobs',
1008 type=int,
1009 default=None,
1010 help='Run N jobs to build in parallel. If not specified, a default value '
1011 'will be derived based on CPUs availability. Please refer to '
1012 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:541013
Abhishek Arya1ec832c2017-12-05 18:06:591014 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:101015 '-v',
1016 '--verbose',
1017 action='store_true',
1018 help='Prints additional output for diagnostics.')
1019
1020 arg_parser.add_argument(
1021 '-l', '--log_file', type=str, help='Redirects logs to a file.')
1022
1023 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:591024 'targets', nargs='+', help='The names of the test targets to run.')
Yuke Liao506e8822017-12-04 16:52:541025
1026 args = arg_parser.parse_args()
1027 return args
1028
1029
1030def Main():
1031 """Execute tool commands."""
Yuke Liaodd1ec0592018-02-02 01:26:371032 assert _GetPlatform() in [
1033 'linux', 'mac'
1034 ], ('Coverage is only supported on linux and mac platforms.')
Yuke Liao506e8822017-12-04 16:52:541035 assert os.path.abspath(os.getcwd()) == SRC_ROOT_PATH, ('This script must be '
1036 'called from the root '
Abhishek Arya1ec832c2017-12-05 18:06:591037 'of checkout.')
Yuke Liao506e8822017-12-04 16:52:541038 DownloadCoverageToolsIfNeeded()
1039
1040 args = _ParseCommandArguments()
1041 global BUILD_DIR
1042 BUILD_DIR = args.build_dir
1043 global OUTPUT_DIR
1044 OUTPUT_DIR = args.output_dir
1045
Yuke Liao481d3482018-01-29 19:17:101046 log_level = logging.DEBUG if args.verbose else logging.INFO
1047 log_format = '[%(asctime)s] %(message)s'
1048 log_file = args.log_file if args.log_file else None
1049 logging.basicConfig(filename=log_file, level=log_level, format=log_format)
1050
Yuke Liao506e8822017-12-04 16:52:541051 assert len(args.targets) == len(args.command), ('Number of targets must be '
1052 'equal to the number of test '
1053 'commands.')
Abhishek Arya1ec832c2017-12-05 18:06:591054 assert os.path.exists(BUILD_DIR), (
1055 'Build directory: {} doesn\'t exist. '
1056 'Please run "gn gen" to generate.').format(BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:541057 _ValidateBuildingWithClangCoverage()
Yuke Liao95d13d72017-12-07 18:18:501058 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
Abhishek Arya16f059a2017-12-07 17:47:321059
1060 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421061 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321062 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421063
Yuke Liao506e8822017-12-04 16:52:541064 if not os.path.exists(OUTPUT_DIR):
1065 os.makedirs(OUTPUT_DIR)
1066
Abhishek Arya1ec832c2017-12-05 18:06:591067 profdata_file_path = _CreateCoverageProfileDataForTargets(
1068 args.targets, args.command, args.jobs)
Yuke Liao506e8822017-12-04 16:52:541069 binary_paths = [_GetBinaryPath(command) for command in args.command]
Yuke Liaoea228d02018-01-05 19:10:331070
Yuke Liao481d3482018-01-29 19:17:101071 logging.info('Generating code coverage report in html (this can take a while '
1072 'depending on size of target!)')
Yuke Liaodd1ec0592018-02-02 01:26:371073 per_file_coverage_summary = _GeneratePerFileCoverageSummary(
1074 binary_paths, profdata_file_path, absolute_filter_paths)
1075 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
1076 absolute_filter_paths)
1077 _GenerateFileViewHtmlIndexFile(per_file_coverage_summary)
1078
1079 per_directory_coverage_summary = _CalculatePerDirectoryCoverageSummary(
1080 per_file_coverage_summary)
1081 _GeneratePerDirectoryCoverageInHtml(per_directory_coverage_summary,
1082 per_file_coverage_summary)
1083 _GenerateDirectoryViewHtmlIndexFile()
1084
1085 component_to_directories = _ExtractComponentToDirectoriesMapping()
1086 per_component_coverage_summary = _CalculatePerComponentCoverageSummary(
1087 component_to_directories, per_directory_coverage_summary)
1088 _GeneratePerComponentCoverageInHtml(per_component_coverage_summary,
1089 component_to_directories,
1090 per_directory_coverage_summary)
1091 _GenerateComponentViewHtmlIndexFile(per_component_coverage_summary)
Yuke Liaoea228d02018-01-05 19:10:331092
1093 # The default index file is generated only for the list of source files, needs
Yuke Liaodd1ec0592018-02-02 01:26:371094 # to overwrite it to display per directory coverage view by default.
Yuke Liaoea228d02018-01-05 19:10:331095 _OverwriteHtmlReportsIndexFile()
1096
Yuke Liao506e8822017-12-04 16:52:541097 html_index_file_path = 'file://' + os.path.abspath(
1098 os.path.join(OUTPUT_DIR, 'index.html'))
Yuke Liao481d3482018-01-29 19:17:101099 logging.info('Index file for html report is generated as: %s',
1100 html_index_file_path)
Yuke Liao506e8822017-12-04 16:52:541101
Abhishek Arya1ec832c2017-12-05 18:06:591102
Yuke Liao506e8822017-12-04 16:52:541103if __name__ == '__main__':
1104 sys.exit(Main())