import argparse
import contextlib
import json
import logging
import os
import posixpath
import re
import shutil
import subprocess
import sys
import tempfile
import time
from collections import OrderedDict
from PIL import Image
SRC_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
PAR_DIR = os.path.join(SRC_DIR, 'testing')
OUT_DIR = os.path.join(SRC_DIR, 'out', 'Release')
BLINK_DIR = os.path.join(SRC_DIR, 'third_party', 'blink')
BLINK_TOOLS = os.path.join(BLINK_DIR, 'tools')
BLINK_WEB_TESTS = os.path.join(BLINK_DIR, 'web_tests')
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')
PYPROTO_LIB = os.path.join(OUT_DIR, 'pyproto', 'google')
WEBVIEW_VARIATIONS_PROTO = os.path.join(OUT_DIR, 'pyproto',
'android_webview', 'proto')
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)
if PYPROTO_LIB not in sys.path:
sys.path.append(PYPROTO_LIB)
if WEBVIEW_VARIATIONS_PROTO not in sys.path:
sys.path.append(WEBVIEW_VARIATIONS_PROTO)
sys.path.append(PAR_DIR)
if 'compile_targets' not in sys.argv:
import aw_variations_seed_pb2
import devil_chromium
from blinkpy.common.host import Host
from blinkpy.common.path_finder import PathFinder
from blinkpy.web_tests.models import test_failures
from blinkpy.web_tests.port.android import (
ANDROID_WEBVIEW, CHROME_ANDROID)
from blinkpy.w3c.wpt_results_processor import WPTResultsProcessor
from devil import devil_env
from devil.android import apk_helper
from devil.android import device_temp_file
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.device import local_device_environment
from pylib.local.emulator import avd
from py_utils.tempfile_ext import NamedTemporaryDirectory
from scripts import common
from skia_gold_infra.finch_skia_gold_properties import FinchSkiaGoldProperties
from skia_gold_infra import finch_skia_gold_session_manager
from skia_gold_infra import finch_skia_gold_utils
from run_wpt_tests import get_device
ANDROID_WEBLAYER = 'android_weblayer'
LOGCAT_TAG = 'finch_test_runner_py'
LOGCAT_FILTERS = [
'chromium:v',
'cr_*:v',
'DEBUG:I',
'StrictMode:D',
'WebView*:v',
'%s:I' % LOGCAT_TAG
]
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
TEST_CASES = {}
def _is_version_greater_than_or_equal(version1, version2):
version1_parts = version1.split('.')
version2_parts = version2.split('.')
for i in range(4):
comp = int(version1_parts[i]) - int(version2_parts[i])
if comp != 0:
return comp > 0
return True
def _merge_results_dicts(dict_to_merge, test_results_dict):
if 'actual' in dict_to_merge:
test_results_dict.update(dict_to_merge)
return
for key in dict_to_merge.keys():
_merge_results_dicts(dict_to_merge[key],
test_results_dict.setdefault(key, {}))
class FinchTestCase(common.BaseIsolatedScriptArgsAdapter):
def __init__(self, device):
self.host = Host()
self.fs = self.host.filesystem
self.path_finder = PathFinder(self.fs)
self.port = self.host.port_factory.get()
super(FinchTestCase, self).__init__()
self._add_extra_arguments()
self._parser = self._override_options(self._parser)
self._include_filename = None
self.layout_test_results_subdir = 'layout-test-results'
self._device = device
self.parse_args()
self.port.set_option_default('target', self.options.target)
self._browser_apk_helper = apk_helper.ToHelper(self.options.browser_apk)
self.browser_package_name = self._browser_apk_helper.GetPackageName()
self.browser_activity_name = (self.options.browser_activity_name or
self.default_browser_activity_name)
self.layout_test_results_subdir = None
self.test_specific_browser_args = []
if self.options.webview_provider_apk:
self.webview_provider_package_name = (
apk_helper.GetPackageName(self.options.webview_provider_apk))
self._skia_gold_corpus = 'finch-smoke-tests'
self._skia_gold_tmp_dir = None
self._skia_gold_session_manager = None
@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
@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 pixel_tests(self):
return []
@property
def default_browser_activity_name(self):
raise NotImplementedError
@property
def default_finch_seed_path(self):
raise NotImplementedError
@classmethod
def finch_seed_download_args(cls):
return []
def generate_test_output_args(self, output):
return ['--log-chromium=%s' % output]
def generate_test_filter_args(self, test_filter_str):
included_tests, excluded_tests = \
self._resolve_tests_from_isolate_filter(test_filter_str)
include_file, self._include_filename = self.fs.open_text_tempfile()
with include_file:
for test in included_tests:
include_file.write(test)
include_file.write('\n')
wpt_args = ['--include-file=%s' % self._include_filename]
for test in excluded_tests:
wpt_args.append('--exclude=%s' % test)
return wpt_args
def _override_options(self, base_parser):
"""Create a parser that overrides existing options.
`argument.ArgumentParser` can extend other parsers and override their
options, with the caveat that the child parser only inherits options
that the parent had at the time of the child's initialization.
See Also:
https://docs.python.org/3/library/argparse.html#parents
"""
parser = argparse.ArgumentParser(
parents=[base_parser],
conflict_handler='resolve',
epilog=('All unrecognized arguments are passed through '
"to wptrunner. Use '--wpt-help' to see wptrunner's usage."),
)
parser.add_argument(
'--isolated-script-test-repeat',
'--repeat',
'--gtest_repeat',
metavar='REPEAT',
type=int,
default=1,
help='Number of times to run the tests')
parser.add_argument(
'--isolated-script-test-launcher-retry-limit',
'--test-launcher-retry-limit',
'--retry-unexpected',
metavar='RETRIES',
type=int,
help=(
'Maximum number of times to rerun unexpectedly failed tests. '
'Defaults to 3 unless given an explicit list of tests to run.'))
parser.add_argument('--xvfb', action='store_true', default=True,
help=argparse.SUPPRESS)
return parser
def generate_test_repeat_args(self, repeat_count):
return ['--repeat=%d' % repeat_count]
def generate_test_launcher_retry_limit_args(self, retry_limit):
return ['--retry-unexpected=%d' % retry_limit]
def generate_sharding_args(self, total_shards, shard_index):
return ['--total-chunks=%d' % total_shards,
'--this-chunk=%d' % (shard_index + 1),
'--chunk-type=hash']
def clean_up_after_test_run(self):
if self._include_filename:
self.fs.remove(self._include_filename)
def new_seed_downloaded(self):
return True
def enable_internet(self):
self._device.RunShellCommand(
['settings', 'put', 'global', 'airplane_mode_on', '0'])
self._device.RunShellCommand(
['am', 'broadcast', '-a',
'android.intent.action.AIRPLANE_MODE'])
self._device.RunShellCommand(['svc', 'wifi', 'enable'])
self._device.RunShellCommand(['svc', 'data', 'enable'])
def disable_internet(self):
self._device.RunShellCommand(
['settings', 'put', 'global', 'airplane_mode_on', '1'])
self._device.RunShellCommand(
['am', 'broadcast', '-a',
'android.intent.action.AIRPLANE_MODE'])
@contextlib.contextmanager
def _archive_logcat(self, filename, endpoint_name):
start_point = 'START {}'.format(endpoint_name)
end_point = 'END {}'.format(endpoint_name)
with logcat_monitor.LogcatMonitor(
self._device.adb,
filter_specs=LOGCAT_FILTERS,
output_file=filename,
check_error=False):
try:
self._device.RunShellCommand(['log', '-p', 'i', '-t', LOGCAT_TAG,
start_point],
check_return=True)
yield
finally:
self._device.RunShellCommand(['log', '-p', 'i', '-t', LOGCAT_TAG,
end_point],
check_return=True)
def parse_args(self, args=None):
super(FinchTestCase, self).parse_args(args)
if (not self.options.finch_seed_path or
not os.path.exists(self.options.finch_seed_path)):
logger.warning('Could not find the finch seed passed '
'as the argument for --finch-seed-path. '
'Running tests on the default finch seed')
self.options.finch_seed_path = self.default_finch_seed_path
@property
def output_directory(self):
return self.path_finder.path_from_chromium_base('out',
self.options.target)
@property
def mojo_js_directory(self):
return self.fs.join(self.output_directory, 'gen')
@property
def wpt_output(self):
return self.options.isolated_script_test_output
@property
def _raw_log_path(self):
return self.fs.join(self.output_directory, 'finch-smoke-raw-events.log')
def __enter__(self):
self._device.EnableRoot()
self.disable_internet()
self._device.adb.Emu(['power', 'ac', 'on'])
self._skia_gold_tmp_dir = tempfile.mkdtemp()
self._skia_gold_session_manager = (
finch_skia_gold_session_manager.FinchSkiaGoldSessionManager(
self._skia_gold_tmp_dir, FinchSkiaGoldProperties(self.options)))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._skia_gold_session_manager = None
if self._skia_gold_tmp_dir:
shutil.rmtree(self._skia_gold_tmp_dir)
self._skia_gold_tmp_dir = None
@property
def rest_args(self):
unknown_args = super(FinchTestCase, self).rest_args
rest_args = list()
rest_args.extend(self.wpt_rest_args(unknown_args))
rest_args.extend([
'--webdriver-arg=--disable-build-check',
'--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',
'--test-type=testharness',
])
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])
return rest_args
@property
def wpt_binary(self):
default_wpt_binary = os.path.join(
common.SRC_DIR, "third_party", "wpt_tools", "wpt", "wpt")
return os.environ.get("WPT_BINARY", default_wpt_binary)
@property
def wpt_root_dir(self):
return self.path_finder.path_from_web_tests(
self.path_finder.wpt_prefix())
@property
def _wpt_run_args(self):
"""The start of a 'wpt run' command."""
return [
self.wpt_binary,
'--venv=%s' % self.path_finder.chromium_base(),
'--skip-venv-setup',
'run',
]
def process_and_upload_results(self, test_name_prefix):
processor = WPTResultsProcessor(
self.host.filesystem,
self.port,
artifacts_dir=os.path.join(os.path.dirname(self.wpt_output),
self.layout_test_results_subdir),
test_name_prefix=test_name_prefix)
processor.recreate_artifacts_dir()
with self.fs.open_text_file_for_reading(self._raw_log_path) as raw_logs:
for event in map(json.loads, raw_logs):
if event.get('action') != 'shutdown':
processor.process_event(event)
processor.process_results_json(self.wpt_output)
def wpt_rest_args(self, unknown_args):
rest_args = list(self._wpt_run_args)
rest_args.extend([
'--no-pause-after-test',
'--no-capture-stdio',
'--no-manifest-download',
'--tests=%s' % self.wpt_root_dir,
'--metadata=%s' % self.wpt_root_dir,
'--mojojs-path=%s' % self.mojo_js_directory,
'--log-raw=%s' % self._raw_log_path,
])
if self.options.default_exclude:
rest_args.extend(['--default-exclude'])
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.append(self.wpt_product_name())
for unknown_arg in unknown_args:
if unknown_arg == '--':
logger.warning(
'Unrecognized options will automatically fall through '
'to wptrunner.')
logger.warning(
"There is no need to use the end-of-options marker '--'.")
else:
rest_args.append(unknown_arg)
return rest_args
@classmethod
def add_common_arguments(cls, parser):
parser.add_argument('--test-case',
choices=TEST_CASES.keys(),
default='webview',
help='Name of test case')
parser.add_argument('--finch-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('--use-webview-installer-tool',
action='store_true',
help='Use the WebView installer tool.')
parser.add_argument('--fake-variations-channel',
action='store',
default='stable',
choices=['dev', 'canary', 'beta', 'stable'],
help='Finch seed release channel')
parser.add_argument('-j',
'--processes',
type=lambda processes: max(0, int(processes)),
default=1,
help='Number of emulator to run.')
common.add_emulator_args(parser)
FinchSkiaGoldProperties.AddCommandLineArguments(parser)
def _add_extra_arguments(self):
parser = self._parser
parser.add_argument(
'-t',
'--target',
default='Release',
help='Target build subdirectory under //out')
parser.add_argument(
'--default-exclude',
action='store_true',
help=('Only run the tests explicitly given in arguments '
'(can run no tests, which will exit with code 0)'))
parser.add_argument(
'-v',
'--verbose',
action='count',
default=0,
help='Increase verbosity')
self.add_product_specific_argument_groups(parser)
self.add_common_arguments(parser)
@classmethod
def add_product_specific_argument_groups(cls, _):
pass
def _compare_screenshots_with_baselines(self, all_pixel_tests_results_dict):
"""Compare pixel tests screenshots with baselines stored in skia gold
Args:
all_pixel_tests_results_dict: Results dictionary for all pixel tests
Returns:
1 if there was an error comparing images otherwise 0
"""
skia_gold_session = (
self._skia_gold_session_manager.GetSkiaGoldSession(
{'platform': 'android'}, self._skia_gold_corpus))
def _process_test_leaf(test_result_dict):
if ('artifacts' not in test_result_dict or
'actual_image' not in test_result_dict['artifacts']):
return 0
return_code = 0
artifacts_dict = test_result_dict['artifacts']
curr_artifacts = list(artifacts_dict.keys())
for artifact_name in curr_artifacts:
artifact_path = artifacts_dict[artifact_name][0]
status, error = skia_gold_session.RunComparison(
artifact_path,
os.path.join(os.path.dirname(self.wpt_output), artifact_path))
if status:
test_result_dict['actual'] = 'FAIL'
all_pixel_tests_results_dict['num_failures_by_type'].setdefault(
'FAIL', 0)
all_pixel_tests_results_dict['num_failures_by_type']['FAIL'] += 1
triage_link = finch_skia_gold_utils.log_skia_gold_status_code(
skia_gold_session, artifact_path, status, error)
if triage_link:
artifacts_dict['%s_triage_link' % artifact_name] = [triage_link]
return_code = 1
else:
test_result_dict['actual'] = 'PASS'
return return_code
def _process_tests(node):
return_code = 0
if 'actual' in node:
return _process_test_leaf(node)
for next_node in node.values():
return_code |= _process_tests(next_node)
return return_code
return _process_tests(all_pixel_tests_results_dict['tests'])
@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)
self._device.ClearApplicationState(
self.browser_package_name,
permissions=self._browser_apk_helper.GetPermissions())
tests_root_dir = posixpath.join(self._device.GetExternalStoragePath(),
'chromium_tests_root')
local_device_environment.place_nomedia_on_device(self._device,
tests_root_dir)
for test_file in self.pixel_tests:
self._device.RunShellCommand(
['mkdir', '-p',
posixpath.join(tests_root_dir,
'pixel_tests',
posixpath.dirname(test_file))],
check_return=True)
self._device.adb.Push(os.path.join(BLINK_WEB_TESTS, test_file),
posixpath.join(tests_root_dir,
'pixel_tests',
test_file))
yield
def browser_command_line_args(self):
return (['--vmodule=variations_field_trial_creator.cc=1', '--v=1',
'--disable-field-trial-config',
'--fake-variations-channel=%s' %
self.options.fake_variations_channel] +
self.test_specific_browser_args)
def run_tests(self, test_run_variation, all_test_results_dict,
extra_browser_args=None, check_seed_loaded=False):
"""Run browser test on test device
Args:
test_run_variation: Test run variation.
all_test_results_dict: Main results dictionary containing
results for all test variations.
extra_browser_args: Extra browser arguments.
check_seed_loaded: Check if the finch seed was loaded.
Returns:
The return code of all tests.
"""
isolate_root_dir = os.path.dirname(
self.options.isolated_script_test_output)
logcat_filename = '{}_{}_test_run_logcat.txt'.format(
self.product_name(), test_run_variation)
self.layout_test_results_subdir = ('%s_smoke_test_artifacts' %
test_run_variation)
self.test_specific_browser_args = extra_browser_args or []
with self._archive_logcat(os.path.join(isolate_root_dir, logcat_filename),
'{} {} tests'.format(self.product_name(),
test_run_variation)):
self.stop_browser()
if self.tests:
ret = super(FinchTestCase, self).run_test()
self.stop_browser()
command_line_file = '%s-command-line' % self.product_name()
with flag_changer.CustomCommandLineFlags(
self._device, command_line_file, self.browser_command_line_args()):
pixel_tests_results_dict, pixel_tests_ret = self._run_pixel_tests()
ret |= pixel_tests_ret
seed_loaded_result_dict = {'num_failures_by_type': {}, 'tests': {}}
test_harness_results_dict = {'num_failures_by_type': {}, 'tests': {}}
if os.path.exists(self.wpt_output):
self.process_and_upload_results(test_run_variation)
with open(self.wpt_output, 'r') as test_harness_results:
test_harness_results_dict = json.load(test_harness_results)
test_harness_results_dict['metadata'] = {'test_name_prefix':
test_run_variation}
with open(self.wpt_output, 'w+') as test_results_file:
json.dump(test_harness_results_dict, test_results_file)
final_logcat_path = os.path.join(isolate_root_dir,
self.layout_test_results_subdir,
logcat_filename)
os.makedirs(os.path.dirname(final_logcat_path), exist_ok=True)
shutil.move(os.path.join(isolate_root_dir, logcat_filename),
final_logcat_path)
if check_seed_loaded:
ret |= self._finch_seed_loaded(final_logcat_path, seed_loaded_result_dict)
for test_results_dict in (test_harness_results_dict,
pixel_tests_results_dict,
seed_loaded_result_dict):
_merge_results_dicts(
test_results_dict['tests'],
all_test_results_dict['tests'].setdefault(test_run_variation, {}))
for result, count in test_results_dict['num_failures_by_type'].items():
all_test_results_dict['num_failures_by_type'].setdefault(result, 0)
all_test_results_dict['num_failures_by_type'][result] += count
return ret
def _finch_seed_loaded(self, logcat_path, all_results_dict):
raise NotImplementedError
def _run_pixel_tests(self):
"""Run pixel tests on device
Returns:
A tuple containing a dictionary of pixel test results
and the skia gold status code.
"""
tests_root_dir = posixpath.join(
self._device.GetExternalStoragePath(),
'chromium_tests_root',
'pixel_tests')
pixel_tests_results_dict = {'tests':{}, 'num_failures_by_type': {}}
for test_file in self.pixel_tests:
logger.info('Running pixel test %s', test_file)
try:
url = 'file://{}'.format(
posixpath.join(tests_root_dir, test_file))
self.start_browser(url)
screenshot_artifact_relpath = os.path.join(
'pixel_tests_artifacts',
self.layout_test_results_subdir.replace('_artifacts', ''),
self.port.output_filename(test_file,
test_failures.FILENAME_SUFFIX_ACTUAL,
'.png'))
screenshot_artifact_abspath = os.path.join(
os.path.dirname(self.options.isolated_script_test_output),
screenshot_artifact_relpath)
self._device.TakeScreenshot(host_path=screenshot_artifact_abspath)
top_bar_height_factor = 0.2
navigation_bar_height_factor = 0.1
image = Image.open(screenshot_artifact_abspath)
width, height = image.size
cropped_image = image.crop(
(0,
int(height * top_bar_height_factor),
width,
int(height * (1 - navigation_bar_height_factor))))
image.close()
cropped_image.save(screenshot_artifact_abspath)
test_results_dict = pixel_tests_results_dict['tests']
for key in test_file.split('/'):
test_results_dict = test_results_dict.setdefault(key, {})
test_results_dict['actual'] = 'PASS'
test_results_dict['expected'] = 'PASS'
test_results_dict['artifacts'] = {
'actual_image': [screenshot_artifact_relpath]}
finally:
self.stop_browser()
return (pixel_tests_results_dict,
self._compare_screenshots_with_baselines(pixel_tests_results_dict))
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, url=None):
full_activity_name = '%s/%s' % (self.browser_package_name,
self.browser_activity_name)
logger.info('Starting activity %s', full_activity_name)
url = url or 'www.google.com'
self._device.RunShellCommand([
'am',
'start',
'-W',
'-n',
full_activity_name,
'-d',
url])
logger.info('Waiting 5 seconds')
time.sleep(5)
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:
The path to the new finch seed under the application data folder.
"""
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)
seed_path = posixpath.join(app_data_dir, 'local_variations_seed')
self._device.adb.Push(self.options.finch_seed_path, seed_path)
user_id = self._device.GetUidForPackage(self.browser_package_name)
self._device.RunShellCommand(['chown', user_id, seed_path], as_root=True)
return seed_path
class ChromeFinchTestCase(FinchTestCase):
@classmethod
def product_name(cls):
"""Returns name of product being tested"""
return 'chrome'
@property
def default_finch_seed_path(self):
return os.path.join(SRC_DIR, 'testing', 'scripts',
'variations_smoke_test_data',
'variations_seed_stable_chrome_android.json')
@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 pixel_tests(self):
return super(WebViewFinchTestCase, self).pixel_tests + [
'external/wpt/svg/render/reftests/blending-001.svg',
'external/wpt/svg/render/reftests/blending-svg-foreign-object.html',
'external/wpt/svg/render/reftests/filter-effects-on-pattern.html',
'external/wpt/svg/pservers/reftests/radialgradient-basic-002.svg',
]
def _finch_seed_loaded(self, logcat_path, all_results_dict):
"""Checks the logcat if the seed was loaded
Args:
logcat_path: Path to the logcat.
all_results_dict: Dictionary containing test results
Returns:
0 if the seed was loaded and experiments were loaded for finch seeds
other than the default seed. Otherwise it returns 1.
"""
with open(logcat_path, 'r') as logcat:
logcat_content = logcat.read()
seed_loaded = 'cr_VariationsUtils: Loaded seed with age' in logcat_content
logcat_relpath = os.path.relpath(logcat_path,
os.path.dirname(self.wpt_output))
seed_loaded_results_dict = (
all_results_dict['tests'].setdefault(
'check_seed_loaded',
{'expected': 'PASS',
'artifacts': {'logcat_path': [logcat_relpath]}}))
if seed_loaded:
logger.info('The finch seed was loaded by WebView')
seed_loaded_results_dict['actual'] = 'PASS'
else:
logger.error('The finch seed was not loaded by WebView')
seed_loaded_results_dict['actual'] = 'FAIL'
all_results_dict['num_failures_by_type']['FAIL'] = 1
if self.default_finch_seed_path != self.options.finch_seed_path:
webview_version = self._device.GetApplicationVersion(
self._device.GetWebViewProvider())
check_for_vlog = (webview_version and
_is_version_greater_than_or_equal(webview_version,
'110.0.5463.0'))
field_trial_check_name = 'check_for_logged_field_trials'
if check_for_vlog:
field_trials_loaded = (
'CreateTrialsFromSeed complete with seed.version='
in logcat_content)
field_trial_check_name = 'check_for_variations_field_trial_creator_logs'
expected_results = 'PASS'
logger.info("Checking for variations_field_trial_creator.cc logs "
"in the logcat")
else:
field_trials_loaded = ('Active field trial '
'"UMA-Uniformity-Trial-100-Percent" '
'in group "group_01"') in logcat_content
expected_results = 'PASS FAIL'
logger.info("Checking for the UMA uniformity trial in the logcat")
field_trials_loaded_results_dict = (
all_results_dict['tests'].setdefault(
field_trial_check_name,
{'expected': expected_results,
'artifacts': {'logcat_path': [logcat_relpath]}}))
if field_trials_loaded:
logger.info('Experiments were loaded from the finch seed by WebView')
field_trials_loaded_results_dict['actual'] = 'PASS'
else:
logger.error('Experiments were not loaded from '
'the finch seed by WebView')
field_trials_loaded_results_dict['actual'] = 'FAIL'
all_results_dict['num_failures_by_type'].setdefault('FAIL', 0)
all_results_dict['num_failures_by_type']['FAIL'] += 1
if 'FAIL' in expected_results:
return 0 if seed_loaded else 1
return 0 if seed_loaded and field_trials_loaded else 1
logger.warning('The default seed is being tested, '
'skipping checks for active field trials')
return 0 if seed_loaded else 1
@classmethod
def finch_seed_download_args(cls):
return [
'--finch-seed-expiration-age=0',
'--finch-seed-min-update-period=0',
'--finch-seed-min-download-period=0',
'--finch-seed-ignore-pending-download',
'--finch-seed-no-charging-requirement']
@property
def default_browser_activity_name(self):
return 'org.chromium.webview_shell.WebViewBrowserActivity'
@property
def default_finch_seed_path(self):
return os.path.join(SRC_DIR, 'testing', 'scripts',
'variations_smoke_test_data',
'webview_test_seed')
@classmethod
def add_product_specific_argument_groups(cls, parser):
installer_tool_group = parser.add_argument_group(
'WebView Installer tool arguments')
installer_tool_group.add_argument(
'--webview-installer-tool', type=os.path.realpath,
help='Path to the WebView installer tool')
installer_tool_group.add_argument(
'--chrome-version', '-V', type=str, default=None,
help='Chrome version to install with the WebView installer tool')
installer_tool_group.add_argument(
'--channel', '-c', help='Channel build of WebView to install',
choices=['dev', 'canary', 'beta', 'stable'], default=None)
installer_tool_group.add_argument(
'--milestone', '-M', help='Milestone build of WebView to install')
installer_tool_group.add_argument(
'--package', '-P', default=None,
help='Name of the WebView apk to install')
def new_seed_downloaded(self):
"""Checks if a new seed was downloaded
Returns:
True if a new seed was downloaded, otherwise False
"""
app_data_dir = posixpath.join(
self._device.GetApplicationDataDirectory(self.browser_package_name),
self.app_user_sub_dir())
remote_seed_path = posixpath.join(app_data_dir, 'variations_seed')
with NamedTemporaryDirectory() as tmp_dir:
current_seed_path = os.path.join(tmp_dir, 'current_seed')
self._device.adb.Pull(remote_seed_path, current_seed_path)
with open(current_seed_path, 'rb') as current_seed_obj, \
open(self.options.finch_seed_path, 'rb') as baseline_seed_obj:
current_seed_content = current_seed_obj.read()
baseline_seed_content = baseline_seed_obj.read()
current_seed = aw_variations_seed_pb2.AwVariationsSeed.FromString(
current_seed_content)
baseline_seed = aw_variations_seed_pb2.AwVariationsSeed.FromString(
baseline_seed_content)
shutil.copy(current_seed_path, os.path.join(OUT_DIR, 'final_seed'))
logger.info("Downloaded seed's signature: %s", current_seed.signature)
logger.info("Baseline seed's signature: %s", baseline_seed.signature)
return current_seed_content != baseline_seed_content
def browser_command_line_args(self):
return (super(WebViewFinchTestCase, self).browser_command_line_args() +
['--webview-verbose-logging'])
@contextlib.contextmanager
def install_apks(self):
"""Install apks for testing"""
with super(WebViewFinchTestCase, self).install_apks():
if self.options.use_webview_installer_tool:
install_webview = self._install_webview_with_tool()
else:
install_webview = webview_app.UseWebViewProvider(
self._device, self.options.webview_provider_apk)
with install_webview:
yield
@contextlib.contextmanager
def _install_webview_with_tool(self):
"""Install WebView with the WebView installer tool"""
original_webview_provider = self._device.GetWebViewProvider()
current_webview_provider = None
try:
cmd = [self.options.webview_installer_tool, '-vvv',
'--product', self.product_name()]
assert (self.options.chrome_version or
self.options.milestone or self.options.channel), (
'The --chrome-version, --milestone or --channel arguments must be '
'used when installing WebView with the WebView installer tool')
assert not(self.options.chrome_version and self.options.milestone), (
'The --chrome-version and --milestone arguments cannot be '
'used together')
if self.options.chrome_version:
cmd.extend(['--chrome-version', self.options.chrome_version])
elif self.options.milestone:
cmd.extend(['--milestone', self.options.milestone])
if self.options.channel:
cmd.extend(['--channel', self.options.channel])
if self.options.package:
cmd.extend(['--package', self.options.package])
exit_code = subprocess.call(cmd)
assert exit_code == 0, (
'The WebView installer tool failed to install WebView')
current_webview_provider = self._device.GetWebViewProvider()
yield
finally:
self._device.SetWebViewImplementation(original_webview_provider)
if current_webview_provider:
self._device.Uninstall(current_webview_provider)
def install_seed(self):
"""Install finch seed for testing
Returns:
None
"""
logcat_file = os.path.join(
os.path.dirname(self.options.isolated_script_test_output),
'install_seed_for_on_device.txt')
with self._archive_logcat(
logcat_file,
'install seed on device {}'.format(self._device.serial)):
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)
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'
@property
def default_finch_seed_path(self):
return os.path.join(SRC_DIR, 'testing', 'scripts',
'variations_smoke_test_data',
'variations_seed_stable_weblayer.json')
@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_common_arguments(parser)
parser.add_argument(
'--isolated-script-test-output', type=str,
required=False,
help='path to write test results JSON object to')
script_common.AddDeviceArguments(parser)
script_common.AddEnvironmentArguments(parser)
logging_common.AddLoggingArguments(parser)
for test_class in TEST_CASES.values():
test_class.add_product_specific_argument_groups(parser)
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)
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))
test_results_dict = OrderedDict({'version': 3, 'interrupted': False,
'num_failures_by_type': {}, 'tests': {}})
if test_case.product_name() == 'webview':
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,
check_seed_loaded=True)
test_case.enable_internet()
ret |= test_case.run_tests(
'extra_restart', test_results_dict,
extra_browser_args=test_case.finch_seed_download_args(),
check_seed_loaded=True)
ret |= test_case.run_tests(
'fetch_new_seed_restart', test_results_dict,
extra_browser_args=test_case.finch_seed_download_args(),
check_seed_loaded=True)
ret |= test_case.run_tests(
'load_new_seed_restart', test_results_dict,
extra_browser_args=test_case.finch_seed_download_args(),
check_seed_loaded=True)
test_case.disable_internet()
else:
installed_seed = test_case.install_seed()
if installed_seed:
extra_args = [f'--variations-test-seed-path={installed_seed}']
ret = test_case.run_tests('with_finch_seed', test_results_dict,
extra_browser_args=extra_args)
else:
ret = test_case.run_tests('with_finch_seed', test_results_dict)
device.ClearApplicationState(test_case.browser_package_name)
ret |= test_case.run_tests('without_finch_seed', test_results_dict)
test_results_dict['seconds_since_epoch'] = int(time.time())
test_results_dict['path_delimiter'] = '/'
with open(test_case.options.isolated_script_test_output, 'w') as json_out:
json_out.write(json.dumps(test_results_dict, indent=4))
if not test_case.new_seed_downloaded():
raise Exception('A new seed was not downloaded')
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:]))