Coverage: Add --web-tests helper for running web tests.
- Add -wt/--web-tests argument for running run_web_tests.py with
custom arguments. Special configuration is needed such as
--no-sandbox, etc, so this automatically does that.
- Add assert to prevent users from incorrectly running
run_web_tests.py.
- Add _GetFullPath helper that resolves symbolic links. This
prevent infinite loop with 'parent_dir == SRC_ROOT_PATH'.
- Make _ExecuteCommand, _ExecuteIOSCommand write stdout, stderr
to file, so that execution can be monitored.
[email protected],[email protected]
Bug: 842851
Change-Id: I2eb1744cee34c3d367ec1ddf418879417064c808
Reviewed-on: https://chromium-review.googlesource.com/1067021
Commit-Queue: Abhishek Arya <[email protected]>
Reviewed-by: Max Moroz <[email protected]>
Cr-Commit-Position: refs/heads/master@{#560284}
diff --git a/tools/code_coverage/coverage.py b/tools/code_coverage/coverage.py
index 2b5128ae..e3a9595 100755
--- a/tools/code_coverage/coverage.py
+++ b/tools/code_coverage/coverage.py
@@ -11,7 +11,7 @@
"use_clang_coverage=true" and "is_component_build=false" GN flags to args.gn
file in your build output directory (e.g. out/coverage).
- Example usage:
+ * Example usage:
gn gen out/coverage --args='use_clang_coverage=true is_component_build=false'
gclient runhooks
@@ -28,7 +28,7 @@
If you want to run tests that try to draw to the screen but don't have a
display connected, you can run tests in headless mode with xvfb.
- Sample flow for running a test target with xvfb (e.g. unit_tests):
+ * Sample flow for running a test target with xvfb (e.g. unit_tests):
python tools/code_coverage/coverage.py unit_tests -b out/coverage \\
-o out/report -c 'python testing/xvfb.py out/coverage/unit_tests'
@@ -36,7 +36,7 @@
If you are building a fuzz target, you need to add "use_libfuzzer=true" GN
flag as well.
- Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
+ * Sample workflow for a fuzz target (e.g. pdfium_fuzzer):
python tools/code_coverage/coverage.py pdfium_fuzzer \\
-b out/coverage -o out/report \\
@@ -48,6 +48,14 @@
<runs> - number of times to fuzz target function. Should be 0 when you just
want to see the coverage on corpus and don't want to fuzz at all.
+ * Sample workflow for running Blink web tests:
+
+ python tools/code_coverage/coverage.py blink_tests \\
+ -wt -b out/coverage -o out/report -f third_party/blink
+
+ If you need to pass arguments to run_web_tests.py, use
+ -wt='arguments to run_web_tests.py e.g. test directories'
+
For more options, please refer to tools/code_coverage/coverage.py -h.
For an overview of how code coverage works in Chromium, please refer to
@@ -61,6 +69,7 @@
import argparse
import json
import logging
+import multiprocessing
import os
import re
import shlex
@@ -81,10 +90,6 @@
import jinja2
from collections import defaultdict
-# Absolute path to the root of the checkout.
-SRC_ROOT_PATH = os.path.abspath(
- os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
-
# Absolute path to the code coverage tools binary. These paths can be
# overwritten by user specified coverage tool paths.
LLVM_BUILD_DIR = clang_update.LLVM_BUILD_DIR
@@ -92,6 +97,9 @@
LLVM_COV_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-cov')
LLVM_PROFDATA_PATH = os.path.join(LLVM_BIN_DIR, 'llvm-profdata')
+# Absolute path to the root of the checkout.
+SRC_ROOT_PATH = None
+
# Build directory, the value is parsed from command line arguments.
BUILD_DIR = None
@@ -353,7 +361,7 @@
def _ConfigureLLVMCoverageTools(args):
"""Configures llvm coverage tools."""
if args.coverage_tools_dir:
- llvm_bin_dir = os.path.abspath(args.coverage_tools_dir)
+ llvm_bin_dir = _GetFullPath(args.coverage_tools_dir)
global LLVM_COV_PATH
global LLVM_PROFDATA_PATH
LLVM_COV_PATH = os.path.join(llvm_bin_dir, 'llvm-cov')
@@ -680,7 +688,7 @@
for component in component_to_directories:
for directory in component_to_directories[component]:
- absolute_directory_path = os.path.abspath(directory)
+ absolute_directory_path = _GetFullPath(directory)
if absolute_directory_path in per_directory_coverage_summary:
per_component_coverage_summary[component].AddSummary(
per_directory_coverage_summary[absolute_directory_path])
@@ -739,7 +747,7 @@
'Path')
for dir_path in component_to_directories[component_name]:
- dir_absolute_path = os.path.abspath(dir_path)
+ dir_absolute_path = _GetFullPath(dir_path)
if dir_absolute_path not in per_directory_coverage_summary:
# Any directory without an excercised file shouldn't be included into the
# report.
@@ -820,7 +828,7 @@
def _GetCoverageHtmlReportPathForFile(file_path):
"""Given a file path, returns the corresponding html report path."""
assert os.path.isfile(file_path), '"%s" is not a file.' % file_path
- html_report_path = os.extsep.join([os.path.abspath(file_path), 'html'])
+ html_report_path = os.extsep.join([_GetFullPath(file_path), 'html'])
# '+' is used instead of os.path.join because both of them are absolute paths
# and os.path.join ignores the first path.
@@ -832,7 +840,7 @@
"""Given a directory path, returns the corresponding html report path."""
assert os.path.isdir(dir_path), '"%s" is not a directory.' % dir_path
html_report_path = os.path.join(
- os.path.abspath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
+ _GetFullPath(dir_path), DIRECTORY_COVERAGE_HTML_REPORT_NAME)
# '+' is used instead of os.path.join because both of them are absolute paths
# and os.path.join ignores the first path.
@@ -985,13 +993,10 @@
# On iOS platform, due to lack of write permissions, profraw files are
# generated outside of the OUTPUT_DIR, and the exact paths are contained
# in the output of the command execution.
- output = _ExecuteIOSCommand(target, command)
+ output = _ExecuteIOSCommand(command, output_file_path)
else:
# On other platforms, profraw files are generated inside the OUTPUT_DIR.
- output = _ExecuteCommand(target, command)
-
- with open(output_file_path, 'w') as output_file:
- output_file.write(output)
+ output = _ExecuteCommand(target, command, output_file_path)
profraw_file_paths = []
if _IsIOS():
@@ -1028,7 +1033,17 @@
return profdata_file_paths
-def _ExecuteCommand(target, command):
+def _GetEnvironmentVars(profraw_file_path):
+ """Return environment vars for subprocess, given a profraw file path."""
+ env = os.environ.copy()
+ env.update({
+ 'LLVM_PROFILE_FILE': profraw_file_path,
+ 'PATH': _GetPathWithLLVMSymbolizerDir()
+ })
+ return env
+
+
+def _ExecuteCommand(target, command, output_file_path):
"""Runs a single command and generates a profraw data file."""
# Per Clang "Source-based Code Coverage" doc:
#
@@ -1058,20 +1073,16 @@
try:
# Some fuzz targets or tests may write into stderr, redirect it as well.
- output = subprocess.check_output(
- shlex.split(command),
- stderr=subprocess.STDOUT,
- env={
- 'LLVM_PROFILE_FILE': expected_profraw_file_path,
- 'PATH': _GetPathWithLLVMSymbolizerDir()
- })
+ with open(output_file_path, 'wb') as output_file_handle:
+ subprocess.check_call(
+ shlex.split(command),
+ stdout=output_file_handle,
+ stderr=subprocess.STDOUT,
+ env=_GetEnvironmentVars(expected_profraw_file_path))
except subprocess.CalledProcessError as e:
- output = e.output
- logging.warning(
- 'Command: "%s" exited with non-zero return code. Output:\n%s', command,
- output)
+ logging.warning('Command: "%s" exited with non-zero return code.', command)
- return output
+ return open(output_file_path, 'rb').read()
def _IsFuzzerTarget(target):
@@ -1082,7 +1093,7 @@
return use_libfuzzer and target.endswith('_fuzzer')
-def _ExecuteIOSCommand(target, command):
+def _ExecuteIOSCommand(command, output_file_path):
"""Runs a single iOS command and generates a profraw data file.
iOS application doesn't have write access to folders outside of the app, so
@@ -1100,18 +1111,18 @@
OUTPUT_DIR, os.extsep.join(['iossim', PROFRAW_FILE_EXTENSION]))
try:
- output = subprocess.check_output(
- shlex.split(command),
- env={
- 'LLVM_PROFILE_FILE': iossim_profraw_file_path,
- 'PATH': _GetPathWithLLVMSymbolizerDir()
- })
+ with open(output_file_path, 'wb') as output_file_handle:
+ subprocess.check_call(
+ shlex.split(command),
+ stdout=output_file_handle,
+ stderr=subprocess.STDOUT,
+ env=_GetEnvironmentVars(iossim_profraw_file_path))
except subprocess.CalledProcessError as e:
# iossim emits non-zero return code even if tests run successfully, so
# ignore the return code.
- output = e.output
+ pass
- return output
+ return open(output_file_path, 'rb').read()
def _GetProfrawDataFileByParsingOutput(output):
@@ -1283,7 +1294,6 @@
<iossim_arguments> -c <app_arguments>
out/Coverage-iphonesimulator/url_unittests.app"
-
Args:
command: A command used to run a target.
@@ -1321,8 +1331,8 @@
the given build directory."""
for command in commands:
binary_path = _GetBinaryPath(command)
- binary_absolute_path = os.path.abspath(os.path.normpath(binary_path))
- assert binary_absolute_path.startswith(BUILD_DIR), (
+ binary_absolute_path = _GetFullPath(binary_path)
+ assert binary_absolute_path.startswith(BUILD_DIR + os.sep), (
'Target executable "%s" in command: "%s" is outside of '
'the given build directory: "%s".' % (binary_path, command, BUILD_DIR))
@@ -1433,6 +1443,41 @@
return binary_paths
+def _GetFullPath(path):
+ """Return full absolute path."""
+ return (os.path.abspath(
+ os.path.realpath(os.path.expandvars(os.path.expanduser(path)))))
+
+
+def _GetCommandForWebTests(arguments):
+ """Return command to run for blink web tests."""
+ command_list = [
+ 'python', 'testing/xvfb.py', 'python',
+ 'third_party/blink/tools/run_web_tests.py',
+ '--additional-driver-flag=--no-sandbox',
+ '--disable-breakpad', '--no-show-results',
+ '--child-processes=%d' % max(1, int(multiprocessing.cpu_count() / 2)),
+ '--target=%s' % os.path.basename(BUILD_DIR), '--time-out-ms=30000'
+ ]
+ if arguments.strip():
+ command_list.append(arguments)
+ return ' '.join(command_list)
+
+
+def _GetBinaryPathForWebTests():
+ """Return binary path used to run blink web tests."""
+ host_platform = _GetHostPlatform()
+ if host_platform == 'win':
+ return os.path.join(BUILD_DIR, 'content_shell.exe')
+ elif host_platform == 'linux':
+ return os.path.join(BUILD_DIR, 'content_shell')
+ elif host_platform == 'mac':
+ return os.path.join(BUILD_DIR, 'Content Shell.app', 'Contents', 'MacOS',
+ 'Content Shell')
+ else:
+ assert False, 'This platform is not supported for web tests.'
+
+
def _ParseCommandArguments():
"""Adds and parses relevant arguments for tool comands.
@@ -1468,6 +1513,15 @@
'incompatible with -p/--profdata-file option.')
arg_parser.add_argument(
+ '-wt',
+ '--web-tests',
+ nargs='?',
+ type=str,
+ const=' ',
+ required=False,
+ help='Run blink web tests. Support passing arguments to run_web_tests.py')
+
+ arg_parser.add_argument(
'-p',
'--profdata-file',
type=str,
@@ -1547,6 +1601,9 @@
return
# Change directory to source root to aid in relative paths calculations.
+ global SRC_ROOT_PATH
+ SRC_ROOT_PATH = _GetFullPath(
+ os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))
os.chdir(SRC_ROOT_PATH)
args = _ParseCommandArguments()
@@ -1554,13 +1611,14 @@
_ConfigureLLVMCoverageTools(args)
global BUILD_DIR
- BUILD_DIR = os.path.abspath(args.build_dir)
+ BUILD_DIR = _GetFullPath(args.build_dir)
global OUTPUT_DIR
- OUTPUT_DIR = os.path.abspath(args.output_dir)
+ OUTPUT_DIR = _GetFullPath(args.output_dir)
- assert args.command or args.profdata_file, (
+ assert args.web_tests or args.command or args.profdata_file, (
'Need to either provide commands to run using -c/--command option OR '
- 'provide prof-data file as input using -p/--profdata-file option.')
+ 'provide prof-data file as input using -p/--profdata-file option OR '
+ 'run web tests using -wt/--run-web-tests.')
assert not args.command or (len(args.targets) == len(args.command)), (
'Number of targets must be equal to the number of test commands.')
@@ -1579,8 +1637,18 @@
if not os.path.exists(_GetCoverageReportRootDirPath()):
os.makedirs(_GetCoverageReportRootDirPath())
- # Get profdate file and list of binary paths.
- if args.command:
+ # Get .profdata file and list of binary paths.
+ if args.web_tests:
+ commands = [_GetCommandForWebTests(args.web_tests)]
+ profdata_file_path = _CreateCoverageProfileDataForTargets(
+ args.targets, commands, args.jobs)
+ binary_paths = [_GetBinaryPathForWebTests()]
+ elif args.command:
+ for i in range(len(args.command)):
+ assert not 'run_web_tests.py' in args.command[i], (
+ 'run_web_tests.py is not supported via --command argument. '
+ 'Please use --run-web-tests argument instead.')
+
# A list of commands are provided. Run them to generate profdata file, and
# create a list of binary paths from parsing commands.
_VerifyTargetExecutablesAreInBuildDirectory(args.command)
@@ -1626,7 +1694,7 @@
_OverwriteHtmlReportsIndexFile()
_CleanUpOutputDir()
- html_index_file_path = 'file://' + os.path.abspath(_GetHtmlIndexPath())
+ html_index_file_path = 'file://' + _GetFullPath(_GetHtmlIndexPath())
logging.info('Index file for html report is generated as: "%s".',
html_index_file_path)