blob: 0ad4f5e7c8079e75e0d24d8ae64629359c75ddd3 [file] [log] [blame]
Yuke Liao506e8822017-12-04 16:52:541#!/usr/bin/python
2# Copyright 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Abhishek Arya1ec832c2017-12-05 18:06:595"""This script helps to generate code coverage report.
Yuke Liao506e8822017-12-04 16:52:546
Abhishek Arya1ec832c2017-12-05 18:06:597 It uses Clang Source-based Code Coverage -
8 https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
Yuke Liao506e8822017-12-04 16:52:549
Abhishek Arya16f059a2017-12-07 17:47:3210 In order to generate code coverage report, you need to first add
Yuke Liaoab9c44e2018-02-21 00:24:4011 "use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
12 file in your build output directory (e.g. out/coverage).
Yuke Liao506e8822017-12-04 16:52:5413
Abhishek Arya03911092018-05-21 16:42:3514 * Example usage:
Abhishek Arya1ec832c2017-12-05 18:06:5915
Max Moroza5a95272018-08-31 16:20:5516 gn gen out/coverage \\
17 --args='use_clang_coverage=true is_component_build=false \\
18 dcheck_always_on=true'
Abhishek Arya16f059a2017-12-07 17:47:3219 gclient runhooks
Abhishek Arya1ec832c2017-12-05 18:06:5920 python tools/code_coverage/coverage.py crypto_unittests url_unittests \\
Abhishek Arya16f059a2017-12-07 17:47:3221 -b out/coverage -o out/report -c 'out/coverage/crypto_unittests' \\
22 -c 'out/coverage/url_unittests --gtest_filter=URLParser.PathURL' \\
23 -f url/ -f crypto/
Abhishek Arya1ec832c2017-12-05 18:06:5924
Abhishek Arya16f059a2017-12-07 17:47:3225 The command above builds crypto_unittests and url_unittests targets and then
26 runs them with specified command line arguments. For url_unittests, it only
27 runs the test URLParser.PathURL. The coverage report is filtered to include
28 only files and sub-directories under url/ and crypto/ directories.
Abhishek Arya1ec832c2017-12-05 18:06:5929
Yuke Liao545db322018-02-15 17:12:0130 If you want to run tests that try to draw to the screen but don't have a
31 display connected, you can run tests in headless mode with xvfb.
32
Abhishek Arya03911092018-05-21 16:42:3533 * Sample flow for running a test target with xvfb (e.g. unit_tests):
Yuke Liao545db322018-02-15 17:12:0134
35 python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
36 -o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
37
Abhishek Arya1ec832c2017-12-05 18:06:5938 If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
39 flag as well.
40
Abhishek Arya03911092018-05-21 16:42:3541 * Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
Abhishek Arya1ec832c2017-12-05 18:06:5942
Abhishek Arya16f059a2017-12-07 17:47:3243 python tools/code_coverage/coverage.py pdfium_fuzzer \\
44 -b out/coverage -o out/report \\
45 -c 'out/coverage/pdfium_fuzzer -runs=<runs> <corpus_dir>' \\
46 -f third_party/pdfium
Abhishek Arya1ec832c2017-12-05 18:06:5947
48 where:
49 <corpus_dir> - directory containing samples files for this format.
50 <runs> - number of times to fuzz target function. Should be 0 when you just
51 want to see the coverage on corpus and don't want to fuzz at all.
52
Abhishek Arya03911092018-05-21 16:42:3553 * Sample workflow for running Blink web tests:
54
55 python tools/code_coverage/coverage.py blink_tests \\
56 -wt -b out/coverage -o out/report -f third_party/blink
57
58 If you need to pass arguments to run_web_tests.py, use
59 -wt='arguments to run_web_tests.py e.g. test directories'
60
Abhishek Arya4a9494f2018-05-22 01:39:4161 Note: Generating coverage over entire suite can take minimum of 3 hours due to
62 --batch-size=1 argument added by default. This is needed since otherwise any
63 crash will cause us to lose coverage from prior successful test runs.
64
Abhishek Arya1ec832c2017-12-05 18:06:5965 For more options, please refer to tools/code_coverage/coverage.py -h.
Yuke Liao8e209fe82018-04-18 20:36:3866
67 For an overview of how code coverage works in Chromium, please refer to
68 https://chromium.googlesource.com/chromium/src/+/master/docs/code_coverage.md
Yuke Liao506e8822017-12-04 16:52:5469"""
70
71from __future__ import print_function
72
73import sys
74
75import argparse
Yuke Liaoea228d02018-01-05 19:10:3376import json
Yuke Liao481d3482018-01-29 19:17:1077import logging
Abhishek Arya03911092018-05-21 16:42:3578import multiprocessing
Yuke Liao506e8822017-12-04 16:52:5479import os
Yuke Liaob2926832018-03-02 17:34:2980import re
81import shlex
Max Moroz025d8952018-05-03 16:33:3482import shutil
Yuke Liao506e8822017-12-04 16:52:5483import subprocess
Yuke Liao506e8822017-12-04 16:52:5484import urllib2
85
Abhishek Arya1ec832c2017-12-05 18:06:5986sys.path.append(
87 os.path.join(
88 os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'tools',
89 'clang', 'scripts'))
Yuke Liao506e8822017-12-04 16:52:5490import update as clang_update
91
Yuke Liaoea228d02018-01-05 19:10:3392sys.path.append(
93 os.path.join(
94 os.path.dirname(__file__), os.path.pardir, os.path.pardir,
95 'third_party'))
Yuke Liaoea228d02018-01-05 19:10:3396from collections import defaultdict
97
Max Moroz1de68d72018-08-21 13:38:1898import coverage_utils
99
Yuke Liao082e99632018-05-18 15:40:40100# Absolute path to the code coverage tools binary. These paths can be
101# overwritten by user specified coverage tool paths.
Yuke Liao506e8822017-12-04 16:52:54102LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
Abhishek Arya1c97ea542018-05-10 03:53:19103LLVM_BIN_DIR = os.path.join(LLVM_BUILD_DIR, 'bin')
104LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
105LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
Yuke Liao506e8822017-12-04 16:52:54106
Abhishek Arya03911092018-05-21 16:42:35107# Absolute path to the root of the checkout.
108SRC_ROOT_PATH = None
109
Yuke Liao506e8822017-12-04 16:52:54110# Build directory, the value is parsed from command line arguments.
111BUILD_DIR = None
112
113# Output directory for generated artifacts, the value is parsed from command
114# line arguemnts.
115OUTPUT_DIR = None
116
117# Default number of jobs used to build when goma is configured and enabled.
118DEFAULT_GOMA_JOBS = 100
119
120# Name of the file extension for profraw data files.
121PROFRAW_FILE_EXTENSION = 'profraw'
122
123# Name of the final profdata file, and this file needs to be passed to
124# "llvm-cov" command in order to call "llvm-cov show" to inspect the
125# line-by-line coverage of specific files.
Max Moroz7c5354f2018-05-06 00:03:48126PROFDATA_FILE_NAME = os.extsep.join(['coverage', 'profdata'])
127
128# Name of the file with summary information generated by llvm-cov export.
129SUMMARY_FILE_NAME = os.extsep.join(['summary', 'json'])
Yuke Liao506e8822017-12-04 16:52:54130
131# Build arg required for generating code coverage data.
132CLANG_COVERAGE_BUILD_ARG = 'use_clang_coverage'
133
Max Moroz7c5354f2018-05-06 00:03:48134LOGS_DIR_NAME = 'logs'
Yuke Liaodd1ec0592018-02-02 01:26:37135
136# Used to extract a mapping between directories and components.
Abhishek Arya1c97ea542018-05-10 03:53:19137COMPONENT_MAPPING_URL = (
138 'https://storage.googleapis.com/chromium-owners/component_map.json')
Yuke Liaodd1ec0592018-02-02 01:26:37139
Yuke Liao80afff32018-03-07 01:26:20140# Caches the results returned by _GetBuildArgs, don't use this variable
141# directly, call _GetBuildArgs instead.
142_BUILD_ARGS = None
143
Abhishek Aryac19bc5ef2018-05-04 22:10:02144# Retry failed merges.
145MERGE_RETRIES = 3
146
Abhishek Aryad35de7e2018-05-10 22:23:04147# Message to guide user to file a bug when everything else fails.
148FILE_BUG_MESSAGE = (
149 'If it persists, please file a bug with the command you used, git revision '
150 'and args.gn config here: '
151 'https://bugs.chromium.org/p/chromium/issues/entry?'
152 'components=Tools%3ECodeCoverage')
153
Abhishek Aryabd0655d2018-05-21 19:55:24154# String to replace with actual llvm profile path.
155LLVM_PROFILE_FILE_PATH_SUBSTITUTION = '<llvm_profile_file_path>'
156
Yuke Liaoea228d02018-01-05 19:10:33157
Yuke Liao082e99632018-05-18 15:40:40158def _ConfigureLLVMCoverageTools(args):
159 """Configures llvm coverage tools."""
160 if args.coverage_tools_dir:
Max Moroz1de68d72018-08-21 13:38:18161 llvm_bin_dir = coverage_utils.GetFullPath(args.coverage_tools_dir)
Yuke Liao082e99632018-05-18 15:40:40162 global LLVM_COV_PATH
163 global LLVM_PROFDATA_PATH
164 LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
165 LLVM_PROFDATA_PATH = os.path.join(llvm_bin_dir, 'llvm-profdata')
166 else:
167 DownloadCoverageToolsIfNeeded()
168
169 coverage_tools_exist = (
170 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
171 assert coverage_tools_exist, ('Cannot find coverage tools, please make sure '
172 'both \'%s\' and \'%s\' exist.') % (
173 LLVM_COV_PATH, LLVM_PROFDATA_PATH)
174
175
Abhishek Arya1c97ea542018-05-10 03:53:19176def _GetPathWithLLVMSymbolizerDir():
177 """Add llvm-symbolizer directory to path for symbolized stacks."""
178 path = os.getenv('PATH')
179 dirs = path.split(os.pathsep)
180 if LLVM_BIN_DIR in dirs:
181 return path
182
183 return path + os.pathsep + LLVM_BIN_DIR
184
185
Yuke Liaoc60b2d02018-03-02 21:40:43186def _GetTargetOS():
187 """Returns the target os specified in args.gn file.
188
189 Returns an empty string is target_os is not specified.
190 """
Yuke Liao80afff32018-03-07 01:26:20191 build_args = _GetBuildArgs()
Yuke Liaoc60b2d02018-03-02 21:40:43192 return build_args['target_os'] if 'target_os' in build_args else ''
193
194
Yuke Liaob2926832018-03-02 17:34:29195def _IsIOS():
Yuke Liaoa0c8c2f2018-02-28 20:14:10196 """Returns true if the target_os specified in args.gn file is ios"""
Yuke Liaoc60b2d02018-03-02 21:40:43197 return _GetTargetOS() == 'ios'
Yuke Liaoa0c8c2f2018-02-28 20:14:10198
199
Yuke Liao506e8822017-12-04 16:52:54200# TODO(crbug.com/759794): remove this function once tools get included to
201# Clang bundle:
202# https://chromium-review.googlesource.com/c/chromium/src/+/688221
203def DownloadCoverageToolsIfNeeded():
204 """Temporary solution to download llvm-profdata and llvm-cov tools."""
Abhishek Arya1ec832c2017-12-05 18:06:59205
Yuke Liaoc60b2d02018-03-02 21:40:43206 def _GetRevisionFromStampFile(stamp_file_path):
Yuke Liao506e8822017-12-04 16:52:54207 """Returns a pair of revision number by reading the build stamp file.
208
209 Args:
210 stamp_file_path: A path the build stamp file created by
211 tools/clang/scripts/update.py.
212 Returns:
213 A pair of integers represeting the main and sub revision respectively.
214 """
215 if not os.path.exists(stamp_file_path):
216 return 0, 0
217
218 with open(stamp_file_path) as stamp_file:
Yuke Liaoc60b2d02018-03-02 21:40:43219 stamp_file_line = stamp_file.readline()
220 if ',' in stamp_file_line:
221 package_version = stamp_file_line.rstrip().split(',')[0]
222 else:
223 package_version = stamp_file_line.rstrip()
Yuke Liao506e8822017-12-04 16:52:54224
Yuke Liaoc60b2d02018-03-02 21:40:43225 clang_revision_str, clang_sub_revision_str = package_version.split('-')
226 return int(clang_revision_str), int(clang_sub_revision_str)
Abhishek Arya1ec832c2017-12-05 18:06:59227
Max Moroz1de68d72018-08-21 13:38:18228 host_platform = coverage_utils.GetHostPlatform()
Yuke Liao506e8822017-12-04 16:52:54229 clang_revision, clang_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43230 clang_update.STAMP_FILE)
Yuke Liao506e8822017-12-04 16:52:54231
232 coverage_revision_stamp_file = os.path.join(
233 os.path.dirname(clang_update.STAMP_FILE), 'cr_coverage_revision')
234 coverage_revision, coverage_sub_revision = _GetRevisionFromStampFile(
Yuke Liaoc60b2d02018-03-02 21:40:43235 coverage_revision_stamp_file)
Yuke Liao506e8822017-12-04 16:52:54236
Yuke Liaoea228d02018-01-05 19:10:33237 has_coverage_tools = (
238 os.path.exists(LLVM_COV_PATH) and os.path.exists(LLVM_PROFDATA_PATH))
Abhishek Arya16f059a2017-12-07 17:47:32239
Yuke Liaoea228d02018-01-05 19:10:33240 if (has_coverage_tools and coverage_revision == clang_revision and
Yuke Liao506e8822017-12-04 16:52:54241 coverage_sub_revision == clang_sub_revision):
242 # LLVM coverage tools are up to date, bail out.
Yuke Liaoc60b2d02018-03-02 21:40:43243 return
Yuke Liao506e8822017-12-04 16:52:54244
245 package_version = '%d-%d' % (clang_revision, clang_sub_revision)
246 coverage_tools_file = 'llvm-code-coverage-%s.tgz' % package_version
247
248 # The code bellow follows the code from tools/clang/scripts/update.py.
Yuke Liaoc60b2d02018-03-02 21:40:43249 if host_platform == 'mac':
Yuke Liao506e8822017-12-04 16:52:54250 coverage_tools_url = clang_update.CDS_URL + '/Mac/' + coverage_tools_file
Yuke Liaoc60b2d02018-03-02 21:40:43251 elif host_platform == 'linux':
Yuke Liao506e8822017-12-04 16:52:54252 coverage_tools_url = (
253 clang_update.CDS_URL + '/Linux_x64/' + coverage_tools_file)
Yuke Liaoc60b2d02018-03-02 21:40:43254 else:
255 assert host_platform == 'win'
256 coverage_tools_url = (clang_update.CDS_URL + '/Win/' + coverage_tools_file)
Yuke Liao506e8822017-12-04 16:52:54257
258 try:
259 clang_update.DownloadAndUnpack(coverage_tools_url,
260 clang_update.LLVM_BUILD_DIR)
Yuke Liao506e8822017-12-04 16:52:54261 with open(coverage_revision_stamp_file, 'w') as file_handle:
Yuke Liaoc60b2d02018-03-02 21:40:43262 file_handle.write('%s,%s' % (package_version, host_platform))
Yuke Liao506e8822017-12-04 16:52:54263 file_handle.write('\n')
264 except urllib2.URLError:
265 raise Exception(
266 'Failed to download coverage tools: %s.' % coverage_tools_url)
267
268
Yuke Liaodd1ec0592018-02-02 01:26:37269def _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:59270 filters, ignore_filename_regex):
Yuke Liao506e8822017-12-04 16:52:54271 """Generates per file line-by-line coverage in html using 'llvm-cov show'.
272
273 For a file with absolute path /a/b/x.cc, a html report is generated as:
274 OUTPUT_DIR/coverage/a/b/x.cc.html. An index html file is also generated as:
275 OUTPUT_DIR/index.html.
276
277 Args:
278 binary_paths: A list of paths to the instrumented binaries.
279 profdata_file_path: A path to the profdata file.
Yuke Liao66da1732017-12-05 22:19:42280 filters: A list of directories and files to get coverage for.
Yuke Liao506e8822017-12-04 16:52:54281 """
Yuke Liao506e8822017-12-04 16:52:54282 # llvm-cov show [options] -instr-profile PROFILE BIN [-object BIN,...]
283 # [[-object BIN]] [SOURCES]
284 # NOTE: For object files, the first one is specified as a positional argument,
285 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10286 logging.debug('Generating per file line by line coverage reports using '
Abhishek Aryafb70b532018-05-06 17:47:40287 '"llvm-cov show" command.')
Abhishek Arya1ec832c2017-12-05 18:06:59288 subprocess_cmd = [
289 LLVM_COV_PATH, 'show', '-format=html',
290 '-output-dir={}'.format(OUTPUT_DIR),
291 '-instr-profile={}'.format(profdata_file_path), binary_paths[0]
292 ]
293 subprocess_cmd.extend(
294 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29295 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Max Moroz1de68d72018-08-21 13:38:18296 if coverage_utils.GetHostPlatform() in ['linux', 'mac']:
Ryan Sleeviae19b2c32018-05-15 22:36:17297 subprocess_cmd.extend(['-Xdemangler', 'c++filt', '-Xdemangler', '-n'])
Yuke Liao66da1732017-12-05 22:19:42298 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59299 if ignore_filename_regex:
300 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
301
Yuke Liao506e8822017-12-04 16:52:54302 subprocess.check_call(subprocess_cmd)
Max Moroz025d8952018-05-03 16:33:34303
Abhishek Aryafb70b532018-05-06 17:47:40304 logging.debug('Finished running "llvm-cov show" command.')
Yuke Liao506e8822017-12-04 16:52:54305
306
Max Moroz7c5354f2018-05-06 00:03:48307def _GetLogsDirectoryPath():
308 """Path to the logs directory."""
Max Moroz1de68d72018-08-21 13:38:18309 return os.path.join(
310 coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR), LOGS_DIR_NAME)
Max Moroz7c5354f2018-05-06 00:03:48311
312
313def _GetProfdataFilePath():
314 """Path to the resulting .profdata file."""
Max Moroz1de68d72018-08-21 13:38:18315 return os.path.join(
316 coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
317 PROFDATA_FILE_NAME)
Max Moroz7c5354f2018-05-06 00:03:48318
319
320def _GetSummaryFilePath():
321 """The JSON file that contains coverage summary written by llvm-cov export."""
Max Moroz1de68d72018-08-21 13:38:18322 return os.path.join(
323 coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
324 SUMMARY_FILE_NAME)
Yuke Liaoea228d02018-01-05 19:10:33325
326
Yuke Liao506e8822017-12-04 16:52:54327def _CreateCoverageProfileDataForTargets(targets, commands, jobs_count=None):
328 """Builds and runs target to generate the coverage profile data.
329
330 Args:
331 targets: A list of targets to build with coverage instrumentation.
332 commands: A list of commands used to run the targets.
333 jobs_count: Number of jobs to run in parallel for building. If None, a
334 default value is derived based on CPUs availability.
335
336 Returns:
337 A relative path to the generated profdata file.
338 """
339 _BuildTargets(targets, jobs_count)
Abhishek Aryac19bc5ef2018-05-04 22:10:02340 target_profdata_file_paths = _GetTargetProfDataPathsByExecutingCommands(
Abhishek Arya1ec832c2017-12-05 18:06:59341 targets, commands)
Abhishek Aryac19bc5ef2018-05-04 22:10:02342 coverage_profdata_file_path = (
343 _CreateCoverageProfileDataFromTargetProfDataFiles(
344 target_profdata_file_paths))
Yuke Liao506e8822017-12-04 16:52:54345
Abhishek Aryac19bc5ef2018-05-04 22:10:02346 for target_profdata_file_path in target_profdata_file_paths:
347 os.remove(target_profdata_file_path)
Yuke Liaod4a9865202018-01-12 23:17:52348
Abhishek Aryac19bc5ef2018-05-04 22:10:02349 return coverage_profdata_file_path
Yuke Liao506e8822017-12-04 16:52:54350
351
352def _BuildTargets(targets, jobs_count):
353 """Builds target with Clang coverage instrumentation.
354
355 This function requires current working directory to be the root of checkout.
356
357 Args:
358 targets: A list of targets to build with coverage instrumentation.
359 jobs_count: Number of jobs to run in parallel for compilation. If None, a
360 default value is derived based on CPUs availability.
Yuke Liao506e8822017-12-04 16:52:54361 """
Abhishek Arya1ec832c2017-12-05 18:06:59362
Yuke Liao506e8822017-12-04 16:52:54363 def _IsGomaConfigured():
364 """Returns True if goma is enabled in the gn build args.
365
366 Returns:
367 A boolean indicates whether goma is configured for building or not.
368 """
Yuke Liao80afff32018-03-07 01:26:20369 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54370 return 'use_goma' in build_args and build_args['use_goma'] == 'true'
371
Abhishek Aryafb70b532018-05-06 17:47:40372 logging.info('Building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54373 if jobs_count is None and _IsGomaConfigured():
374 jobs_count = DEFAULT_GOMA_JOBS
375
376 subprocess_cmd = ['ninja', '-C', BUILD_DIR]
377 if jobs_count is not None:
378 subprocess_cmd.append('-j' + str(jobs_count))
379
380 subprocess_cmd.extend(targets)
381 subprocess.check_call(subprocess_cmd)
Abhishek Aryafb70b532018-05-06 17:47:40382 logging.debug('Finished building %s.', str(targets))
Yuke Liao506e8822017-12-04 16:52:54383
384
Abhishek Aryac19bc5ef2018-05-04 22:10:02385def _GetTargetProfDataPathsByExecutingCommands(targets, commands):
Yuke Liao506e8822017-12-04 16:52:54386 """Runs commands and returns the relative paths to the profraw data files.
387
388 Args:
389 targets: A list of targets built with coverage instrumentation.
390 commands: A list of commands used to run the targets.
391
392 Returns:
393 A list of relative paths to the generated profraw data files.
394 """
Abhishek Aryafb70b532018-05-06 17:47:40395 logging.debug('Executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10396
Yuke Liao506e8822017-12-04 16:52:54397 # Remove existing profraw data files.
Max Moroz1de68d72018-08-21 13:38:18398 report_root_dir = coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR)
399 for file_or_dir in os.listdir(report_root_dir):
Yuke Liao506e8822017-12-04 16:52:54400 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz1de68d72018-08-21 13:38:18401 os.remove(os.path.join(report_root_dir, file_or_dir))
Max Moroz7c5354f2018-05-06 00:03:48402
403 # Ensure that logs directory exists.
404 if not os.path.exists(_GetLogsDirectoryPath()):
405 os.makedirs(_GetLogsDirectoryPath())
Yuke Liao506e8822017-12-04 16:52:54406
Abhishek Aryac19bc5ef2018-05-04 22:10:02407 profdata_file_paths = []
Yuke Liaoa0c8c2f2018-02-28 20:14:10408
Yuke Liaod4a9865202018-01-12 23:17:52409 # Run all test targets to generate profraw data files.
Yuke Liao506e8822017-12-04 16:52:54410 for target, command in zip(targets, commands):
Max Moroz7c5354f2018-05-06 00:03:48411 output_file_name = os.extsep.join([target + '_output', 'log'])
412 output_file_path = os.path.join(_GetLogsDirectoryPath(), output_file_name)
Yuke Liaoa0c8c2f2018-02-28 20:14:10413
Abhishek Aryac19bc5ef2018-05-04 22:10:02414 profdata_file_path = None
415 for _ in xrange(MERGE_RETRIES):
Abhishek Aryafb70b532018-05-06 17:47:40416 logging.info('Running command: "%s", the output is redirected to "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02417 command, output_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10418
Abhishek Aryac19bc5ef2018-05-04 22:10:02419 if _IsIOSCommand(command):
420 # On iOS platform, due to lack of write permissions, profraw files are
421 # generated outside of the OUTPUT_DIR, and the exact paths are contained
422 # in the output of the command execution.
Abhishek Arya03911092018-05-21 16:42:35423 output = _ExecuteIOSCommand(command, output_file_path)
Abhishek Aryac19bc5ef2018-05-04 22:10:02424 else:
425 # On other platforms, profraw files are generated inside the OUTPUT_DIR.
Abhishek Arya03911092018-05-21 16:42:35426 output = _ExecuteCommand(target, command, output_file_path)
Abhishek Aryac19bc5ef2018-05-04 22:10:02427
428 profraw_file_paths = []
429 if _IsIOS():
Yuke Liao9c2c70b2018-05-23 15:37:57430 profraw_file_paths = [_GetProfrawDataFileByParsingOutput(output)]
Abhishek Aryac19bc5ef2018-05-04 22:10:02431 else:
Max Moroz1de68d72018-08-21 13:38:18432 for file_or_dir in os.listdir(report_root_dir):
Abhishek Aryac19bc5ef2018-05-04 22:10:02433 if file_or_dir.endswith(PROFRAW_FILE_EXTENSION):
Max Moroz7c5354f2018-05-06 00:03:48434 profraw_file_paths.append(
Max Moroz1de68d72018-08-21 13:38:18435 os.path.join(report_root_dir, file_or_dir))
Abhishek Aryac19bc5ef2018-05-04 22:10:02436
437 assert profraw_file_paths, (
Abhishek Aryafb70b532018-05-06 17:47:40438 'Running target "%s" failed to generate any profraw data file, '
Abhishek Aryad35de7e2018-05-10 22:23:04439 'please make sure the binary exists, is properly instrumented and '
440 'does not crash. %s' % (target, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:02441
Yuke Liao9c2c70b2018-05-23 15:37:57442 assert isinstance(profraw_file_paths, list), (
Max Moroz1de68d72018-08-21 13:38:18443 'Variable \'profraw_file_paths\' is expected to be of type \'list\', '
444 'but it is a %s. %s' % (type(profraw_file_paths), FILE_BUG_MESSAGE))
Yuke Liao9c2c70b2018-05-23 15:37:57445
Abhishek Aryac19bc5ef2018-05-04 22:10:02446 try:
447 profdata_file_path = _CreateTargetProfDataFileFromProfRawFiles(
448 target, profraw_file_paths)
449 break
450 except Exception:
Abhishek Aryad35de7e2018-05-10 22:23:04451 logging.info('Retrying...')
Abhishek Aryac19bc5ef2018-05-04 22:10:02452 finally:
453 # Remove profraw files now so that they are not used in next iteration.
454 for profraw_file_path in profraw_file_paths:
455 os.remove(profraw_file_path)
456
457 assert profdata_file_path, (
Abhishek Aryad35de7e2018-05-10 22:23:04458 'Failed to merge target "%s" profraw files after %d retries. %s' %
459 (target, MERGE_RETRIES, FILE_BUG_MESSAGE))
Abhishek Aryac19bc5ef2018-05-04 22:10:02460 profdata_file_paths.append(profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54461
Abhishek Aryafb70b532018-05-06 17:47:40462 logging.debug('Finished executing the test commands.')
Yuke Liao481d3482018-01-29 19:17:10463
Abhishek Aryac19bc5ef2018-05-04 22:10:02464 return profdata_file_paths
Yuke Liao506e8822017-12-04 16:52:54465
466
Abhishek Arya03911092018-05-21 16:42:35467def _GetEnvironmentVars(profraw_file_path):
468 """Return environment vars for subprocess, given a profraw file path."""
469 env = os.environ.copy()
470 env.update({
471 'LLVM_PROFILE_FILE': profraw_file_path,
472 'PATH': _GetPathWithLLVMSymbolizerDir()
473 })
474 return env
475
476
477def _ExecuteCommand(target, command, output_file_path):
Yuke Liaoa0c8c2f2018-02-28 20:14:10478 """Runs a single command and generates a profraw data file."""
Yuke Liaod4a9865202018-01-12 23:17:52479 # Per Clang "Source-based Code Coverage" doc:
Yuke Liao27349c92018-03-22 21:10:01480 #
Max Morozd73e45f2018-04-24 18:32:47481 # "%p" expands out to the process ID. It's not used by this scripts due to:
482 # 1) If a target program spawns too many processess, it may exhaust all disk
483 # space available. For example, unit_tests writes thousands of .profraw
484 # files each of size 1GB+.
485 # 2) If a target binary uses shared libraries, coverage profile data for them
486 # will be missing, resulting in incomplete coverage reports.
Yuke Liao27349c92018-03-22 21:10:01487 #
Yuke Liaod4a9865202018-01-12 23:17:52488 # "%Nm" expands out to the instrumented binary's signature. When this pattern
489 # is specified, the runtime creates a pool of N raw profiles which are used
490 # for on-line profile merging. The runtime takes care of selecting a raw
491 # profile from the pool, locking it, and updating it before the program exits.
Yuke Liaod4a9865202018-01-12 23:17:52492 # N must be between 1 and 9. The merge pool specifier can only occur once per
493 # filename pattern.
494 #
Max Morozd73e45f2018-04-24 18:32:47495 # "%1m" is used when tests run in single process, such as fuzz targets.
Yuke Liao27349c92018-03-22 21:10:01496 #
Max Morozd73e45f2018-04-24 18:32:47497 # For other cases, "%4m" is chosen as it creates some level of parallelism,
498 # but it's not too big to consume too much computing resource or disk space.
499 profile_pattern_string = '%1m' if _IsFuzzerTarget(target) else '%4m'
Abhishek Arya1ec832c2017-12-05 18:06:59500 expected_profraw_file_name = os.extsep.join(
Yuke Liao27349c92018-03-22 21:10:01501 [target, profile_pattern_string, PROFRAW_FILE_EXTENSION])
Max Moroz1de68d72018-08-21 13:38:18502 expected_profraw_file_path = os.path.join(
503 coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR),
504 expected_profraw_file_name)
Abhishek Aryabd0655d2018-05-21 19:55:24505 command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
506 expected_profraw_file_path)
Yuke Liao506e8822017-12-04 16:52:54507
Yuke Liaoa0c8c2f2018-02-28 20:14:10508 try:
Max Moroz7c5354f2018-05-06 00:03:48509 # Some fuzz targets or tests may write into stderr, redirect it as well.
Abhishek Arya03911092018-05-21 16:42:35510 with open(output_file_path, 'wb') as output_file_handle:
511 subprocess.check_call(
512 shlex.split(command),
513 stdout=output_file_handle,
514 stderr=subprocess.STDOUT,
515 env=_GetEnvironmentVars(expected_profraw_file_path))
Yuke Liaoa0c8c2f2018-02-28 20:14:10516 except subprocess.CalledProcessError as e:
Abhishek Arya03911092018-05-21 16:42:35517 logging.warning('Command: "%s" exited with non-zero return code.', command)
Yuke Liaoa0c8c2f2018-02-28 20:14:10518
Abhishek Arya03911092018-05-21 16:42:35519 return open(output_file_path, 'rb').read()
Yuke Liaoa0c8c2f2018-02-28 20:14:10520
521
Yuke Liao27349c92018-03-22 21:10:01522def _IsFuzzerTarget(target):
523 """Returns true if the target is a fuzzer target."""
524 build_args = _GetBuildArgs()
525 use_libfuzzer = ('use_libfuzzer' in build_args and
526 build_args['use_libfuzzer'] == 'true')
527 return use_libfuzzer and target.endswith('_fuzzer')
528
529
Abhishek Arya03911092018-05-21 16:42:35530def _ExecuteIOSCommand(command, output_file_path):
Yuke Liaoa0c8c2f2018-02-28 20:14:10531 """Runs a single iOS command and generates a profraw data file.
532
533 iOS application doesn't have write access to folders outside of the app, so
534 it's impossible to instruct the app to flush the profraw data file to the
535 desired location. The profraw data file will be generated somewhere within the
536 application's Documents folder, and the full path can be obtained by parsing
537 the output.
538 """
Yuke Liaob2926832018-03-02 17:34:29539 assert _IsIOSCommand(command)
540
541 # After running tests, iossim generates a profraw data file, it won't be
542 # needed anyway, so dump it into the OUTPUT_DIR to avoid polluting the
543 # checkout.
544 iossim_profraw_file_path = os.path.join(
545 OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
Abhishek Aryabd0655d2018-05-21 19:55:24546 command = command.replace(LLVM_PROFILE_FILE_PATH_SUBSTITUTION,
547 iossim_profraw_file_path)
Yuke Liaoa0c8c2f2018-02-28 20:14:10548
549 try:
Abhishek Arya03911092018-05-21 16:42:35550 with open(output_file_path, 'wb') as output_file_handle:
551 subprocess.check_call(
552 shlex.split(command),
553 stdout=output_file_handle,
554 stderr=subprocess.STDOUT,
555 env=_GetEnvironmentVars(iossim_profraw_file_path))
Yuke Liaoa0c8c2f2018-02-28 20:14:10556 except subprocess.CalledProcessError as e:
557 # iossim emits non-zero return code even if tests run successfully, so
558 # ignore the return code.
Abhishek Arya03911092018-05-21 16:42:35559 pass
Yuke Liaoa0c8c2f2018-02-28 20:14:10560
Abhishek Arya03911092018-05-21 16:42:35561 return open(output_file_path, 'rb').read()
Yuke Liaoa0c8c2f2018-02-28 20:14:10562
563
564def _GetProfrawDataFileByParsingOutput(output):
565 """Returns the path to the profraw data file obtained by parsing the output.
566
567 The output of running the test target has no format, but it is guaranteed to
568 have a single line containing the path to the generated profraw data file.
569 NOTE: This should only be called when target os is iOS.
570 """
Yuke Liaob2926832018-03-02 17:34:29571 assert _IsIOS()
Yuke Liaoa0c8c2f2018-02-28 20:14:10572
Yuke Liaob2926832018-03-02 17:34:29573 output_by_lines = ''.join(output).splitlines()
574 profraw_file_pattern = re.compile('.*Coverage data at (.*coverage\.profraw).')
Yuke Liaoa0c8c2f2018-02-28 20:14:10575
576 for line in output_by_lines:
Yuke Liaob2926832018-03-02 17:34:29577 result = profraw_file_pattern.match(line)
578 if result:
579 return result.group(1)
Yuke Liaoa0c8c2f2018-02-28 20:14:10580
581 assert False, ('No profraw data file was generated, did you call '
582 'coverage_util::ConfigureCoverageReportPath() in test setup? '
583 'Please refer to base/test/test_support_ios.mm for example.')
Yuke Liao506e8822017-12-04 16:52:54584
585
Abhishek Aryac19bc5ef2018-05-04 22:10:02586def _CreateCoverageProfileDataFromTargetProfDataFiles(profdata_file_paths):
587 """Returns a relative path to coverage profdata file by merging target
588 profdata files.
Yuke Liao506e8822017-12-04 16:52:54589
590 Args:
Abhishek Aryac19bc5ef2018-05-04 22:10:02591 profdata_file_paths: A list of relative paths to the profdata data files
592 that are to be merged.
Yuke Liao506e8822017-12-04 16:52:54593
594 Returns:
Abhishek Aryac19bc5ef2018-05-04 22:10:02595 A relative path to the merged coverage profdata file.
Yuke Liao506e8822017-12-04 16:52:54596
597 Raises:
Abhishek Aryac19bc5ef2018-05-04 22:10:02598 CalledProcessError: An error occurred merging profdata files.
Yuke Liao506e8822017-12-04 16:52:54599 """
Abhishek Aryafb70b532018-05-06 17:47:40600 logging.info('Creating the coverage profile data file.')
601 logging.debug('Merging target profraw files to create target profdata file.')
Max Moroz7c5354f2018-05-06 00:03:48602 profdata_file_path = _GetProfdataFilePath()
Yuke Liao506e8822017-12-04 16:52:54603 try:
Abhishek Arya1ec832c2017-12-05 18:06:59604 subprocess_cmd = [
605 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
606 ]
Abhishek Aryac19bc5ef2018-05-04 22:10:02607 subprocess_cmd.extend(profdata_file_paths)
Abhishek Aryae5811afa2018-05-24 03:56:01608
609 output = subprocess.check_output(subprocess_cmd)
Max Moroz1de68d72018-08-21 13:38:18610 logging.debug('Merge output: %s', output)
Abhishek Aryac19bc5ef2018-05-04 22:10:02611 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:04612 logging.error(
613 'Failed to merge target profdata files to create coverage profdata. %s',
614 FILE_BUG_MESSAGE)
Abhishek Aryac19bc5ef2018-05-04 22:10:02615 raise error
616
Abhishek Aryafb70b532018-05-06 17:47:40617 logging.debug('Finished merging target profdata files.')
618 logging.info('Code coverage profile data is created as: "%s".',
Abhishek Aryac19bc5ef2018-05-04 22:10:02619 profdata_file_path)
620 return profdata_file_path
621
622
623def _CreateTargetProfDataFileFromProfRawFiles(target, profraw_file_paths):
624 """Returns a relative path to target profdata file by merging target
625 profraw files.
626
627 Args:
628 profraw_file_paths: A list of relative paths to the profdata data files
629 that are to be merged.
630
631 Returns:
632 A relative path to the merged coverage profdata file.
633
634 Raises:
635 CalledProcessError: An error occurred merging profdata files.
636 """
Abhishek Aryafb70b532018-05-06 17:47:40637 logging.info('Creating target profile data file.')
638 logging.debug('Merging target profraw files to create target profdata file.')
Abhishek Aryac19bc5ef2018-05-04 22:10:02639 profdata_file_path = os.path.join(OUTPUT_DIR, '%s.profdata' % target)
640
641 try:
642 subprocess_cmd = [
643 LLVM_PROFDATA_PATH, 'merge', '-o', profdata_file_path, '-sparse=true'
644 ]
Yuke Liao506e8822017-12-04 16:52:54645 subprocess_cmd.extend(profraw_file_paths)
Abhishek Aryae5811afa2018-05-24 03:56:01646
647 output = subprocess.check_output(subprocess_cmd)
Max Moroz1de68d72018-08-21 13:38:18648 logging.debug('Merge output: %s', output)
Yuke Liao506e8822017-12-04 16:52:54649 except subprocess.CalledProcessError as error:
Abhishek Aryad35de7e2018-05-10 22:23:04650 logging.error(
651 'Failed to merge target profraw files to create target profdata.')
Yuke Liao506e8822017-12-04 16:52:54652 raise error
653
Abhishek Aryafb70b532018-05-06 17:47:40654 logging.debug('Finished merging target profraw files.')
655 logging.info('Target "%s" profile data is created as: "%s".', target,
Yuke Liao481d3482018-01-29 19:17:10656 profdata_file_path)
Yuke Liao506e8822017-12-04 16:52:54657 return profdata_file_path
658
659
Yuke Liao0e4c8682018-04-18 21:06:59660def _GeneratePerFileCoverageSummary(binary_paths, profdata_file_path, filters,
661 ignore_filename_regex):
Yuke Liaoea228d02018-01-05 19:10:33662 """Generates per file coverage summary using "llvm-cov export" command."""
663 # llvm-cov export [options] -instr-profile PROFILE BIN [-object BIN,...]
664 # [[-object BIN]] [SOURCES].
665 # NOTE: For object files, the first one is specified as a positional argument,
666 # and the rest are specified as keyword argument.
Yuke Liao481d3482018-01-29 19:17:10667 logging.debug('Generating per-file code coverage summary using "llvm-cov '
Abhishek Aryafb70b532018-05-06 17:47:40668 'export -summary-only" command.')
Yuke Liaoea228d02018-01-05 19:10:33669 subprocess_cmd = [
670 LLVM_COV_PATH, 'export', '-summary-only',
671 '-instr-profile=' + profdata_file_path, binary_paths[0]
672 ]
673 subprocess_cmd.extend(
674 ['-object=' + binary_path for binary_path in binary_paths[1:]])
Yuke Liaob2926832018-03-02 17:34:29675 _AddArchArgumentForIOSIfNeeded(subprocess_cmd, len(binary_paths))
Yuke Liaoea228d02018-01-05 19:10:33676 subprocess_cmd.extend(filters)
Yuke Liao0e4c8682018-04-18 21:06:59677 if ignore_filename_regex:
678 subprocess_cmd.append('-ignore-filename-regex=%s' % ignore_filename_regex)
Yuke Liaoea228d02018-01-05 19:10:33679
Max Moroz7c5354f2018-05-06 00:03:48680 export_output = subprocess.check_output(subprocess_cmd)
681
682 # Write output on the disk to be used by code coverage bot.
683 with open(_GetSummaryFilePath(), 'w') as f:
684 f.write(export_output)
685
Max Moroz1de68d72018-08-21 13:38:18686 return export_output
Yuke Liaoea228d02018-01-05 19:10:33687
688
Yuke Liaob2926832018-03-02 17:34:29689def _AddArchArgumentForIOSIfNeeded(cmd_list, num_archs):
690 """Appends -arch arguments to the command list if it's ios platform.
691
692 iOS binaries are universal binaries, and require specifying the architecture
693 to use, and one architecture needs to be specified for each binary.
694 """
695 if _IsIOS():
696 cmd_list.extend(['-arch=x86_64'] * num_archs)
697
698
Yuke Liao506e8822017-12-04 16:52:54699def _GetBinaryPath(command):
700 """Returns a relative path to the binary to be run by the command.
701
Yuke Liao545db322018-02-15 17:12:01702 Currently, following types of commands are supported (e.g. url_unittests):
703 1. Run test binary direcly: "out/coverage/url_unittests <arguments>"
704 2. Use xvfb.
705 2.1. "python testing/xvfb.py out/coverage/url_unittests <arguments>"
706 2.2. "testing/xvfb.py out/coverage/url_unittests <arguments>"
Yuke Liao92107f02018-03-07 01:44:37707 3. Use iossim to run tests on iOS platform, please refer to testing/iossim.mm
708 for its usage.
Yuke Liaoa0c8c2f2018-02-28 20:14:10709 3.1. "out/Coverage-iphonesimulator/iossim
Yuke Liao92107f02018-03-07 01:44:37710 <iossim_arguments> -c <app_arguments>
711 out/Coverage-iphonesimulator/url_unittests.app"
712
Yuke Liao506e8822017-12-04 16:52:54713 Args:
714 command: A command used to run a target.
715
716 Returns:
717 A relative path to the binary.
718 """
Yuke Liao545db322018-02-15 17:12:01719 xvfb_script_name = os.extsep.join(['xvfb', 'py'])
720
Yuke Liaob2926832018-03-02 17:34:29721 command_parts = shlex.split(command)
Yuke Liao545db322018-02-15 17:12:01722 if os.path.basename(command_parts[0]) == 'python':
723 assert os.path.basename(command_parts[1]) == xvfb_script_name, (
Abhishek Aryafb70b532018-05-06 17:47:40724 'This tool doesn\'t understand the command: "%s".' % command)
Yuke Liao545db322018-02-15 17:12:01725 return command_parts[2]
726
727 if os.path.basename(command_parts[0]) == xvfb_script_name:
728 return command_parts[1]
729
Yuke Liaob2926832018-03-02 17:34:29730 if _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10731 # For a given application bundle, the binary resides in the bundle and has
732 # the same name with the application without the .app extension.
Yuke Liao92107f02018-03-07 01:44:37733 app_path = command_parts[-1].rstrip(os.path.sep)
Yuke Liaoa0c8c2f2018-02-28 20:14:10734 app_name = os.path.splitext(os.path.basename(app_path))[0]
735 return os.path.join(app_path, app_name)
736
Yuke Liaob2926832018-03-02 17:34:29737 return command_parts[0]
Yuke Liao506e8822017-12-04 16:52:54738
739
Yuke Liaob2926832018-03-02 17:34:29740def _IsIOSCommand(command):
Yuke Liaoa0c8c2f2018-02-28 20:14:10741 """Returns true if command is used to run tests on iOS platform."""
Yuke Liaob2926832018-03-02 17:34:29742 return os.path.basename(shlex.split(command)[0]) == 'iossim'
Yuke Liaoa0c8c2f2018-02-28 20:14:10743
744
Yuke Liao95d13d72017-12-07 18:18:50745def _VerifyTargetExecutablesAreInBuildDirectory(commands):
746 """Verifies that the target executables specified in the commands are inside
747 the given build directory."""
Yuke Liao506e8822017-12-04 16:52:54748 for command in commands:
749 binary_path = _GetBinaryPath(command)
Max Moroz1de68d72018-08-21 13:38:18750 binary_absolute_path = coverage_utils.GetFullPath(binary_path)
Abhishek Arya03911092018-05-21 16:42:35751 assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
Yuke Liao95d13d72017-12-07 18:18:50752 'Target executable "%s" in command: "%s" is outside of '
753 'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
Yuke Liao506e8822017-12-04 16:52:54754
755
756def _ValidateBuildingWithClangCoverage():
757 """Asserts that targets are built with Clang coverage enabled."""
Yuke Liao80afff32018-03-07 01:26:20758 build_args = _GetBuildArgs()
Yuke Liao506e8822017-12-04 16:52:54759
760 if (CLANG_COVERAGE_BUILD_ARG not in build_args or
761 build_args[CLANG_COVERAGE_BUILD_ARG] != 'true'):
Abhishek Arya1ec832c2017-12-05 18:06:59762 assert False, ('\'{} = true\' is required in args.gn.'
763 ).format(CLANG_COVERAGE_BUILD_ARG)
Yuke Liao506e8822017-12-04 16:52:54764
765
Yuke Liaoc60b2d02018-03-02 21:40:43766def _ValidateCurrentPlatformIsSupported():
767 """Asserts that this script suports running on the current platform"""
768 target_os = _GetTargetOS()
769 if target_os:
770 current_platform = target_os
771 else:
Max Moroz1de68d72018-08-21 13:38:18772 current_platform = coverage_utils.GetHostPlatform()
Yuke Liaoc60b2d02018-03-02 21:40:43773
774 assert current_platform in [
775 'linux', 'mac', 'chromeos', 'ios'
776 ], ('Coverage is only supported on linux, mac, chromeos and ios.')
777
778
Yuke Liao80afff32018-03-07 01:26:20779def _GetBuildArgs():
Yuke Liao506e8822017-12-04 16:52:54780 """Parses args.gn file and returns results as a dictionary.
781
782 Returns:
783 A dictionary representing the build args.
784 """
Yuke Liao80afff32018-03-07 01:26:20785 global _BUILD_ARGS
786 if _BUILD_ARGS is not None:
787 return _BUILD_ARGS
788
789 _BUILD_ARGS = {}
Yuke Liao506e8822017-12-04 16:52:54790 build_args_path = os.path.join(BUILD_DIR, 'args.gn')
791 assert os.path.exists(build_args_path), ('"%s" is not a build directory, '
792 'missing args.gn file.' % BUILD_DIR)
793 with open(build_args_path) as build_args_file:
794 build_args_lines = build_args_file.readlines()
795
Yuke Liao506e8822017-12-04 16:52:54796 for build_arg_line in build_args_lines:
797 build_arg_without_comments = build_arg_line.split('#')[0]
798 key_value_pair = build_arg_without_comments.split('=')
799 if len(key_value_pair) != 2:
800 continue
801
802 key = key_value_pair[0].strip()
Yuke Liaoc60b2d02018-03-02 21:40:43803
804 # Values are wrapped within a pair of double-quotes, so remove the leading
805 # and trailing double-quotes.
806 value = key_value_pair[1].strip().strip('"')
Yuke Liao80afff32018-03-07 01:26:20807 _BUILD_ARGS[key] = value
Yuke Liao506e8822017-12-04 16:52:54808
Yuke Liao80afff32018-03-07 01:26:20809 return _BUILD_ARGS
Yuke Liao506e8822017-12-04 16:52:54810
811
Abhishek Arya16f059a2017-12-07 17:47:32812def _VerifyPathsAndReturnAbsolutes(paths):
813 """Verifies that the paths specified in |paths| exist and returns absolute
814 versions.
Yuke Liao66da1732017-12-05 22:19:42815
816 Args:
817 paths: A list of files or directories.
818 """
Abhishek Arya16f059a2017-12-07 17:47:32819 absolute_paths = []
Yuke Liao66da1732017-12-05 22:19:42820 for path in paths:
Abhishek Arya16f059a2017-12-07 17:47:32821 absolute_path = os.path.join(SRC_ROOT_PATH, path)
822 assert os.path.exists(absolute_path), ('Path: "%s" doesn\'t exist.' % path)
823
824 absolute_paths.append(absolute_path)
825
826 return absolute_paths
Yuke Liao66da1732017-12-05 22:19:42827
828
Abhishek Arya64636af2018-05-04 14:42:13829def _GetBinaryPathsFromTargets(targets, build_dir):
830 """Return binary paths from target names."""
831 # FIXME: Derive output binary from target build definitions rather than
832 # assuming that it is always the same name.
833 binary_paths = []
834 for target in targets:
835 binary_path = os.path.join(build_dir, target)
Max Moroz1de68d72018-08-21 13:38:18836 if coverage_utils.GetHostPlatform() == 'win':
Abhishek Arya64636af2018-05-04 14:42:13837 binary_path += '.exe'
838
839 if os.path.exists(binary_path):
840 binary_paths.append(binary_path)
841 else:
842 logging.warning(
Abhishek Aryafb70b532018-05-06 17:47:40843 'Target binary "%s" not found in build directory, skipping.',
Abhishek Arya64636af2018-05-04 14:42:13844 os.path.basename(binary_path))
845
846 return binary_paths
847
848
Abhishek Arya03911092018-05-21 16:42:35849def _GetCommandForWebTests(arguments):
850 """Return command to run for blink web tests."""
851 command_list = [
852 'python', 'testing/xvfb.py', 'python',
853 'third_party/blink/tools/run_web_tests.py',
854 '--additional-driver-flag=--no-sandbox',
Abhishek Aryabd0655d2018-05-21 19:55:24855 '--additional-env-var=LLVM_PROFILE_FILE=%s' %
Max Moroz1de68d72018-08-21 13:38:18856 LLVM_PROFILE_FILE_PATH_SUBSTITUTION, '--batch-size=1',
Abhishek Arya03911092018-05-21 16:42:35857 '--child-processes=%d' % max(1, int(multiprocessing.cpu_count() / 2)),
Abhishek Aryabd0655d2018-05-21 19:55:24858 '--disable-breakpad', '--no-show-results', '--skip-failing-tests',
Abhishek Arya03911092018-05-21 16:42:35859 '--target=%s' % os.path.basename(BUILD_DIR), '--time-out-ms=30000'
860 ]
861 if arguments.strip():
862 command_list.append(arguments)
863 return ' '.join(command_list)
864
865
866def _GetBinaryPathForWebTests():
867 """Return binary path used to run blink web tests."""
Max Moroz1de68d72018-08-21 13:38:18868 host_platform = coverage_utils.GetHostPlatform()
Abhishek Arya03911092018-05-21 16:42:35869 if host_platform == 'win':
870 return os.path.join(BUILD_DIR, 'content_shell.exe')
871 elif host_platform == 'linux':
872 return os.path.join(BUILD_DIR, 'content_shell')
873 elif host_platform == 'mac':
874 return os.path.join(BUILD_DIR, 'Content Shell.app', 'Contents', 'MacOS',
875 'Content Shell')
876 else:
877 assert False, 'This platform is not supported for web tests.'
878
879
Abhishek Aryae5811afa2018-05-24 03:56:01880def _SetupOutputDir():
881 """Setup output directory."""
882 if os.path.exists(OUTPUT_DIR):
883 shutil.rmtree(OUTPUT_DIR)
884
885 # Creates |OUTPUT_DIR| and its platform sub-directory.
Max Moroz1de68d72018-08-21 13:38:18886 os.makedirs(coverage_utils.GetCoverageReportRootDirPath(OUTPUT_DIR))
Abhishek Aryae5811afa2018-05-24 03:56:01887
888
Yuke Liao506e8822017-12-04 16:52:54889def _ParseCommandArguments():
890 """Adds and parses relevant arguments for tool comands.
891
892 Returns:
893 A dictionary representing the arguments.
894 """
895 arg_parser = argparse.ArgumentParser()
896 arg_parser.usage = __doc__
897
Abhishek Arya1ec832c2017-12-05 18:06:59898 arg_parser.add_argument(
899 '-b',
900 '--build-dir',
901 type=str,
902 required=True,
903 help='The build directory, the path needs to be relative to the root of '
904 'the checkout.')
Yuke Liao506e8822017-12-04 16:52:54905
Abhishek Arya1ec832c2017-12-05 18:06:59906 arg_parser.add_argument(
907 '-o',
908 '--output-dir',
909 type=str,
910 required=True,
911 help='Output directory for generated artifacts.')
Yuke Liao506e8822017-12-04 16:52:54912
Abhishek Arya1ec832c2017-12-05 18:06:59913 arg_parser.add_argument(
914 '-c',
915 '--command',
916 action='append',
Abhishek Arya64636af2018-05-04 14:42:13917 required=False,
Abhishek Arya1ec832c2017-12-05 18:06:59918 help='Commands used to run test targets, one test target needs one and '
919 'only one command, when specifying commands, one should assume the '
Abhishek Arya64636af2018-05-04 14:42:13920 'current working directory is the root of the checkout. This option is '
921 'incompatible with -p/--profdata-file option.')
922
923 arg_parser.add_argument(
Abhishek Arya03911092018-05-21 16:42:35924 '-wt',
925 '--web-tests',
926 nargs='?',
927 type=str,
928 const=' ',
929 required=False,
930 help='Run blink web tests. Support passing arguments to run_web_tests.py')
931
932 arg_parser.add_argument(
Abhishek Arya64636af2018-05-04 14:42:13933 '-p',
934 '--profdata-file',
935 type=str,
936 required=False,
937 help='Path to profdata file to use for generating code coverage reports. '
938 'This can be useful if you generated the profdata file seperately in '
939 'your own test harness. This option is ignored if run command(s) are '
940 'already provided above using -c/--command option.')
Yuke Liao506e8822017-12-04 16:52:54941
Abhishek Arya1ec832c2017-12-05 18:06:59942 arg_parser.add_argument(
Yuke Liao66da1732017-12-05 22:19:42943 '-f',
944 '--filters',
945 action='append',
Abhishek Arya16f059a2017-12-07 17:47:32946 required=False,
Yuke Liao66da1732017-12-05 22:19:42947 help='Directories or files to get code coverage for, and all files under '
948 'the directories are included recursively.')
949
950 arg_parser.add_argument(
Yuke Liao0e4c8682018-04-18 21:06:59951 '-i',
952 '--ignore-filename-regex',
953 type=str,
954 help='Skip source code files with file paths that match the given '
955 'regular expression. For example, use -i=\'.*/out/.*|.*/third_party/.*\' '
956 'to exclude files in third_party/ and out/ folders from the report.')
957
958 arg_parser.add_argument(
Yuke Liao1b852fd2018-05-11 17:07:32959 '--no-file-view',
960 action='store_true',
961 help='Don\'t generate the file view in the coverage report. When there '
962 'are large number of html files, the file view becomes heavy and may '
963 'cause the browser to freeze, and this argument comes handy.')
964
965 arg_parser.add_argument(
Max Moroz1de68d72018-08-21 13:38:18966 '--no-component-view',
967 action='store_true',
968 help='Don\'t generate the component view in the coverage report.')
969
970 arg_parser.add_argument(
Yuke Liao082e99632018-05-18 15:40:40971 '--coverage-tools-dir',
972 type=str,
973 help='Path of the directory where LLVM coverage tools (llvm-cov, '
974 'llvm-profdata) exist. This should be only needed if you are testing '
975 'against a custom built clang revision. Otherwise, we pick coverage '
976 'tools automatically from your current source checkout.')
977
978 arg_parser.add_argument(
Abhishek Arya1ec832c2017-12-05 18:06:59979 '-j',
980 '--jobs',
981 type=int,
982 default=None,
983 help='Run N jobs to build in parallel. If not specified, a default value '
984 'will be derived based on CPUs availability. Please refer to '
985 '\'ninja -h\' for more details.')
Yuke Liao506e8822017-12-04 16:52:54986
Abhishek Arya1ec832c2017-12-05 18:06:59987 arg_parser.add_argument(
Yuke Liao481d3482018-01-29 19:17:10988 '-v',
989 '--verbose',
990 action='store_true',
991 help='Prints additional output for diagnostics.')
992
993 arg_parser.add_argument(
994 '-l', '--log_file', type=str, help='Redirects logs to a file.')
995
996 arg_parser.add_argument(
Abhishek Aryac19bc5ef2018-05-04 22:10:02997 'targets',
998 nargs='+',
999 help='The names of the test targets to run. If multiple run commands are '
1000 'specified using the -c/--command option, then the order of targets and '
1001 'commands must match, otherwise coverage generation will fail.')
Yuke Liao506e8822017-12-04 16:52:541002
1003 args = arg_parser.parse_args()
1004 return args
1005
1006
1007def Main():
1008 """Execute tool commands."""
Yuke Liao082e99632018-05-18 15:40:401009 # Setup coverage binaries even when script is called with empty params. This
1010 # is used by coverage bot for initial setup.
1011 if len(sys.argv) == 1:
1012 DownloadCoverageToolsIfNeeded()
1013 print(__doc__)
1014 return
1015
Abhishek Arya64636af2018-05-04 14:42:131016 # Change directory to source root to aid in relative paths calculations.
Abhishek Arya03911092018-05-21 16:42:351017 global SRC_ROOT_PATH
Max Moroz1de68d72018-08-21 13:38:181018 SRC_ROOT_PATH = coverage_utils.GetFullPath(
Abhishek Arya03911092018-05-21 16:42:351019 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
Abhishek Arya64636af2018-05-04 14:42:131020 os.chdir(SRC_ROOT_PATH)
Abhishek Arya8a0751a2018-05-03 18:53:111021
Yuke Liao506e8822017-12-04 16:52:541022 args = _ParseCommandArguments()
Max Moroz1de68d72018-08-21 13:38:181023 coverage_utils.ConfigureLogging(verbose=args.verbose, log_file=args.log_file)
Yuke Liao082e99632018-05-18 15:40:401024 _ConfigureLLVMCoverageTools(args)
Abhishek Arya64636af2018-05-04 14:42:131025
Yuke Liao506e8822017-12-04 16:52:541026 global BUILD_DIR
Max Moroz1de68d72018-08-21 13:38:181027 BUILD_DIR = coverage_utils.GetFullPath(args.build_dir)
Abhishek Aryae5811afa2018-05-24 03:56:011028
Yuke Liao506e8822017-12-04 16:52:541029 global OUTPUT_DIR
Max Moroz1de68d72018-08-21 13:38:181030 OUTPUT_DIR = coverage_utils.GetFullPath(args.output_dir)
Yuke Liao506e8822017-12-04 16:52:541031
Abhishek Arya03911092018-05-21 16:42:351032 assert args.web_tests or args.command or args.profdata_file, (
Abhishek Arya64636af2018-05-04 14:42:131033 'Need to either provide commands to run using -c/--command option OR '
Abhishek Arya03911092018-05-21 16:42:351034 'provide prof-data file as input using -p/--profdata-file option OR '
1035 'run web tests using -wt/--run-web-tests.')
Yuke Liaoc60b2d02018-03-02 21:40:431036
Abhishek Arya64636af2018-05-04 14:42:131037 assert not args.command or (len(args.targets) == len(args.command)), (
1038 'Number of targets must be equal to the number of test commands.')
Yuke Liaoc60b2d02018-03-02 21:40:431039
Abhishek Arya1ec832c2017-12-05 18:06:591040 assert os.path.exists(BUILD_DIR), (
Abhishek Aryafb70b532018-05-06 17:47:401041 'Build directory: "%s" doesn\'t exist. '
1042 'Please run "gn gen" to generate.' % BUILD_DIR)
Abhishek Arya64636af2018-05-04 14:42:131043
Yuke Liaoc60b2d02018-03-02 21:40:431044 _ValidateCurrentPlatformIsSupported()
Yuke Liao506e8822017-12-04 16:52:541045 _ValidateBuildingWithClangCoverage()
Abhishek Arya16f059a2017-12-07 17:47:321046
1047 absolute_filter_paths = []
Yuke Liao66da1732017-12-05 22:19:421048 if args.filters:
Abhishek Arya16f059a2017-12-07 17:47:321049 absolute_filter_paths = _VerifyPathsAndReturnAbsolutes(args.filters)
Yuke Liao66da1732017-12-05 22:19:421050
Abhishek Aryae5811afa2018-05-24 03:56:011051 _SetupOutputDir()
Yuke Liao506e8822017-12-04 16:52:541052
Abhishek Arya03911092018-05-21 16:42:351053 # Get .profdata file and list of binary paths.
1054 if args.web_tests:
1055 commands = [_GetCommandForWebTests(args.web_tests)]
1056 profdata_file_path = _CreateCoverageProfileDataForTargets(
1057 args.targets, commands, args.jobs)
1058 binary_paths = [_GetBinaryPathForWebTests()]
1059 elif args.command:
1060 for i in range(len(args.command)):
1061 assert not 'run_web_tests.py' in args.command[i], (
1062 'run_web_tests.py is not supported via --command argument. '
1063 'Please use --run-web-tests argument instead.')
1064
Abhishek Arya64636af2018-05-04 14:42:131065 # A list of commands are provided. Run them to generate profdata file, and
1066 # create a list of binary paths from parsing commands.
1067 _VerifyTargetExecutablesAreInBuildDirectory(args.command)
1068 profdata_file_path = _CreateCoverageProfileDataForTargets(
1069 args.targets, args.command, args.jobs)
1070 binary_paths = [_GetBinaryPath(command) for command in args.command]
1071 else:
1072 # An input prof-data file is already provided. Just calculate binary paths.
1073 profdata_file_path = args.profdata_file
1074 binary_paths = _GetBinaryPathsFromTargets(args.targets, args.build_dir)
Yuke Liaoea228d02018-01-05 19:10:331075
Max Moroz1de68d72018-08-21 13:38:181076 binary_paths.extend(
1077 coverage_utils.GetSharedLibraries(binary_paths, BUILD_DIR))
Abhishek Arya78120bc2018-05-07 20:53:541078
Yuke Liao481d3482018-01-29 19:17:101079 logging.info('Generating code coverage report in html (this can take a while '
Abhishek Aryafb70b532018-05-06 17:47:401080 'depending on size of target!).')
Max Moroz1de68d72018-08-21 13:38:181081 per_file_summary_data = _GeneratePerFileCoverageSummary(
Yuke Liao0e4c8682018-04-18 21:06:591082 binary_paths, profdata_file_path, absolute_filter_paths,
1083 args.ignore_filename_regex)
Yuke Liaodd1ec0592018-02-02 01:26:371084 _GeneratePerFileLineByLineCoverageInHtml(binary_paths, profdata_file_path,
Yuke Liao0e4c8682018-04-18 21:06:591085 absolute_filter_paths,
1086 args.ignore_filename_regex)
Max Moroz1de68d72018-08-21 13:38:181087 component_mappings = None
1088 if not args.no_component_view:
1089 component_mappings = json.load(urllib2.urlopen(COMPONENT_MAPPING_URL))
Yuke Liaodd1ec0592018-02-02 01:26:371090
Max Moroz1de68d72018-08-21 13:38:181091 # Call prepare here.
1092 processor = coverage_utils.CoverageReportPostProcessor(
1093 OUTPUT_DIR,
1094 SRC_ROOT_PATH,
1095 per_file_summary_data,
1096 no_component_view=args.no_component_view,
1097 no_file_view=args.no_file_view,
1098 component_mappings=component_mappings)
Yuke Liaodd1ec0592018-02-02 01:26:371099
Max Moroz1de68d72018-08-21 13:38:181100 processor.PrepareHtmlReport()
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())