blob: 13f428abe1de594b58075ceebe34508b66a6b7cf [file] [log] [blame]
#!/usr/bin/env vpython3
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Runs Web Platform Tests (WPT) on Android browsers.
This script supports running tests on the Chromium Waterfall by mapping isolated
script flags to WPT flags.
It is also useful for local reproduction by performing APK installation and
configuring the browser to resolve test hosts. Be sure to invoke this
executable directly rather than using python run_android_wpt.py so that
WPT dependencies in Chromium vpython are found.
If you need more advanced test control, please use the runner located at
//third_party/wpt_tools/wpt/wpt.
Here's the mapping [isolate script flag] : [wpt flag]
--isolated-script-test-output : --log-chromium
--total-shards : --total-chunks
--shard-index : -- this-chunk
"""
# TODO(aluo): Combine or factor out commons parts with run_wpt_tests.py script.
import argparse
import contextlib
import json
import logging
import os
import shutil
import sys
import common
import wpt_common
logger = logging.getLogger(__name__)
SRC_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
BUILD_ANDROID = os.path.join(SRC_DIR, 'build', 'android')
BLINK_TOOLS_DIR = os.path.join(
SRC_DIR, 'third_party', 'blink', 'tools')
CATAPULT_DIR = os.path.join(SRC_DIR, 'third_party', 'catapult')
DEFAULT_WPT = os.path.join(
SRC_DIR, 'third_party', 'wpt_tools', 'wpt', 'wpt')
PYUTILS = os.path.join(CATAPULT_DIR, 'common', 'py_utils')
TOMBSTONE_PARSER = os.path.join(SRC_DIR, 'build', 'android', 'tombstones.py')
if PYUTILS not in sys.path:
sys.path.append(PYUTILS)
if BLINK_TOOLS_DIR not in sys.path:
sys.path.append(BLINK_TOOLS_DIR)
if BUILD_ANDROID not in sys.path:
sys.path.append(BUILD_ANDROID)
import devil_chromium
from blinkpy.web_tests.port.android import (
PRODUCTS, PRODUCTS_TO_EXPECTATION_FILE_PATHS, ANDROID_WEBLAYER,
ANDROID_WEBVIEW, CHROME_ANDROID, ANDROID_DISABLED_TESTS)
from devil import devil_env
from devil.android import apk_helper
from devil.android import device_utils
from devil.android.tools import system_app
from devil.android.tools import webview_app
from pylib.local.emulator import avd
from py_utils.tempfile_ext import NamedTemporaryDirectory
class PassThroughArgs(argparse.Action):
pass_through_args = []
def __call__(self, parser, namespace, values, option_string=None):
if option_string:
if self.nargs == 0:
self.add_unique_pass_through_arg(option_string)
elif self.nargs is None:
self.add_unique_pass_through_arg('{}={}'.format(option_string, values))
else:
raise ValueError("nargs {} not supported: {} {}".format(
self.nargs, option_string, values))
@classmethod
def add_unique_pass_through_arg(cls, arg):
if arg not in cls.pass_through_args:
cls.pass_through_args.append(arg)
def _get_adapter(product, device):
if product == ANDROID_WEBLAYER:
return WPTWeblayerAdapter(device)
elif product == ANDROID_WEBVIEW:
return WPTWebviewAdapter(device)
else:
return WPTClankAdapter(device)
class WPTAndroidAdapter(wpt_common.BaseWptScriptAdapter):
def __init__(self, device):
self.pass_through_wpt_args = []
self.pass_through_binary_args = []
self._metadata_dir = None
self._device = device
super(WPTAndroidAdapter, self).__init__()
# Arguments from add_extra_argumentsparse were added so
# its safe to parse the arguments and set self._options
self.parse_args()
self.output_directory = os.path.join(SRC_DIR, 'out', self.options.target)
self.mojo_js_directory = os.path.join(self.output_directory, 'gen')
@property
def rest_args(self):
rest_args = super(WPTAndroidAdapter, self).rest_args
# Here we add all of the arguments required to run WPT tests on Android.
rest_args.extend([self.options.wpt_path])
# By default, WPT will treat unexpected passes as errors, so we disable
# that to be consistent with Chromium CI.
rest_args.extend(['--no-fail-on-unexpected-pass'])
# vpython has packages needed by wpt, so force it to skip the setup
rest_args.extend(['--venv=' + SRC_DIR, '--skip-venv-setup'])
rest_args.extend(['run',
'--tests=' + wpt_common.EXTERNAL_WPT_TESTS_DIR,
'--test-type=' + self.options.test_type,
'--device-serial', self._device.serial,
'--webdriver-binary',
self.options.webdriver_binary,
'--symbols-path',
self.output_directory,
'--stackwalk-binary',
TOMBSTONE_PARSER,
'--headless',
'--no-pause-after-test',
'--no-capture-stdio',
'--no-manifest-download',
'--binary-arg=--enable-blink-features=MojoJS,MojoJSTest',
'--binary-arg=--enable-blink-test-features',
'--binary-arg=--disable-field-trial-config',
'--enable-mojojs',
'--mojojs-path=' + self.mojo_js_directory,
'--binary-arg=--enable-features=DownloadService<DownloadServiceStudy',
'--binary-arg=--force-fieldtrials=DownloadServiceStudy/Enabled',
'--binary-arg=--force-fieldtrial-params=DownloadServiceStudy.Enabled:'
'start_up_delay_ms/0',
])
# if metadata was created then add the metadata directory
# to the list of wpt arguments
if self._metadata_dir:
rest_args.extend(['--metadata', self._metadata_dir])
if self.options.verbose >= 3:
rest_args.extend(['--log-mach=-', '--log-mach-level=debug',
'--log-mach-verbose'])
if self.options.verbose >= 4:
rest_args.extend(['--webdriver-arg=--verbose',
'--webdriver-arg="--log-path=-"'])
rest_args.extend(self.pass_through_wpt_args)
return rest_args
@property
def browser_specific_expectations_path(self):
raise NotImplementedError
def _extra_metadata_builder_args(self):
args = ['--additional-expectations=%s' % path
for path in self.options.additional_expectations]
if not self.options.ignore_browser_specific_expectations:
args.extend(['--additional-expectations',
self.browser_specific_expectations_path])
return args
def _maybe_build_metadata(self):
metadata_builder_cmd = [
sys.executable,
os.path.join(wpt_common.BLINK_TOOLS_DIR, 'build_wpt_metadata.py'),
'--android-product',
self.options.product,
'--metadata-output-dir',
self._metadata_dir,
'--additional-expectations',
ANDROID_DISABLED_TESTS,
'--use-subtest-results',
]
if self.options.ignore_default_expectations:
metadata_builder_cmd += [ '--ignore-default-expectations' ]
metadata_builder_cmd.extend(self._extra_metadata_builder_args())
return common.run_command(metadata_builder_cmd)
def run_test(self):
with NamedTemporaryDirectory() as tmp_dir, self._install_apks():
self._metadata_dir = os.path.join(tmp_dir, 'metadata_dir')
metadata_command_ret = self._maybe_build_metadata()
if metadata_command_ret != 0:
return metadata_command_ret
# If there is no metadata then we need to create an
# empty directory to pass to wptrunner
if not os.path.exists(self._metadata_dir):
os.makedirs(self._metadata_dir)
return super(WPTAndroidAdapter, self).run_test()
def _install_apks(self):
raise NotImplementedError
def clean_up_after_test_run(self):
# Avoid having a dangling reference to the temp directory
# which was deleted
self._metadata_dir = None
def add_extra_arguments(self, parser):
# TODO: |pass_through_args| are broke and need to be supplied by way of
# --binary-arg".
class BinaryPassThroughArgs(PassThroughArgs):
pass_through_args = self.pass_through_binary_args
class WPTPassThroughArgs(PassThroughArgs):
pass_through_args = self.pass_through_wpt_args
# Add this so that product argument does not go in self._rest_args
# when self.parse_args() is called
parser.add_argument('--target', '-t', default='Release',
help='Specify the target build subdirectory under'
' src/out/.')
parser.add_argument('--product', help=argparse.SUPPRESS)
parser.add_argument('--webdriver-binary', required=True,
help='Path of the webdriver binary. It needs to have'
' the same major version as the apk.')
parser.add_argument('--wpt-path', default=DEFAULT_WPT,
help='Controls the path of the WPT runner to use'
' (therefore tests). Defaults the revision rolled into'
' Chromium.')
parser.add_argument('--additional-expectations',
action='append', default=[],
help='Paths to additional test expectations files.')
parser.add_argument('--ignore-default-expectations', action='store_true',
help='Do not use the default set of'
' TestExpectations files.')
parser.add_argument('--ignore-browser-specific-expectations',
action='store_true', default=False,
help='Ignore browser specific expectation files.')
parser.add_argument('--test-type', default='testharness',
help='Specify to experiment with other test types.'
' Currently only the default is expected to work.')
parser.add_argument('--verbose', '-v', action='count', default=0,
help='Verbosity level.')
parser.add_argument('--repeat',
action=WPTPassThroughArgs, type=int,
help='Number of times to run the tests.')
parser.add_argument('--include', metavar='TEST_OR_DIR',
action=WPTPassThroughArgs,
help='Test(s) to run, defaults to run all tests.')
parser.add_argument('--include-file',
action=WPTPassThroughArgs,
help='A file listing test(s) to run')
parser.add_argument('--list-tests', action=WPTPassThroughArgs, nargs=0,
help="Don't run any tests, just print out a list of"
' tests that would be run.')
parser.add_argument('--webdriver-arg', action=WPTPassThroughArgs,
help='WebDriver args.')
parser.add_argument('--log-wptreport', metavar='WPT_REPORT_FILE',
action=WPTPassThroughArgs,
help="Log wptreport with subtest details.")
parser.add_argument('--log-raw', metavar='RAW_REPORT_FILE',
action=WPTPassThroughArgs,
help="Log raw report.")
parser.add_argument('--log-html', metavar='HTML_REPORT_FILE',
action=WPTPassThroughArgs,
help="Log html report.")
parser.add_argument('--log-xunit', metavar='XUNIT_REPORT_FILE',
action=WPTPassThroughArgs,
help="Log xunit report.")
parser.add_argument('--enable-features', action=BinaryPassThroughArgs,
help='Chromium features to enable during testing.')
parser.add_argument('--disable-features', action=BinaryPassThroughArgs,
help='Chromium features to disable during testing.')
parser.add_argument('--disable-field-trial-config',
action=BinaryPassThroughArgs,
help='Disable test trials for Chromium features.')
parser.add_argument('--force-fieldtrials', action=BinaryPassThroughArgs,
help='Force trials for Chromium features.')
parser.add_argument('--force-fieldtrial-params',
action=BinaryPassThroughArgs,
help='Force trial params for Chromium features.')
add_emulator_args(parser)
class WPTWeblayerAdapter(WPTAndroidAdapter):
WEBLAYER_SHELL_PKG = 'org.chromium.weblayer.shell'
WEBLAYER_SUPPORT_PKG = 'org.chromium.weblayer.support'
@contextlib.contextmanager
def _install_apks(self):
install_weblayer_shell_as_needed = maybe_install_user_apk(
self._device, self.options.weblayer_shell, self.WEBLAYER_SHELL_PKG)
install_weblayer_support_as_needed = maybe_install_user_apk(
self._device, self.options.weblayer_support, self.WEBLAYER_SUPPORT_PKG)
install_webview_provider_as_needed = maybe_install_webview_provider(
self._device, self.options.webview_provider)
with install_weblayer_shell_as_needed, \
install_weblayer_support_as_needed, \
install_webview_provider_as_needed:
yield
@property
def browser_specific_expectations_path(self):
return PRODUCTS_TO_EXPECTATION_FILE_PATHS[ANDROID_WEBLAYER]
def add_extra_arguments(self, parser):
super(WPTWeblayerAdapter, self).add_extra_arguments(parser)
parser.add_argument('--weblayer-shell',
help='WebLayer Shell apk to install.')
parser.add_argument('--weblayer-support',
help='WebLayer Support apk to install.')
parser.add_argument('--webview-provider',
help='Webview provider apk to install.')
@property
def rest_args(self):
args = super(WPTWeblayerAdapter, self).rest_args
args.append(ANDROID_WEBLAYER)
return args
class WPTWebviewAdapter(WPTAndroidAdapter):
def __init__(self, device):
super(WPTWebviewAdapter, self).__init__(device)
if self.options.system_webview_shell is not None:
self.system_webview_shell_pkg = apk_helper.GetPackageName(
self.options.system_webview_shell)
else:
self.system_webview_shell_pkg = 'org.chromium.webview_shell'
@contextlib.contextmanager
def _install_apks(self):
install_shell_as_needed = maybe_install_user_apk(
self._device, self.options.system_webview_shell,
self.system_webview_shell_pkg)
install_webview_provider_as_needed = maybe_install_webview_provider(
self._device, self.options.webview_provider)
with install_shell_as_needed, install_webview_provider_as_needed:
yield
@property
def browser_specific_expectations_path(self):
return PRODUCTS_TO_EXPECTATION_FILE_PATHS[ANDROID_WEBVIEW]
def add_extra_arguments(self, parser):
super(WPTWebviewAdapter, self).add_extra_arguments(parser)
parser.add_argument('--system-webview-shell',
help=('System WebView Shell apk to install. If not '
'specified then the on-device WebView apk '
'will be used.'))
parser.add_argument('--webview-provider',
help='Webview provider APK to install.')
@property
def rest_args(self):
args = super(WPTWebviewAdapter, self).rest_args
args.extend(['--package-name', self.system_webview_shell_pkg])
args.append(ANDROID_WEBVIEW)
return args
class WPTClankAdapter(WPTAndroidAdapter):
@contextlib.contextmanager
def _install_apks(self):
install_clank_as_needed = maybe_install_user_apk(
self._device, self.options.chrome_apk)
with install_clank_as_needed:
yield
@property
def browser_specific_expectations_path(self):
return PRODUCTS_TO_EXPECTATION_FILE_PATHS[CHROME_ANDROID]
def add_extra_arguments(self, parser):
super(WPTClankAdapter, self).add_extra_arguments(parser)
parser.add_argument(
'--chrome-apk', help='Chrome apk to install.')
parser.add_argument(
'--chrome-package-name',
help=('The package name of Chrome to test,'
' defaults to that of the compiled Chrome apk.'))
@property
def rest_args(self):
args = super(WPTClankAdapter, self).rest_args
if not self.options.chrome_package_name and not self.options.chrome_apk:
raise Exception('Either the --chrome-package-name or --chrome-apk '
'command line arguments must be used.')
if not self.options.chrome_package_name:
self.options.chrome_package_name = apk_helper.GetPackageName(
self.options.chrome_apk)
logger.info("Using Chrome apk's default package %s." %
self.options.chrome_package_name)
args.extend(['--package-name', self.options.chrome_package_name])
# add the product postional argument
args.append(CHROME_ANDROID)
return args
def maybe_install_webview_provider(device, apk):
if apk:
logger.info('Will install WebView apk at ' + apk)
return webview_app.UseWebViewProvider(device, apk)
else:
return no_op()
def maybe_install_user_apk(device, apk, expected_pkg=None):
"""contextmanager to install apk on device.
Args:
device: DeviceUtils instance on which to install the apk.
apk: Apk file path on host.
expected_pkg: Optional, check that apk's package name matches.
Returns:
If apk evaluates to false, returns a do-nothing contextmanager.
Otherwise, returns a contextmanager to install apk on device.
"""
if apk:
pkg = apk_helper.GetPackageName(apk)
if expected_pkg and pkg != expected_pkg:
raise ValueError('{} has incorrect package name: {}, expected {}.'.format(
apk, pkg, expected_pkg))
install_as_needed = app_installed(device, apk, pkg)
logger.info('Will install ' + pkg + ' at ' + apk)
else:
install_as_needed = no_op()
return install_as_needed
@contextlib.contextmanager
def app_installed(device, apk, pkg):
device.Install(apk)
try:
yield
finally:
device.Uninstall(pkg)
# Dummy contextmanager to simplify multiple optional managers.
@contextlib.contextmanager
def no_op():
yield
# This is not really a "script test" so does not need to manually add
# any additional compile targets.
def main_compile_targets(args):
json.dump([], args.output)
@contextlib.contextmanager
def get_device(args):
instance = None
try:
if args.avd_config:
avd_config = avd.AvdConfig(args.avd_config)
logger.warning('Install emulator from ' + args.avd_config)
avd_config.Install()
instance = avd_config.CreateInstance()
instance.Start(writable_system=True, window=args.emulator_window)
device_utils.DeviceUtils(instance.serial).WaitUntilFullyBooted()
#TODO(weizhong): when choose device, make sure abi matches with target
devices = device_utils.DeviceUtils.HealthyDevices()
if devices:
yield devices[0]
else:
yield
finally:
if instance:
instance.Stop()
def add_emulator_args(parser):
parser.add_argument(
'--avd-config',
type=os.path.realpath,
help='Path to the avd config textpb. '
'(See //tools/android/avd/proto/ for message definition'
' and existing textpb files.)')
parser.add_argument(
'--emulator-window',
action='store_true',
default=False,
help='Enable graphical window display on the emulator.')
def main():
devil_chromium.Initialize()
usage = '%(prog)s --product={' + ','.join(PRODUCTS) + '} ...'
product_parser = argparse.ArgumentParser(
add_help=False, prog='run_android_wpt.py', usage=usage)
product_parser.add_argument(
'--product', action='store', required=True, choices=PRODUCTS)
add_emulator_args(product_parser)
args, _ = product_parser.parse_known_args()
product = args.product
with get_device(args) as device:
if not device:
logger.error('There are no devices attached to this host. Exiting...')
return
adapter = _get_adapter(product, device)
if adapter.options.verbose:
if adapter.options.verbose == 1:
logger.setLevel(logging.INFO)
else:
logger.setLevel(logging.DEBUG)
# WPT setup for chrome and webview requires that PATH contains adb.
platform_tools_path = os.path.dirname(devil_env.config.FetchPath('adb'))
os.environ['PATH'] = ':'.join([platform_tools_path] +
os.environ['PATH'].split(':'))
return adapter.run_test()
if __name__ == '__main__':
# Conform minimally to the protocol defined by ScriptTest.
if 'compile_targets' in sys.argv:
funcs = {
'run': None,
'compile_targets': main_compile_targets,
}
sys.exit(common.run_script(sys.argv[1:], funcs))
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger()
sys.exit(main())