blob: c1fbd8b0a249607b9cf254731d54f132ebf6c717 [file] [log] [blame]
# Copyright 2021 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.
import argparse
import contextlib
import json
import logging
import os
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')
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.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 py_utils.tempfile_ext import NamedTemporaryDirectory
from pylib.local.emulator import avd
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)
class WPTAndroidAdapter(wpt_common.BaseWptScriptAdapter):
def __init__(self, devices):
self.pass_through_wpt_args = []
self.pass_through_binary_args = []
self._metadata_dir = None
self._devices = devices
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
# Update the output directory to the default if it's not set.
self.maybe_set_default_isolated_script_test_output()
# Here we add all of the arguments required to run WPT tests on Android.
rest_args.extend([self.wpt_binary])
# 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'])
if self.options.default_exclude:
rest_args.extend(['--default-exclude'])
# 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,
'--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',
])
for device in self._devices:
rest_args.extend(['--device-serial', device.serial])
# 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=-"'])
if self.options.log_wptreport:
wpt_output = self.options.isolated_script_test_output
self.wptreport = os.path.join(os.path.dirname(wpt_output),
'reports.json')
rest_args.extend(['--log-wptreport',
self.wptreport])
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('--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('--default-exclude', action='store_true', default=False,
help="Only run the tests explicitly given in arguments."
"No tests will run if the list is empty, and the "
"program will exit with status code 0.")
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',
action='store_true', default=False,
help="Generates a test report in JSON format.")
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._devices, self.options.weblayer_shell, self.WEBLAYER_SHELL_PKG)
install_weblayer_support_as_needed = _maybe_install_user_apk(
self._devices, self.options.weblayer_support, self.WEBLAYER_SUPPORT_PKG)
install_webview_provider_as_needed = _maybe_install_webview_provider(
self._devices, 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, devices):
super(WPTWebviewAdapter, self).__init__(devices)
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._devices, self.options.system_webview_shell,
self.system_webview_shell_pkg)
install_webview_provider_as_needed = _maybe_install_webview_provider(
self._devices, 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._devices, 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 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.')
parser.add_argument(
'-j', '--processes', dest='processes',
type=int,
default=1,
help='Number of emulator to run.')
@contextlib.contextmanager
def get_device(args):
with get_devices(args) as devices:
yield None if not devices else devices[0]
@contextlib.contextmanager
def get_devices(args):
instances = []
try:
if args.avd_config:
avd_config = avd.AvdConfig(args.avd_config)
logger.warning('Install emulator from ' + args.avd_config)
avd_config.Install()
for _ in range(max(args.processes, 1)):
instance = avd_config.CreateInstance()
instance.Start(writable_system=True, window=args.emulator_window)
device_utils.DeviceUtils(instance.serial).WaitUntilFullyBooted()
instances.append(instance)
#TODO(weizhong): when choose device, make sure abi matches with target
devices = device_utils.DeviceUtils.HealthyDevices()
if devices:
yield devices
else:
yield
finally:
for instance in instances:
instance.Stop()
def _maybe_install_webview_provider(devices, apk):
if apk:
logger.info('Will install WebView apk at ' + apk)
@contextlib.contextmanager
def use_webview_provider(devices, apk):
with contextlib.ExitStack() as stack:
for device in devices:
stack.enter_context(webview_app.UseWebViewProvider(device, apk))
yield
return use_webview_provider(devices, apk)
else:
return _no_op()
def _maybe_install_user_apk(devices, 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(devices, apk, pkg)
logger.info('Will install ' + pkg + ' at ' + apk)
else:
install_as_needed = _no_op()
return install_as_needed
@contextlib.contextmanager
def _app_installed(devices, apk, pkg):
for device in devices:
device.Install(apk)
try:
yield
finally:
for device in devices:
device.Uninstall(pkg)
# Dummy contextmanager to simplify multiple optional managers.
@contextlib.contextmanager
def _no_op():
yield