# 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 range(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 range(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 range(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 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() 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, flavor=self.get_option('result_report_flavor'), sdk=host.build_version if host else None, )