| #!/usr/bin/env python3 |
| # Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Contains utilities to create black box tests and run them.""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import argparse |
| import importlib |
| import logging |
| import random |
| import socket |
| import sys |
| import unittest |
| |
| from cobalt.black_box_tests import black_box_cobalt_runner |
| from cobalt.black_box_tests.proxy_server import ProxyServer |
| from starboard.tools import abstract_launcher |
| from starboard.tools import build |
| from starboard.tools import command_line |
| from starboard.tools import log_level |
| |
| _DISABLED_BLACKBOXTEST_CONFIGS = [ |
| 'android-arm/devel', |
| 'android-arm64/devel', |
| 'android-x86/devel', |
| 'evergreen-arm/devel', |
| 'evergreen-x64/devel', |
| 'raspi-0/devel', |
| ] |
| |
| _EVERGREEN_COMPATIBLE_CONFIGS = [ |
| # TODO(b/283788059): enable when there are GitHub jobs to run integration |
| # and Black Box Tests on evergreen-arm-hardfp. |
| #'evergreen-arm/devel', |
| # TODO(b/283144901): enable when the Starboard 16 binaries are released for |
| # Evergreen. |
| #'evergreen-x64/devel', |
| ] |
| |
| _PORT_SELECTION_RETRY_LIMIT = 10 |
| _PORT_SELECTION_RANGE = [5000, 7000] |
| # List of blocked ports. |
| _RESTRICTED_PORTS = [6000, 6665, 6666, 6667, 6668, 6669, 6697] |
| _SERVER_EXIT_TIMEOUT_SECONDS = 30 |
| _SOCKET_SUCCESS = 0 |
| # These tests can only be run on platforms whose app launcher can send suspend/ |
| # resume signals. |
| _TESTS_NEEDING_SYSTEM_SIGNAL = [ |
| 'cancel_sync_loads_when_suspended', |
| 'pointer_test', |
| 'preload_font', |
| 'preload_visibility', |
| 'preload_launch_parameter', |
| # TODO(b/254502632): Investigate the cause of the flakiness from this test. |
| # 'signal_handler_doesnt_crash', |
| 'suspend_visibility', |
| 'timer_hit_after_preload', |
| 'timer_hit_in_preload', |
| ] |
| # These tests only need app launchers with webdriver. |
| _TESTS_NO_SIGNAL = [ |
| 'allow_eval', |
| 'compression_test', |
| 'default_site_can_load', |
| 'disable_eval_with_csp', |
| 'h5vcc_storage_write_verify_test', |
| 'http_cache', |
| 'persistent_cookie', |
| 'scroll', |
| 'service_worker_add_to_cache_test', |
| 'service_worker_cache_keys_test', |
| 'service_worker_controller_activation_test', |
| 'service_worker_get_registrations_test', |
| 'service_worker_fetch_main_resource_test', |
| 'service_worker_fetch_test', |
| 'service_worker_message_test', |
| 'service_worker_post_message_test', |
| 'service_worker_test', |
| 'service_worker_persist_test', |
| 'soft_mic_platform_service_test', |
| 'telemetry_test', |
| 'text_encoding_test', |
| 'wasm_basic_test', |
| 'web_debugger', |
| 'web_worker_test', |
| 'worker_csp_test', |
| 'worker_load_test', |
| 'worker_post_message_test', |
| ] |
| # These are very different and require a custom config + proxy |
| _WPT_TESTS = [ |
| 'web_platform_tests', |
| ] |
| # These tests can only be run on platforms whose app launcher can send deep |
| # links. |
| _TESTS_NEEDING_DEEP_LINK = [ |
| 'deep_links', |
| ] |
| # These tests can only run on Evergreen-compatible platforms. |
| _TESTS_EVERGREEN_END_TO_END = [ |
| 'evergreen_verify_qa_channel_update_test', |
| ] |
| # Location of test files. |
| _TEST_DIR_PATH = 'cobalt.black_box_tests.tests.' |
| |
| _LAUNCH_TARGET = 'cobalt' |
| |
| # Platform configuration and device information parameters. |
| _launcher_params = None |
| # Binding address used to create the test server. |
| _server_binding_address = None |
| # Port used to create the web platform test http server. |
| _wpt_http_port = None |
| |
| |
| class BlackBoxTestCase(unittest.TestCase): |
| """Base class for Cobalt black box test cases.""" |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self.launcher_params = _launcher_params |
| self.platform_config = build.GetPlatformConfig(_launcher_params.platform) |
| self.cobalt_config = self.platform_config.GetApplicationConfiguration( |
| 'cobalt') |
| |
| @classmethod |
| def setUpClass(cls): |
| super(BlackBoxTestCase, cls).setUpClass() |
| logging.info('\n\n\n%s\n\n', '=' * 40) |
| logging.info('Running %s', cls.__name__) |
| |
| @classmethod |
| def tearDownClass(cls): |
| super(BlackBoxTestCase, cls).tearDownClass() |
| logging.info('Done %s', cls.__name__) |
| |
| def CreateCobaltRunner(self, url=None, target_params=None, **kwargs): |
| all_target_params = list(target_params) if target_params else [] |
| if _launcher_params.target_params is not None: |
| all_target_params += _launcher_params.target_params |
| |
| return black_box_cobalt_runner.BlackBoxCobaltRunner( |
| launcher_params=_launcher_params, |
| url=url, |
| target_params=all_target_params, |
| **kwargs) |
| |
| def GetBindingAddress(self): |
| return _server_binding_address |
| |
| def GetWptHttpPort(self): |
| return _wpt_http_port |
| |
| |
| def LoadTests(launcher_params, test_set): |
| launcher = abstract_launcher.LauncherFactory( |
| launcher_params.platform, |
| _LAUNCH_TARGET, |
| launcher_params.config, |
| device_id=launcher_params.device_id, |
| target_params=None, |
| output_file=None, |
| out_directory=launcher_params.out_directory, |
| loader_platform=launcher_params.loader_platform, |
| loader_config=launcher_params.loader_config, |
| loader_out_directory=launcher_params.loader_out_directory) |
| |
| test_targets = [] |
| |
| if test_set in ['all', 'blackbox']: |
| test_targets = _TESTS_NO_SIGNAL |
| |
| if launcher.SupportsSuspendResume(): |
| test_targets += _TESTS_NEEDING_SYSTEM_SIGNAL |
| |
| if launcher.SupportsDeepLink(): |
| test_targets += _TESTS_NEEDING_DEEP_LINK |
| |
| if test_set in ['all', 'wpt']: |
| test_targets += _WPT_TESTS |
| |
| test_suite = unittest.TestSuite() |
| for test in test_targets: |
| test_suite.addTest(unittest.TestLoader().loadTestsFromModule( |
| importlib.import_module(_TEST_DIR_PATH + test))) |
| return test_suite |
| |
| |
| def LoadEvergreenEndToEndTests(launcher_params): |
| launcher = abstract_launcher.LauncherFactory( # pylint: disable=unused-variable |
| launcher_params.platform, |
| _LAUNCH_TARGET, |
| launcher_params.config, |
| device_id=launcher_params.device_id, |
| target_params=None, |
| output_file=None, |
| out_directory=launcher_params.out_directory, |
| loader_platform=launcher_params.loader_platform, |
| loader_config=launcher_params.loader_config, |
| loader_out_directory=launcher_params.loader_out_directory, |
| # The more lightweight elf_loader_sandbox can't be used since it has no |
| # knowledge of updates or installations. |
| loader_target='loader_app') |
| |
| test_targets = _TESTS_EVERGREEN_END_TO_END |
| |
| test_suite = unittest.TestSuite() |
| for test in test_targets: |
| test_suite.addTest(unittest.TestLoader().loadTestsFromModule( |
| importlib.import_module(_TEST_DIR_PATH + test))) |
| return test_suite |
| |
| |
| class BlackBoxTests(object): |
| """Helper class to run all black box tests and return results.""" |
| |
| def __init__(self, args): |
| |
| self.args = args |
| |
| #TODO(b/137905502): These globals should be refactored |
| # Setup global variables used by test cases. |
| global _launcher_params |
| _launcher_params = command_line.CreateLauncherParams() |
| # Keep other modules from seeing these args. |
| sys.argv = sys.argv[:1] |
| global _server_binding_address |
| _server_binding_address = args.server_binding_address |
| |
| # Port used to create the web platform test http server. If not specified, |
| # a random free port is used. |
| global _wpt_http_port |
| _wpt_http_port = args.wpt_http_port or str( |
| self.GetUnusedPort([_server_binding_address])) |
| |
| # Proxy is only needed for WPT |
| self.use_proxy = args.test_set in ['all', 'wpt'] |
| |
| # TODO: Remove generation of --dev_servers_listen_ip once executable will |
| # be able to bind correctly with incomplete support of IPv6 |
| if args.device_id and IsValidIpAddress(args.device_id): |
| _launcher_params.target_params.append( |
| f'--dev_servers_listen_ip={args.device_id}') |
| elif IsValidIpAddress(_server_binding_address): |
| _launcher_params.target_params.append( |
| f'--dev_servers_listen_ip={_server_binding_address}') |
| _launcher_params.target_params.append( |
| f'--web-platform-test-server=http://web-platform.test:{_wpt_http_port}') |
| |
| # Port used to create the proxy server. If not specified, a random free |
| # port is used. |
| if self.use_proxy: |
| self.proxy_port = args.proxy_port or str( |
| self.GetUnusedPort([_server_binding_address])) |
| proxy_address = args.proxy_address or _server_binding_address |
| _launcher_params.target_params.append( |
| f'--proxy={proxy_address}:{self.proxy_port}') |
| |
| self.device_ips = args.device_ips |
| |
| # Test domains used in web platform tests to be resolved to the server |
| # binding address. |
| hosts = [ |
| 'web-platform.test', 'www.web-platform.test', 'www1.web-platform.test', |
| 'www2.web-platform.test', 'xn--n8j6ds53lwwkrqhv28a.web-platform.test', |
| 'xn--lve-6lad.web-platform.test' |
| ] |
| self.host_resolve_map = {host: _server_binding_address for host in hosts} |
| |
| def Run(self): |
| if self.use_proxy and self.proxy_port == '-1': |
| return 1 |
| |
| run_cobalt_tests = True |
| run_evergreen_tests = False |
| launch_config = f'{_launcher_params.platform}/{_launcher_params.config}' |
| # TODO(b/135549281): Configuring this in Python is superfluous, the on/off |
| # flags can be in Github Actions code |
| if launch_config in _DISABLED_BLACKBOXTEST_CONFIGS: |
| run_cobalt_tests = False |
| logging.warning( |
| 'Cobalt blackbox tests disabled for platform:%s config:%s', |
| _launcher_params.platform, _launcher_params.config) |
| |
| if launch_config in _EVERGREEN_COMPATIBLE_CONFIGS: |
| run_evergreen_tests = self.args.test_set in ['all', 'evergreen'] |
| |
| if not (run_cobalt_tests or run_evergreen_tests): |
| return 0 |
| |
| def LoadAndRunTests(): |
| if self.args.test_name: |
| suite = unittest.TestLoader().loadTestsFromName(_TEST_DIR_PATH + |
| self.args.test_name) |
| return_code = not unittest.TextTestRunner( |
| verbosity=2, stream=sys.stdout).run(suite).wasSuccessful() |
| return return_code |
| else: |
| cobalt_tests_return_code = 0 |
| if run_cobalt_tests: |
| suite = LoadTests(_launcher_params, self.args.test_set) |
| # Using verbosity=2 to log individual test function names and results. |
| cobalt_tests_return_code = not unittest.TextTestRunner( |
| verbosity=2, stream=sys.stdout).run(suite).wasSuccessful() |
| |
| evergreen_tests_return_code = 0 |
| if run_evergreen_tests: |
| suite = LoadEvergreenEndToEndTests(_launcher_params) |
| evergreen_tests_return_code = not unittest.TextTestRunner( |
| verbosity=2, stream=sys.stdout).run(suite).wasSuccessful() |
| |
| return cobalt_tests_return_code or evergreen_tests_return_code |
| |
| if self.use_proxy: |
| logging.info('Using proxy port: %s', self.proxy_port) |
| with ProxyServer( |
| port=self.proxy_port, |
| host_resolve_map=self.host_resolve_map, |
| client_ips=self.args.device_ips): |
| return LoadAndRunTests() |
| else: |
| return LoadAndRunTests() |
| |
| def GetUnusedPort(self, addresses): |
| """Find a free port on the list of addresses by pinging with sockets.""" |
| |
| if not addresses: |
| logging.error('Can not find unused port on invalid addresses.') |
| return -1 |
| |
| socks = [] |
| for address in addresses: |
| socks.append((address, socket.socket(socket.AF_INET, socket.SOCK_STREAM))) |
| try: |
| for _ in range(_PORT_SELECTION_RETRY_LIMIT): |
| port = random.choice([ |
| i for i in range(_PORT_SELECTION_RANGE[0], _PORT_SELECTION_RANGE[1]) |
| if i not in _RESTRICTED_PORTS |
| ]) |
| unused = True |
| for sock in socks: |
| result = sock[1].connect_ex((sock[0], port)) |
| if result == _SOCKET_SUCCESS: |
| unused = False |
| break |
| if unused: |
| return port |
| logging.error('Can not find unused port on addresses within %s attempts.', |
| _PORT_SELECTION_RETRY_LIMIT) |
| return -1 |
| finally: |
| for sock in socks: |
| sock[1].close() |
| |
| |
| def IsValidIpAddress(address): |
| """Checks if address is valid IP address.""" |
| return IsValidIpv4Address(address) or IsValidIpv6Address(address) |
| |
| |
| def IsValidIpv4Address(address): |
| """Checks if address is valid IPv4 address.""" |
| try: |
| socket.inet_pton(socket.AF_INET, address) |
| return True |
| except socket.error: # not a valid address |
| return False |
| |
| |
| def IsValidIpv6Address(address): |
| """Checks if address is valid IPv6 address.""" |
| try: |
| socket.inet_pton(socket.AF_INET6, address) |
| return True |
| except socket.error: # not a valid address |
| return False |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('-v', '--verbose', required=False, action='store_true') |
| parser.add_argument( |
| '--server_binding_address', |
| default='127.0.0.1', |
| help='Binding address used to create the test server.') |
| parser.add_argument( |
| '--proxy_address', |
| default=None, |
| help=('Address to the proxy server that all black box' |
| 'tests are run through. If not specified, the' |
| 'server binding address is used.')) |
| parser.add_argument( |
| '--proxy_port', |
| default=None, |
| help=('Port used to create the proxy server that all' |
| 'black box tests are run through. If not' |
| 'specified, a random free port is used.')) |
| parser.add_argument( |
| '--test_name', |
| default=None, |
| help=('Name of test to be run. If not specified, all ' |
| 'tests are run.')) |
| parser.add_argument( |
| '--wpt_http_port', |
| default=None, |
| help=('Port used to create the web platform test http' |
| 'server. If not specified, a random free port is' |
| 'used.')) |
| parser.add_argument( |
| '--device_id', |
| default=None, |
| help=('ID of test device to connect. If specified, it will be passed ' |
| 'as --dev_servers_listen_ip param on the test device.')) |
| parser.add_argument( |
| '--device_ips', |
| default=None, |
| nargs='*', |
| help=('IPs of test devices that will be allowed to connect. If not ' |
| 'specified, all IPs will be allowed to connect.')) |
| parser.add_argument( |
| '--test_set', |
| choices=['all', 'wpt', 'blackbox', 'evergreen'], |
| default='all') |
| args, _ = parser.parse_known_args() |
| |
| log_level.InitializeLogging(args) |
| |
| test_object = BlackBoxTests(args) |
| sys.exit(test_object.Run()) |
| |
| |
| if __name__ == '__main__': |
| # Running this script on the command line and importing this file are |
| # different and create two modules. |
| # Import this module to ensure we are using the same module as the tests to |
| # make module-owned variables like launcher_param accessible to the tests. |
| main_module = importlib.import_module( |
| 'cobalt.black_box_tests.black_box_tests') |
| sys.exit(main_module.main()) |