blob: bddd5fd85e650d651ec663d1a871ce973c512ae2 [file] [log] [blame]
# 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 <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()
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,
)