| # Copyright (C) 2018-2019 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import logging |
| import traceback |
| |
| from webkitpy.common.version_name_map import VersionNameMap, PUBLIC_TABLE, INTERNAL_TABLE |
| from webkitpy.layout_tests.models.test_configuration import TestConfiguration |
| from webkitpy.port.darwin import DarwinPort |
| from webkitpy.port.simulator_process import SimulatorProcess |
| from webkitpy.results.upload import Upload |
| from webkitpy.xcode.device_type import DeviceType |
| from webkitpy.xcode.simulated_device import DeviceRequest, SimulatedDeviceManager |
| |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class DevicePort(DarwinPort): |
| |
| DEVICE_MANAGER = None |
| NO_DEVICE_MANAGER = 'No device manager found for port' |
| |
| def __init__(self, *args, **kwargs): |
| super(DevicePort, self).__init__(*args, **kwargs) |
| self._test_runner_process_constructor = SimulatorProcess |
| self._printing_cmd_line = False |
| |
| def driver_cmd_line_for_logging(self): |
| # Avoid creating/connecting to devices just for command line logging. |
| self._printing_cmd_line = True |
| result = super(DevicePort, self).driver_cmd_line_for_logging() |
| self._printing_cmd_line = False |
| return result |
| |
| def _generate_all_test_configurations(self): |
| configurations = [] |
| for build_type in self.ALL_BUILD_TYPES: |
| for architecture in self.ARCHITECTURES: |
| configurations.append(TestConfiguration(version=self.version_name(), architecture=architecture, build_type=build_type)) |
| return configurations |
| |
| def child_processes(self): |
| return int(self.get_option('child_processes')) |
| |
| def driver_name(self): |
| if self.get_option('driver_name'): |
| return self.get_option('driver_name') |
| if self.get_option('webkit_test_runner'): |
| return 'WebKitTestRunnerApp.app' |
| return 'DumpRenderTree.app' |
| |
| # A device is the target host for a specific worker number. |
| def target_host(self, worker_number=None): |
| if self._printing_cmd_line or worker_number is None: |
| return self.host |
| if self.DEVICE_MANAGER is None: |
| raise RuntimeError('No device manager for specified port') |
| if self.DEVICE_MANAGER.INITIALIZED_DEVICES is None: |
| raise RuntimeError('No initialized devices for testing') |
| return self.DEVICE_MANAGER.INITIALIZED_DEVICES[worker_number] |
| |
| def devices(self): |
| if self.DEVICE_MANAGER is None: |
| return [] |
| if self.DEVICE_MANAGER.INITIALIZED_DEVICES is None: |
| return [] |
| return self.DEVICE_MANAGER.INITIALIZED_DEVICES |
| |
| # Despite their names, these flags do not actually get passed all the way down to webkit-build. |
| def _build_driver_flags(self): |
| return ['--sdk', self.SDK] + (['ARCHS=%s' % self.architecture()] if self.architecture() else []) |
| |
| def _install(self): |
| if not self.get_option('install'): |
| _log.debug('Skipping installation') |
| return |
| |
| for i in xrange(self.child_processes()): |
| device = self.target_host(i) |
| _log.debug(u'Installing to {}'.format(device)) |
| # Without passing DYLD_LIBRARY_PATH, libWebCoreTestSupport cannot be loaded and DRT/WKTR will crash pre-launch, |
| # leaving a crash log which will be picked up in results. DYLD_FRAMEWORK_PATH is needed to prevent an early crash. |
| if not device.install_app(self._path_to_driver(), {'DYLD_LIBRARY_PATH': self._build_path(), 'DYLD_FRAMEWORK_PATH': self._build_path()}): |
| raise RuntimeError('Failed to install app {} on device {}'.format(self._path_to_driver(), device.udid)) |
| if not device.install_dylibs(self._build_path()): |
| raise RuntimeError('Failed to install dylibs at {} on device {}'.format(self._build_path(), device.udid)) |
| |
| def _device_type_with_version(self, device_type=None): |
| device_type = device_type if device_type else self.DEVICE_TYPE |
| return DeviceType( |
| hardware_family=device_type.hardware_family, |
| hardware_type=device_type.hardware_type, |
| software_version=self.device_version(), |
| software_variant=device_type.software_variant, |
| ) |
| |
| def default_child_processes(self, device_type=None): |
| if not self.DEVICE_MANAGER: |
| raise RuntimeError(self.NO_DEVICE_MANAGER) |
| |
| device_type = self._device_type_with_version(device_type) |
| if device_type not in self.DEVICE_TYPE: |
| return 0 |
| |
| if self.get_option('force'): |
| device_type.hardware_family = None |
| device_type.hardware_type = None |
| |
| return self.DEVICE_MANAGER.device_count_for_type( |
| self._device_type_with_version(device_type), |
| host=self.host, |
| use_booted_simulator=not self.get_option('dedicated_simulators', False), |
| ) |
| |
| def max_child_processes(self, device_type=None): |
| result = self.default_child_processes(device_type=device_type) |
| if result and self.DEVICE_MANAGER == SimulatedDeviceManager: |
| return super(DevicePort, self).max_child_processes(device_type=None) |
| return result |
| |
| def supported_device_types(self): |
| types = set() |
| for device in self.DEVICE_MANAGER.available_devices(host=self.host): |
| if self.DEVICE_MANAGER == SimulatedDeviceManager and not device.platform_device.is_booted_or_booting(): |
| continue |
| if device.device_type in self.DEVICE_TYPE: |
| types.add(device.device_type) |
| if types and not self.get_option('dedicated_simulators', False): |
| |
| def sorted_by_default_device_type(type): |
| try: |
| return self.DEFAULT_DEVICE_TYPES.index(type) |
| except ValueError: |
| return len(self.DEFAULT_DEVICE_TYPES) |
| |
| return sorted(types, key=sorted_by_default_device_type) |
| |
| return self.DEFAULT_DEVICE_TYPES or [self.DEVICE_TYPE] |
| |
| def setup_test_run(self, device_type=None): |
| if not self.DEVICE_MANAGER: |
| raise RuntimeError(self.NO_DEVICE_MANAGER) |
| |
| device_type = self._device_type_with_version(device_type) |
| _log.debug(u'\nCreating devices for {}'.format(device_type)) |
| |
| request = DeviceRequest( |
| device_type, |
| use_booted_simulator=not self.get_option('dedicated_simulators', False), |
| use_existing_simulator=False, |
| allow_incomplete_match=self.get_option('force'), |
| ) |
| self.DEVICE_MANAGER.initialize_devices( |
| [request] * self.child_processes(), |
| self.host, |
| layout_test_dir=self.layout_tests_dir(), |
| pin=self.get_option('pin', None), |
| use_nfs=self.get_option('use_nfs', True), |
| reboot=self.get_option('reboot', False), |
| ) |
| |
| if not self.devices(): |
| raise RuntimeError('No devices are available for testing') |
| if len(self.DEVICE_MANAGER.INITIALIZED_DEVICES) < self.child_processes(): |
| raise RuntimeError('To few connected devices for {} processes'.format(self.child_processes())) |
| |
| self._install() |
| |
| for i in xrange(self.child_processes()): |
| host = self.target_host(i) |
| host.prepare_for_testing( |
| self.ports_to_forward(), |
| self.app_identifier_from_bundle(self._path_to_driver()), |
| self.layout_tests_dir(), |
| ) |
| self._crash_logs_to_skip_for_host[host] = host.filesystem.files_under(self.path_to_crash_logs()) |
| |
| def clean_up_test_run(self): |
| super(DevicePort, self).clean_up_test_run() |
| |
| # Best effort to let every device teardown before throwing any exceptions here. |
| # Failure to teardown devices can leave things in a bad state. |
| exception_list = [] |
| for i in xrange(self.child_processes()): |
| device = self.target_host(i) |
| if not device: |
| continue |
| try: |
| device.finished_testing() |
| except BaseException as e: |
| trace = traceback.format_exc() |
| if isinstance(e, Exception): |
| exception_list.append([e, trace]) |
| else: |
| exception_list.append([Exception(u'Exception while tearing down {}'.format(device)), trace]) |
| |
| if len(exception_list) == 1: |
| raise |
| if len(exception_list) > 1: |
| print('\n') |
| for exception in exception_list: |
| _log.error('{} raised: {}'.format(exception[0].__class__.__name__, exception[0])) |
| _log.error(exception[1]) |
| _log.error('--------------------------------------------------') |
| |
| raise RuntimeError('Multiple failures when teardown devices') |
| |
| def did_spawn_worker(self, worker_number): |
| super(DevicePort, self).did_spawn_worker(worker_number) |
| |
| self.target_host(worker_number).release_worker_resources() |
| |
| def setup_environ_for_server(self, server_name=None): |
| env = super(DevicePort, self).setup_environ_for_server(server_name) |
| if server_name == self.driver_name() and self.get_option('guard_malloc'): |
| self._append_value_colon_separated(env, 'DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib') |
| self._append_value_colon_separated(env, '__XPC_DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib') |
| env['XML_CATALOG_FILES'] = '' # work around missing /etc/catalog <rdar://problem/4292995> |
| return env |
| |
| def device_version(self): |
| raise NotImplementedError |
| |
| def configuration_for_upload(self, host=None): |
| configuration = self.test_configuration() |
| |
| device_type = host.device_type if host else self.DEVICE_TYPE |
| model = device_type.hardware_family |
| if model and device_type.hardware_type: |
| model += u' {}'.format(device_type.hardware_type) |
| |
| version = self.device_version() |
| version_name = None |
| for table in [INTERNAL_TABLE, PUBLIC_TABLE]: |
| version_name = VersionNameMap.map(self.host.platform).to_name(version, platform=device_type.software_variant.lower(), table=table) |
| if version_name: |
| break |
| |
| if self.get_option('guard_malloc'): |
| style = 'guard-malloc' |
| elif self._config.asan: |
| style = 'asan' |
| else: |
| style = configuration.build_type |
| |
| return Upload.create_configuration( |
| platform=device_type.software_variant.lower(), |
| is_simulator=self.DEVICE_MANAGER == SimulatedDeviceManager, |
| version=str(version), |
| version_name=version_name, |
| architecture=configuration.architecture, |
| style=style, |
| model=model, |
| sdk=host.build_version if host else None, |
| ) |