blob: 81c350871e806df5c8d93d98cf5ea9deca390a0c [file] [log] [blame]
#!/usr/bin/env vpython3
# 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 posixpath
import re
import shutil
import sys
import time
from collections import OrderedDict
SRC_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
BLINK_TOOLS = os.path.join(
SRC_DIR, 'third_party', 'blink', 'tools')
BUILD_ANDROID = os.path.join(SRC_DIR, 'build', 'android')
CATAPULT_DIR = os.path.join(SRC_DIR, 'third_party', 'catapult')
PYUTILS = os.path.join(CATAPULT_DIR, 'common', 'py_utils')
TEST_SEED_PATH = os.path.join(SRC_DIR, 'testing', 'scripts',
'variations_smoke_test_data',
'webview_test_seed')
if PYUTILS not in sys.path:
sys.path.append(PYUTILS)
if BUILD_ANDROID not in sys.path:
sys.path.append(BUILD_ANDROID)
if BLINK_TOOLS not in sys.path:
sys.path.append(BLINK_TOOLS)
import common
import devil_chromium
import wpt_common
from blinkpy.web_tests.port.android import (
ANDROID_WEBLAYER, ANDROID_WEBVIEW, CHROME_ANDROID)
from devil import devil_env
from devil.android import apk_helper
from devil.android import flag_changer
from devil.android import logcat_monitor
from devil.android.tools import script_common
from devil.android.tools import system_app
from devil.android.tools import webview_app
from devil.utils import logging_common
from pylib.local.emulator import avd
from py_utils.tempfile_ext import NamedTemporaryDirectory
from wpt_android_lib import add_emulator_args, get_device
_LOGCAT_FILTERS = [
'chromium:v',
'cr_*:v',
'DEBUG:I',
'StrictMode:D',
'WebView*:v'
]
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
TEST_CASES = {}
class FinchTestCase(wpt_common.BaseWptScriptAdapter):
def __init__(self, device):
super(FinchTestCase, self).__init__()
self._device = device
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')
self.flags = flag_changer.FlagChanger(
self._device, '%s-command-line' % self.product_name())
self.browser_package_name = apk_helper.GetPackageName(
self.options.browser_apk)
self.browser_activity_name = (self.options.browser_activity_name or
self.default_browser_activity_name)
self.log_mon = None
if self.options.webview_provider_apk:
self.webview_provider_package_name = (
apk_helper.GetPackageName(self.options.webview_provider_apk))
@classmethod
def app_user_sub_dir(cls):
"""Returns sub directory within user directory"""
return 'app_%s' % cls.product_name()
@classmethod
def product_name(cls):
raise NotImplementedError
@classmethod
def wpt_product_name(cls):
raise NotImplementedError
@property
def tests(self):
return [
'dom/collections/HTMLCollection-delete.html',
'dom/collections/HTMLCollection-supported-property-names.html',
'dom/collections/HTMLCollection-supported-property-indices.html',
]
@property
def default_browser_activity_name(self):
raise NotImplementedError
def __enter__(self):
self._device.EnableRoot()
self.log_mon = logcat_monitor.LogcatMonitor(
self._device.adb,
output_file=os.path.join(
os.path.dirname(self.options.isolated_script_test_output),
'%s_finch_smoke_tests_logcat.txt' % self.product_name()),
filter_specs=_LOGCAT_FILTERS)
self.log_mon.Start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.flags.ReplaceFlags([])
self.log_mon.Stop()
@property
def rest_args(self):
rest_args = super(FinchTestCase, 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([
os.path.join(SRC_DIR, 'third_party', 'wpt_tools', 'wpt', 'wpt')])
# 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',
self.wpt_product_name(),
'--tests=' + wpt_common.EXTERNAL_WPT_TESTS_DIR,
'--test-type=' + 'testharness',
'--device-serial',
self._device.serial,
'--webdriver-binary',
os.path.join('clang_x64', 'chromedriver'),
'--symbols-path',
self.output_directory,
'--package-name',
self.browser_package_name,
'--keep-app-data-directory',
'--no-pause-after-test',
'--no-capture-stdio',
'--no-manifest-download',
'--enable-mojojs',
'--mojojs-path=' + self.mojo_js_directory,
])
for binary_arg in self.browser_command_line_args():
rest_args.append('--binary-arg=%s' % binary_arg)
for test in self.tests:
rest_args.extend(['--include', test])
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=-"'])
return rest_args
@classmethod
def add_extra_arguments(cls, parser):
parser.add_argument('--test-case',
choices=TEST_CASES.keys(),
# TODO(rmhasan): Remove default values after
# adding arguments to test suites. Also make
# this argument required.
default='webview',
help='Name of test case')
parser.add_argument('--finch-seed-path', default=TEST_SEED_PATH,
type=os.path.realpath,
help='Path to the finch seed')
parser.add_argument('--browser-apk',
'--webview-shell-apk',
'--weblayer-shell-apk',
help='Path to the browser apk',
type=os.path.realpath,
required=True)
parser.add_argument('--webview-provider-apk',
type=os.path.realpath,
help='Path to the WebView provider apk')
parser.add_argument('--additional-apk',
action='append',
type=os.path.realpath,
default=[],
help='List of additional apk\'s to install')
parser.add_argument('--browser-activity-name',
action='store',
help='Browser activity name')
parser.add_argument('--target',
action='store',
default='Release',
help='Build configuration')
add_emulator_args(parser)
script_common.AddDeviceArguments(parser)
script_common.AddEnvironmentArguments(parser)
logging_common.AddLoggingArguments(parser)
@contextlib.contextmanager
def _install_apks(self):
yield
@contextlib.contextmanager
def install_apks(self):
"""Install apks for testing"""
self._device.Uninstall(self.browser_package_name)
self._device.Install(self.options.browser_apk, reinstall=True)
for apk_path in self.options.additional_apk:
self._device.Install(apk_path)
yield
def browser_command_line_args(self):
# TODO(rmhasan): Add browser command line arguments
# for weblayer and chrome
return []
def run_tests(self, test_run_variation, results_dict):
"""Run browser test on test device
Args:
test_suffix: Suffix for log output
Returns:
True if browser did not crash or False if the browser crashed
"""
self.layout_test_results_subdir = ('%s_smoke_test_artifacts' %
test_run_variation)
# Make sure the browser is not running before the tests run
self.stop_browser()
ret = super(FinchTestCase, self).run_test()
self.stop_browser()
with open(self.wpt_output, 'r') as curr_test_results:
curr_results_dict = json.loads(curr_test_results.read())
results_dict['tests'][test_run_variation] = curr_results_dict['tests']
for result, count in curr_results_dict['num_failures_by_type'].items():
results_dict['num_failures_by_type'].setdefault(result, 0)
results_dict['num_failures_by_type'][result] += count
return ret
def stop_browser(self):
logger.info('Stopping package %s', self.browser_package_name)
self._device.ForceStop(self.browser_package_name)
if self.options.webview_provider_apk:
logger.info('Stopping package %s', self.webview_provider_package_name)
self._device.ForceStop(
self.webview_provider_package_name)
def start_browser(self):
full_activity_name = '%s/%s' % (self.browser_package_name,
self.browser_activity_name)
logger.info('Starting activity %s', full_activity_name)
self._device.RunShellCommand([
'am',
'start',
'-W',
'-n',
full_activity_name,
'-d',
'data:,'])
logger.info('Waiting 10 seconds')
time.sleep(10)
def _wait_for_local_state_file(self, local_state_file):
"""Wait for local state file to be generated"""
max_wait_time_secs = 120
delta_secs = 10
total_wait_time_secs = 0
self.start_browser()
while total_wait_time_secs < max_wait_time_secs:
if self._device.PathExists(local_state_file):
logger.info('Local state file generated')
self.stop_browser()
return
logger.info('Waiting %d seconds for the local state file to generate',
delta_secs)
time.sleep(delta_secs)
total_wait_time_secs += delta_secs
raise Exception('Timed out waiting for the '
'local state file to be generated')
def install_seed(self):
"""Install finch seed for testing
Returns:
None
"""
app_data_dir = posixpath.join(
self._device.GetApplicationDataDirectory(self.browser_package_name),
self.app_user_sub_dir())
device_local_state_file = posixpath.join(app_data_dir, 'Local State')
self._wait_for_local_state_file(device_local_state_file)
with NamedTemporaryDirectory() as tmp_dir:
tmp_ls_path = os.path.join(tmp_dir, 'local_state.json')
self._device.adb.Pull(device_local_state_file, tmp_ls_path)
with open(tmp_ls_path, 'r') as local_state_content, \
open(self.options.finch_seed_path, 'r') as test_seed_content:
local_state_json = json.loads(local_state_content.read())
test_seed_json = json.loads(test_seed_content.read())
# Copy over the seed data and signature
local_state_json['variations_compressed_seed'] = (
test_seed_json['variations_compressed_seed'])
local_state_json['variations_seed_signature'] = (
test_seed_json['variations_seed_signature'])
with open(os.path.join(tmp_dir, 'new_local_state.json'),
'w') as new_local_state:
new_local_state.write(json.dumps(local_state_json))
self._device.adb.Push(new_local_state.name, device_local_state_file)
user_id = self._device.GetUidForPackage(self.browser_package_name)
logger.info('Setting owner of Local State file to %r', user_id)
self._device.RunShellCommand(
['chown', user_id, device_local_state_file], as_root=True)
class ChromeFinchTestCase(FinchTestCase):
@classmethod
def product_name(cls):
"""Returns name of product being tested"""
return 'chrome'
@classmethod
def wpt_product_name(cls):
return CHROME_ANDROID
@property
def default_browser_activity_name(self):
return 'org.chromium.chrome.browser.ChromeTabbedActivity'
class WebViewFinchTestCase(FinchTestCase):
@classmethod
def product_name(cls):
"""Returns name of product being tested"""
return 'webview'
@classmethod
def wpt_product_name(cls):
return ANDROID_WEBVIEW
@property
def default_browser_activity_name(self):
return 'org.chromium.webview_shell.WebPlatformTestsActivity'
def browser_command_line_args(self):
return ['--webview-verbose-logging']
@contextlib.contextmanager
def install_apks(self):
"""Install apks for testing"""
with super(WebViewFinchTestCase, self).install_apks(), \
webview_app.UseWebViewProvider(self._device,
self.options.webview_provider_apk):
yield
def install_seed(self):
"""Install finch seed for testing
Returns:
None
"""
app_data_dir = posixpath.join(
self._device.GetApplicationDataDirectory(self.browser_package_name),
self.app_user_sub_dir())
self._device.RunShellCommand(['mkdir', '-p', app_data_dir],
run_as=self.browser_package_name)
seed_path = posixpath.join(app_data_dir, 'variations_seed')
seed_new_path = posixpath.join(app_data_dir, 'variations_seed_new')
seed_stamp = posixpath.join(app_data_dir, 'variations_stamp')
self._device.adb.Push(self.options.finch_seed_path, seed_path)
self._device.adb.Push(self.options.finch_seed_path, seed_new_path)
self._device.RunShellCommand(
['touch', seed_stamp], check_return=True,
run_as=self.browser_package_name)
# We need to make the WebView shell package an owner of the seeds,
# see crbug.com/1191169#c19
user_id = self._device.GetUidForPackage(self.browser_package_name)
logger.info('Setting owner of seed files to %r', user_id)
self._device.RunShellCommand(['chown', user_id, seed_path], as_root=True)
self._device.RunShellCommand(
['chown', user_id, seed_new_path], as_root=True)
class WebLayerFinchTestCase(FinchTestCase):
@classmethod
def product_name(cls):
"""Returns name of product being tested"""
return 'weblayer'
@classmethod
def wpt_product_name(cls):
return ANDROID_WEBLAYER
@property
def default_browser_activity_name(self):
return 'org.chromium.weblayer.shell.WebLayerShellActivity'
@contextlib.contextmanager
def install_apks(self):
"""Install apks for testing"""
with super(WebLayerFinchTestCase, self).install_apks(), \
webview_app.UseWebViewProvider(self._device,
self.options.webview_provider_apk):
yield
def main(args):
TEST_CASES.update(
{p.product_name(): p
for p in [ChromeFinchTestCase, WebViewFinchTestCase,
WebLayerFinchTestCase]})
parser = argparse.ArgumentParser()
FinchTestCase.add_extra_arguments(parser)
parser.add_argument(
'--isolated-script-test-output', type=str,
required=False,
help='path to write test results JSON object to')
options, _ = parser.parse_known_args(args)
with get_device(options) as device, \
TEST_CASES[options.test_case](device) as test_case, \
test_case.install_apks():
devil_chromium.Initialize(adb_path=options.adb_path)
logging_common.InitializeLogging(options)
# TODO(rmhasan): Best practice in Chromium is to allow users to provide
# their own adb binary to avoid adb server restarts. We should add a new
# command line argument to wptrunner so that users can pass the path to
# their adb binary.
platform_tools_path = os.path.dirname(devil_env.config.FetchPath('adb'))
os.environ['PATH'] = os.pathsep.join([platform_tools_path] +
os.environ['PATH'].split(os.pathsep))
device.RunShellCommand(
['pm', 'clear', test_case.browser_package_name],
check_return=True)
test_results_dict = OrderedDict({'version': 3, 'interrupted': False,
'num_failures_by_type': {}, 'tests': {}})
ret = test_case.run_tests('without_finch_seed', test_results_dict)
test_case.install_seed()
ret |= test_case.run_tests('with_finch_seed', test_results_dict)
test_results_dict['seconds_since_epoch'] = int(time.time())
test_results_dict['path_delimiter'] = '/'
with open(options.isolated_script_test_output, 'w') as json_out:
json_out.write(json.dumps(test_results_dict, indent=4))
# Return zero exit code if tests pass
return ret
def main_compile_targets(args):
json.dump([], args.output)
if __name__ == '__main__':
if 'compile_targets' in sys.argv:
funcs = {
'run': None,
'compile_targets': main_compile_targets,
}
sys.exit(common.run_script(sys.argv[1:], funcs))
sys.exit(main(sys.argv[1:]))