| # Copyright (C) 2014-2016 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 itertools |
| import logging |
| import os |
| import re |
| import shutil |
| import subprocess |
| import time |
| |
| from webkitpy.common.memoized import memoized |
| from webkitpy.common.system.crashlogs import CrashLogs |
| from webkitpy.common.system.executive import ScriptError |
| from webkitpy.layout_tests.models.test_configuration import TestConfiguration |
| from webkitpy.port import config as port_config |
| from webkitpy.port import driver, image_diff |
| from webkitpy.port.apple import ApplePort |
| from webkitpy.port.base import Port |
| from webkitpy.xcode.simulator import Simulator, Runtime, DeviceType |
| |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class IOSPort(ApplePort): |
| port_name = "ios" |
| |
| ARCHITECTURES = ['armv7', 'armv7s', 'arm64'] |
| DEFAULT_ARCHITECTURE = 'arm64' |
| VERSION_FALLBACK_ORDER = ['ios-7', 'ios-8', 'ios-9', 'ios-10'] |
| |
| @classmethod |
| def determine_full_port_name(cls, host, options, port_name): |
| if port_name == cls.port_name: |
| iphoneos_sdk_version = host.platform.xcode_sdk_version('iphoneos') |
| if not iphoneos_sdk_version: |
| raise Exception("Please install the iOS SDK.") |
| major_version_number = iphoneos_sdk_version.split('.')[0] |
| port_name = port_name + '-' + major_version_number |
| return port_name |
| |
| # Despite their names, these flags do not actually get passed all the way down to webkit-build. |
| def _build_driver_flags(self): |
| return ['--sdk', 'iphoneos'] + (['ARCHS=%s' % self.architecture()] if self.architecture() else []) |
| |
| def operating_system(self): |
| return 'ios' |
| |
| |
| class IOSSimulatorPort(ApplePort): |
| port_name = "ios-simulator" |
| |
| FUTURE_VERSION = 'future' |
| ARCHITECTURES = ['x86_64', 'x86'] |
| DEFAULT_ARCHITECTURE = 'x86_64' |
| |
| DEFAULT_DEVICE_CLASS = 'iphone' |
| CUSTOM_DEVICE_CLASSES = ['ipad'] |
| |
| SIMULATOR_BUNDLE_ID = 'com.apple.iphonesimulator' |
| relay_name = 'LayoutTestRelay' |
| SIMULATOR_DIRECTORY = "/tmp/WebKitTestingSimulators/" |
| LSREGISTER_PATH = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister" |
| PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 100 |
| |
| DEVICE_CLASS_MAP = { |
| 'x86_64': { |
| 'iphone': 'iPhone 5s', |
| 'ipad': 'iPad Air' |
| }, |
| 'x86': { |
| 'iphone': 'iPhone 5', |
| 'ipad': 'iPad Retina' |
| }, |
| } |
| |
| def __init__(self, host, port_name, **kwargs): |
| super(IOSSimulatorPort, self).__init__(host, port_name, **kwargs) |
| |
| optional_device_class = self.get_option('device_class') |
| self._printing_cmd_line = False |
| self._device_class = optional_device_class if optional_device_class else self.DEFAULT_DEVICE_CLASS |
| _log.debug('IOSSimulatorPort _device_class is %s', self._device_class) |
| |
| 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' |
| |
| def driver_cmd_line_for_logging(self): |
| # Avoid spinning up devices just for logging the commandline. |
| self._printing_cmd_line = True |
| result = super(IOSSimulatorPort, self).driver_cmd_line_for_logging() |
| self._printing_cmd_line = False |
| return result |
| |
| @property |
| @memoized |
| def simulator_runtime(self): |
| runtime_identifier = self.get_option('runtime') |
| if runtime_identifier: |
| runtime = Runtime.from_identifier(runtime_identifier) |
| else: |
| runtime = Runtime.from_version_string(self.host.platform.xcode_sdk_version('iphonesimulator')) |
| return runtime |
| |
| def simulator_device_type(self): |
| device_type_identifier = self.get_option('device_type') |
| if device_type_identifier: |
| _log.debug('simulator_device_type for device identifier %s', device_type_identifier) |
| device_type = DeviceType.from_identifier(device_type_identifier) |
| else: |
| _log.debug('simulator_device_type for device %s', self._device_class) |
| device_name = self.DEVICE_CLASS_MAP[self.architecture()][self._device_class] |
| if not device_name: |
| raise Exception('Failed to find device for architecture {} and device class {}'.format(self.architecture()), self._device_class) |
| device_type = DeviceType.from_name(device_name) |
| return device_type |
| |
| @property |
| @memoized |
| def relay_path(self): |
| if self._root_was_set: |
| path = self._filesystem.abspath(self.get_option('root')) |
| else: |
| mac_config = port_config.Config(self._executive, self._filesystem, 'mac') |
| path = mac_config.build_directory(self.get_option('configuration')) |
| return self._filesystem.join(path, self.relay_name) |
| |
| @memoized |
| def child_processes(self): |
| return int(self.get_option('child_processes')) |
| |
| @memoized |
| def default_child_processes(self): |
| """Return the number of Simulators instances to use for this port.""" |
| best_child_process_count_for_cpu = self._executive.cpu_count() / 2 |
| system_process_count_limit = int(subprocess.check_output(["ulimit", "-u"]).strip()) |
| current_process_count = len(subprocess.check_output(["ps", "aux"]).strip().split('\n')) |
| _log.debug('Process limit: %d, current #processes: %d' % (system_process_count_limit, current_process_count)) |
| maximum_simulator_count_on_this_system = (system_process_count_limit - current_process_count) // self.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE |
| # FIXME: We should also take into account the available RAM. |
| |
| if (maximum_simulator_count_on_this_system < best_child_process_count_for_cpu): |
| _log.warn("This machine could support %s simulators, but is only configured for %s." |
| % (best_child_process_count_for_cpu, maximum_simulator_count_on_this_system)) |
| _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.') |
| |
| if maximum_simulator_count_on_this_system == 0: |
| maximum_simulator_count_on_this_system = 1 |
| |
| return min(maximum_simulator_count_on_this_system, best_child_process_count_for_cpu) |
| |
| def _check_relay(self): |
| if not self._filesystem.exists(self.relay_path): |
| _log.error("%s was not found at %s" % (self.relay_name, self.relay_path)) |
| return False |
| return True |
| |
| def _check_port_build(self): |
| if not self._root_was_set and self.get_option('build') and not self._build_relay(): |
| return False |
| if not self._check_relay(): |
| return False |
| return True |
| |
| def _build_relay(self): |
| environment = self.host.copy_current_environment() |
| environment.disable_gcc_smartquotes() |
| env = environment.to_dictionary() |
| |
| try: |
| # FIXME: We should be passing _arguments_for_configuration(), which respects build configuration and port, |
| # instead of hardcoding --ios-simulator. |
| self._run_script("build-layouttestrelay", args=["--ios-simulator"], env=env) |
| except ScriptError, e: |
| _log.error(e.message_with_output(output_limit=None)) |
| return False |
| return True |
| |
| def _build_driver(self): |
| built_tool = super(IOSSimulatorPort, self)._build_driver() |
| built_relay = self._build_relay() |
| return built_tool and built_relay |
| |
| def _build_driver_flags(self): |
| archs = ['ARCHS=i386'] if self.architecture() == 'x86' else [] |
| sdk = ['--sdk', 'iphonesimulator'] |
| return archs + sdk |
| |
| 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, architecture=architecture, build_type=build_type)) |
| return configurations |
| |
| def _driver_class(self): |
| return driver.IOSSimulatorDriver |
| |
| def default_baseline_search_path(self): |
| if self.get_option('webkit_test_runner'): |
| fallback_names = [self._wk2_port_name()] + [self.port_name] + ['wk2'] |
| else: |
| fallback_names = [self.port_name + '-wk1'] + [self.port_name] |
| |
| return map(self._webkit_baseline_path, fallback_names) |
| |
| def _port_specific_expectations_files(self): |
| return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path()])) |
| |
| def _set_device_class(self, device_class): |
| self._device_class = device_class if device_class else self.DEFAULT_DEVICE_CLASS |
| |
| def _create_simulators(self): |
| if (self.default_child_processes() < self.child_processes()): |
| _log.warn("You have specified very high value({0}) for --child-processes".format(self.child_processes())) |
| _log.warn("maximum child-processes which can be supported on this system are: {0}".format(self.default_child_processes())) |
| _log.warn("This is very likely to fail.") |
| |
| self._createSimulatorApps() |
| |
| for i in xrange(self.child_processes()): |
| self._create_device(i) |
| |
| for i in xrange(self.child_processes()): |
| device_udid = self._testing_device(i).udid |
| Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN) |
| Simulator.reset_device(device_udid) |
| |
| def setup_test_run(self, device_class=None): |
| mac_os_version = self.host.platform.os_version |
| |
| self._set_device_class(device_class) |
| |
| _log.debug('') |
| _log.debug('setup_test_run for %s', self._device_class) |
| |
| self._create_simulators() |
| |
| for i in xrange(self.child_processes()): |
| device_udid = self._testing_device(i).udid |
| _log.debug('testing device %s has udid %s', i, device_udid) |
| |
| # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator |
| self._executive.run_command([ |
| 'open', '-g', '-b', self.SIMULATOR_BUNDLE_ID + str(i), |
| '--args', '-CurrentDeviceUDID', device_udid]) |
| |
| if mac_os_version in ['elcapitan', 'yosemite', 'mavericks']: |
| time.sleep(2.5) |
| |
| _log.info('Waiting for all iOS Simulators to finish booting.') |
| for i in xrange(self.child_processes()): |
| Simulator.wait_until_device_is_booted(self._testing_device(i).udid) |
| |
| def _quit_ios_simulator(self): |
| _log.debug("_quit_ios_simulator killing all Simulator processes") |
| # FIXME: We should kill only the Simulators we started. |
| subprocess.call(["killall", "-9", "-m", "Simulator"]) |
| |
| def clean_up_test_run(self): |
| super(IOSSimulatorPort, self).clean_up_test_run() |
| _log.debug("clean_up_test_run") |
| self._quit_ios_simulator() |
| fifos = [path for path in os.listdir('/tmp') if re.search('org.webkit.(DumpRenderTree|WebKitTestRunner).*_(IN|OUT|ERROR)', path)] |
| for fifo in fifos: |
| try: |
| os.remove(os.path.join('/tmp', fifo)) |
| except OSError: |
| _log.warning('Unable to remove ' + fifo) |
| pass |
| |
| for i in xrange(self.child_processes()): |
| simulator_path = self.get_simulator_path(i) |
| device_udid = self._testing_device(i).udid |
| self._remove_device(i) |
| |
| if not os.path.exists(simulator_path): |
| continue |
| try: |
| self._executive.run_command([self.LSREGISTER_PATH, "-u", simulator_path]) |
| |
| _log.debug('rmtree %s', simulator_path) |
| self._filesystem.rmtree(simulator_path) |
| |
| logs_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Logs/CoreSimulator/", device_udid) |
| _log.debug('rmtree %s', logs_path) |
| self._filesystem.rmtree(logs_path) |
| |
| saved_state_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Saved Application State/", self.SIMULATOR_BUNDLE_ID + str(i) + ".savedState") |
| _log.debug('rmtree %s', saved_state_path) |
| self._filesystem.rmtree(saved_state_path) |
| |
| except: |
| _log.warning('Unable to remove Simulator' + str(i)) |
| |
| def setup_environ_for_server(self, server_name=None): |
| _log.debug("setup_environ_for_server") |
| env = super(IOSSimulatorPort, self).setup_environ_for_server(server_name) |
| if server_name == self.driver_name(): |
| if self.get_option('leaks'): |
| env['MallocStackLogging'] = '1' |
| env['__XPC_MallocStackLogging'] = '1' |
| env['MallocScribble'] = '1' |
| env['__XPC_MallocScribble'] = '1' |
| if 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 operating_system(self): |
| return 'ios-simulator' |
| |
| def check_sys_deps(self, needs_http): |
| if not self.simulator_runtime.available: |
| _log.error('The iOS Simulator runtime with identifier "{0}" cannot be used because it is unavailable.'.format(self.simulator_runtime.identifier)) |
| return False |
| return super(IOSSimulatorPort, self).check_sys_deps(needs_http) |
| |
| SUBPROCESS_CRASH_REGEX = re.compile('#CRASHED - (?P<subprocess_name>\S+) \(pid (?P<subprocess_pid>\d+)\)') |
| |
| def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=time.time, sleep_fn=time.sleep, wait_for_log=True): |
| time_fn = time_fn or time.time |
| sleep_fn = sleep_fn or time.sleep |
| |
| # FIXME: We should collect the actual crash log for DumpRenderTree.app because it includes more |
| # information (e.g. exception codes) than is available in the stack trace written to standard error. |
| stderr_lines = [] |
| crashed_subprocess_name_and_pid = None # e.g. ('DumpRenderTree.app', 1234) |
| for line in (stderr or '').splitlines(): |
| if not crashed_subprocess_name_and_pid: |
| match = self.SUBPROCESS_CRASH_REGEX.match(line) |
| if match: |
| crashed_subprocess_name_and_pid = (match.group('subprocess_name'), int(match.group('subprocess_pid'))) |
| continue |
| stderr_lines.append(line) |
| |
| if crashed_subprocess_name_and_pid: |
| return self._get_crash_log(crashed_subprocess_name_and_pid[0], crashed_subprocess_name_and_pid[1], stdout, |
| '\n'.join(stderr_lines), newer_than, time_fn, sleep_fn, wait_for_log) |
| |
| # LayoutTestRelay crashed |
| _log.debug('looking for crash log for %s:%s' % (name, str(pid))) |
| crash_log = '' |
| crash_logs = CrashLogs(self.host) |
| now = time_fn() |
| deadline = now + 5 * int(self.get_option('child_processes', 1)) |
| while not crash_log and now <= deadline: |
| crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than) |
| if not wait_for_log: |
| break |
| if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]: |
| sleep_fn(0.1) |
| now = time_fn() |
| |
| if not crash_log: |
| return stderr, None |
| return stderr, crash_log |
| |
| def _create_device(self, number): |
| return Simulator.create_device(number, self.simulator_device_type(), self.simulator_runtime) |
| |
| def _remove_device(self, number): |
| Simulator.remove_device(number) |
| |
| def _testing_device(self, number): |
| return Simulator.device_number(number) |
| |
| # This is only exposed so that IOSSimulatorDriver can use it. |
| def device_id_for_worker_number(self, number): |
| if self._printing_cmd_line: |
| return '<dummy id>' |
| return self._testing_device(number).udid |
| |
| def get_simulator_path(self, suffix=""): |
| return os.path.join(self.SIMULATOR_DIRECTORY, "Simulator" + str(suffix) + ".app") |
| |
| def diff_image(self, expected_contents, actual_contents, tolerance=None): |
| if not actual_contents and not expected_contents: |
| return (None, 0, None) |
| if not actual_contents or not expected_contents: |
| return (True, 0, None) |
| if not self._image_differ: |
| self._image_differ = image_diff.IOSSimulatorImageDiffer(self) |
| self.set_option_default('tolerance', 0.1) |
| if tolerance is None: |
| tolerance = self.get_option('tolerance') |
| return self._image_differ.diff_image(expected_contents, actual_contents, tolerance) |
| |
| def reset_preferences(self): |
| _log.debug("reset_preferences") |
| self._quit_ios_simulator() |
| # Maybe this should delete all devices that we've created? |
| |
| def nm_command(self): |
| return self.xcrun_find('nm') |
| |
| def xcrun_find(self, command, fallback=None): |
| fallback = fallback or command |
| try: |
| return self._executive.run_command(['xcrun', '--sdk', 'iphonesimulator', '-find', command]).rstrip() |
| except ScriptError: |
| _log.warn("xcrun failed; falling back to '%s'." % fallback) |
| return fallback |
| |
| @property |
| @memoized |
| def developer_dir(self): |
| return self._executive.run_command(['xcode-select', '--print-path']).rstrip() |
| |
| def logging_patterns_to_strip(self): |
| return [] |
| |
| def stderr_patterns_to_strip(self): |
| return [] |
| |
| def _createSimulatorApps(self): |
| for i in xrange(self.child_processes()): |
| self._createSimulatorApp(i) |
| |
| def _createSimulatorApp(self, suffix): |
| destination = self.get_simulator_path(suffix) |
| _log.info("Creating app:" + destination) |
| if os.path.exists(destination): |
| shutil.rmtree(destination, ignore_errors=True) |
| simulator_app_path = self.developer_dir + "/Applications/Simulator.app" |
| shutil.copytree(simulator_app_path, destination) |
| |
| # Update app's package-name inside plist and re-code-sign it |
| plist_path = destination + "/Contents/Info.plist" |
| command = "Set CFBundleIdentifier com.apple.iphonesimulator" + str(suffix) |
| subprocess.check_output(["/usr/libexec/PlistBuddy", "-c", command, plist_path]) |
| subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/Library/PrivateFrameworks/", destination + "/Contents/MacOS/Simulator"]) |
| subprocess.check_output(["codesign", "-fs", "-", destination]) |
| subprocess.check_output([self.LSREGISTER_PATH, "-f", destination]) |