Add support for different versions of iOS when loading test expectations
[WebKit-https.git] / Tools / Scripts / webkitpy / port / ios_simulator.py
1 # Copyright (C) 2014-2017 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1.  Redistributions of source code must retain the above copyright
7 #     notice, this list of conditions and the following disclaimer.
8 # 2.  Redistributions in binary form must reproduce the above copyright
9 #     notice, this list of conditions and the following disclaimer in the
10 #     documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
13 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
16 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 import atexit
24 import logging
25 import os
26 import re
27 import shutil
28 import subprocess
29 import time
30
31 from webkitpy.common.memoized import memoized
32 from webkitpy.port.device import Device
33 from webkitpy.port.ios import IOSPort
34 from webkitpy.xcode.simulator import Simulator, Runtime, DeviceType
35
36
37 _log = logging.getLogger(__name__)
38
39
40 class IOSSimulatorPort(IOSPort):
41     port_name = "ios-simulator"
42
43     FUTURE_VERSION = 'future'
44     ARCHITECTURES = ['x86_64', 'x86']
45     DEFAULT_ARCHITECTURE = 'x86_64'
46
47     DEFAULT_DEVICE_CLASS = 'iphone'
48     CUSTOM_DEVICE_CLASSES = ['ipad', 'iphone7']
49     SDK = 'iphonesimulator'
50
51     SIMULATOR_BUNDLE_ID = 'com.apple.iphonesimulator'
52     SIMULATOR_DIRECTORY = "/tmp/WebKitTestingSimulators/"
53     LSREGISTER_PATH = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister"
54     PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 125
55
56     DEVICE_CLASS_MAP = {
57         'x86_64': {
58             'iphone': 'iPhone 5s',
59             'iphone7': 'iPhone 7',
60             'ipad': 'iPad Air',
61         },
62         'x86': {
63             'iphone': 'iPhone 5',
64             'ipad': 'iPad Retina',
65         },
66     }
67
68     #FIXME: Ports are recreated in each process. This is a problem for IOSSimulatorPort, it means devices are not
69     # persistent and devices hold a listening socket expected to be persistent across processes.
70     _DEVICE_MAP = {}
71     _CURRENT_DEVICE = None
72
73     def __init__(self, host, port_name, **kwargs):
74         super(IOSSimulatorPort, self).__init__(host, port_name, **kwargs)
75
76         optional_device_class = self.get_option('device_class')
77         self._device_class = optional_device_class if optional_device_class else self.DEFAULT_DEVICE_CLASS
78         _log.debug('IOSSimulatorPort _device_class is %s', self._device_class)
79
80         if not IOSSimulatorPort._CURRENT_DEVICE:
81             IOSSimulatorPort._CURRENT_DEVICE = Device(Simulator(host).current_device())
82         self._current_device = IOSSimulatorPort._CURRENT_DEVICE
83         if not self._current_device:
84             self.set_option('dedicated_simulators', True)
85         if not self.get_option('dedicated_simulators'):
86             if self.get_option('child_processes') > 1:
87                 _log.warn('Cannot have more than one child process when using a running simulator.  Setting child_processes to 1.')
88             self.set_option('child_processes', 1)
89
90     def _device_for_worker_number_map(self):
91         return IOSSimulatorPort._DEVICE_MAP
92
93     @property
94     @memoized
95     def simulator_runtime(self):
96         runtime_identifier = self.get_option('runtime')
97         if runtime_identifier:
98             runtime = Runtime.from_identifier(runtime_identifier)
99         else:
100             runtime = Runtime.from_version_string(self.host.platform.xcode_sdk_version('iphonesimulator'))
101         return runtime
102
103     def ios_version(self):
104         # FIXME: We should replace --runtime with something which makes sense for both Simulator and Device
105         # https://bugs.webkit.org/show_bug.cgi?id=173775
106         runtime_identifier = self.get_option('runtime')
107         if runtime_identifier:
108             return '.'.join(str(i) for i in Runtime.from_identifier(runtime_identifier).version)
109         return self.host.platform.xcode_sdk_version('iphonesimulator')
110
111     def simulator_device_type(self):
112         device_type_identifier = self.get_option('device_type')
113         if device_type_identifier:
114             _log.debug('simulator_device_type for device identifier %s', device_type_identifier)
115             device_type = DeviceType.from_identifier(device_type_identifier)
116         else:
117             _log.debug('simulator_device_type for device %s', self._device_class)
118             device_name = self.DEVICE_CLASS_MAP[self.architecture()][self._device_class]
119             if not device_name:
120                 raise Exception('Failed to find device for architecture {} and device class {}'.format(self.architecture()), self._device_class)
121             device_type = DeviceType.from_name(device_name)
122         return device_type
123
124     @memoized
125     def default_child_processes(self):
126         """Return the number of Simulators instances to use for this port."""
127         best_child_process_count_for_cpu = self._executive.cpu_count() / 2
128         system_process_count_limit = int(subprocess.check_output(["ulimit", "-u"]).strip())
129         current_process_count = len(subprocess.check_output(["ps", "aux"]).strip().split('\n'))
130         _log.debug('Process limit: %d, current #processes: %d' % (system_process_count_limit, current_process_count))
131         maximum_simulator_count_on_this_system = (system_process_count_limit - current_process_count) // self.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
132         # FIXME: We should also take into account the available RAM.
133
134         if (maximum_simulator_count_on_this_system < best_child_process_count_for_cpu):
135             _log.warn("This machine could support %s simulators, but is only configured for %s."
136                 % (best_child_process_count_for_cpu, maximum_simulator_count_on_this_system))
137             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
138
139         if maximum_simulator_count_on_this_system == 0:
140             maximum_simulator_count_on_this_system = 1
141
142         return min(maximum_simulator_count_on_this_system, best_child_process_count_for_cpu)
143
144     def _build_driver_flags(self):
145         archs = ['ARCHS=i386'] if self.architecture() == 'x86' else []
146         sdk = ['--sdk', 'iphonesimulator']
147         return archs + sdk
148
149     def _set_device_class(self, device_class):
150         self._device_class = device_class if device_class else self.DEFAULT_DEVICE_CLASS
151
152     # This function may be called more than once.
153     def _teardown_managed_simulators(self):
154         if not self._using_dedicated_simulators():
155             return
156         self._quit_ios_simulator()
157
158         for i in xrange(len(Simulator.managed_devices)):
159             simulator_path = self.get_simulator_path(i)
160             device_udid = Simulator.managed_devices[i].udid
161             Simulator.remove_device(i)
162
163             if not os.path.exists(simulator_path):
164                 continue
165             try:
166                 self._executive.run_command([IOSSimulatorPort.LSREGISTER_PATH, "-u", simulator_path])
167
168                 _log.debug('rmtree %s', simulator_path)
169                 self._filesystem.rmtree(simulator_path)
170
171                 logs_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Logs/CoreSimulator/", device_udid)
172                 _log.debug('rmtree %s', logs_path)
173                 self._filesystem.rmtree(logs_path)
174
175                 saved_state_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Saved Application State/", IOSSimulatorPort.SIMULATOR_BUNDLE_ID + str(i) + ".savedState")
176                 _log.debug('rmtree %s', saved_state_path)
177                 self._filesystem.rmtree(saved_state_path)
178             except:
179                 _log.warning('Unable to remove Simulator' + str(i))
180
181     def use_multiple_simulator_apps(self):
182         return int(self.host.platform.xcode_version().split('.')[0]) < 9
183
184     def _create_simulators(self):
185         if (self.default_child_processes() < self.child_processes()):
186             _log.warn('You have specified very high value({0}) for --child-processes'.format(self.child_processes()))
187             _log.warn('maximum child-processes which can be supported on this system are: {0}'.format(self.default_child_processes()))
188             _log.warn('This is very likely to fail.')
189
190         if self._using_dedicated_simulators():
191             atexit.register(lambda: self._teardown_managed_simulators())
192
193             if self.use_multiple_simulator_apps():
194                 self._createSimulatorApps()
195
196             for i in xrange(self.child_processes()):
197                 self._create_device(i)
198
199             for i in xrange(self.child_processes()):
200                 device_udid = Simulator.managed_devices[i].udid
201                 Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN)
202                 Simulator.reset_device(device_udid)
203         else:
204             assert(self._current_device)
205             if self._current_device.platform_device.name != self.simulator_device_type().name:
206                 _log.warn("Expected simulator of type '" + self.simulator_device_type().name + "' but found simulator of type '" + self._current_device.platform_device.name + "'")
207                 _log.warn('The next block of tests may fail due to device mis-match')
208
209     def _create_devices(self, device_class):
210         mac_os_version = self.host.platform.os_version
211
212         self._set_device_class(device_class)
213
214         _log.debug('')
215         _log.debug('setup_test_run for %s', self._device_class)
216
217         self._create_simulators()
218
219         if not self._using_dedicated_simulators():
220             return
221
222         for i in xrange(self.child_processes()):
223             device_udid = Simulator.managed_devices[i].udid
224             _log.debug('testing device %s has udid %s', i, device_udid)
225
226             # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator
227             if self.use_multiple_simulator_apps():
228                 self._executive.run_command([
229                     'open', '-g', '-b', self.SIMULATOR_BUNDLE_ID + str(i),
230                     '--args', '-CurrentDeviceUDID', device_udid])
231             else:
232                 self._executive.run_command(['xcrun', 'simctl', 'boot', device_udid])
233
234             if mac_os_version in ['elcapitan', 'yosemite', 'mavericks']:
235                 time.sleep(2.5)
236
237         if not self.use_multiple_simulator_apps():
238             self._executive.run_command(['open', '-g', '-b', self.SIMULATOR_BUNDLE_ID], return_exit_code=True)
239
240         _log.info('Waiting for all iOS Simulators to finish booting.')
241         for i in xrange(self.child_processes()):
242             Simulator.wait_until_device_is_booted(Simulator.managed_devices[i].udid)
243         _log.info('All simulators have booted.')
244
245         IOSSimulatorPort._DEVICE_MAP = {}
246         for i in xrange(self.child_processes()):
247             IOSSimulatorPort._DEVICE_MAP[i] = Device(Simulator.managed_devices[i])
248
249     def _quit_ios_simulator(self):
250         if not self._using_dedicated_simulators():
251             return
252         _log.debug("_quit_ios_simulator killing all Simulator processes")
253         # FIXME: We should kill only the Simulators we started.
254         subprocess.call(["killall", "-9", "-m", "Simulator"])
255
256     def clean_up_test_run(self):
257         super(IOSSimulatorPort, self).clean_up_test_run()
258         _log.debug("clean_up_test_run")
259
260         if not self._using_dedicated_simulators():
261             return
262
263         self._teardown_managed_simulators()
264         IOSSimulatorPort._DEVICE_MAP = {}
265
266     def setup_environ_for_server(self, server_name=None):
267         _log.debug("setup_environ_for_server")
268         env = super(IOSSimulatorPort, self).setup_environ_for_server(server_name)
269         if server_name == self.driver_name():
270             if self.get_option('leaks'):
271                 env['MallocStackLogging'] = '1'
272                 env['__XPC_MallocStackLogging'] = '1'
273                 env['MallocScribble'] = '1'
274                 env['__XPC_MallocScribble'] = '1'
275             if self.get_option('guard_malloc'):
276                 self._append_value_colon_separated(env, 'DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
277                 self._append_value_colon_separated(env, '__XPC_DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
278         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
279         return env
280
281     def operating_system(self):
282         return 'ios-simulator'
283
284     def check_sys_deps(self, needs_http):
285         if not self.simulator_runtime.available:
286             _log.error('The iOS Simulator runtime with identifier "{0}" cannot be used because it is unavailable.'.format(self.simulator_runtime.identifier))
287             return False
288         return super(IOSSimulatorPort, self).check_sys_deps(needs_http)
289
290     SUBPROCESS_CRASH_REGEX = re.compile('#CRASHED - (?P<subprocess_name>\S+) \(pid (?P<subprocess_pid>\d+)\)')
291
292     def _using_dedicated_simulators(self):
293         return self.get_option('dedicated_simulators')
294
295     def using_multiple_devices(self):
296         return self._using_dedicated_simulators()
297
298     def _create_device(self, number):
299         return Simulator.create_device(number, self.simulator_device_type(), self.simulator_runtime)
300
301     def get_simulator_path(self, suffix=""):
302         return os.path.join(self.SIMULATOR_DIRECTORY, "Simulator" + str(suffix) + ".app")
303
304     def reset_preferences(self):
305         _log.debug("reset_preferences")
306         self._quit_ios_simulator()
307         # Maybe this should delete all devices that we've created?
308
309     def nm_command(self):
310         return self.xcrun_find('nm')
311
312     @property
313     @memoized
314     def developer_dir(self):
315         return self._executive.run_command(['xcode-select', '--print-path']).rstrip()
316
317     def logging_patterns_to_strip(self):
318         return []
319
320     def stderr_patterns_to_strip(self):
321         return []
322
323     def _createSimulatorApps(self):
324         for i in xrange(self.child_processes()):
325             self._createSimulatorApp(i)
326
327     def _createSimulatorApp(self, suffix):
328         destination = self.get_simulator_path(suffix)
329         _log.info("Creating app:" + destination)
330         if os.path.exists(destination):
331             shutil.rmtree(destination, ignore_errors=True)
332         simulator_app_path = self.developer_dir + "/Applications/Simulator.app"
333         shutil.copytree(simulator_app_path, destination)
334
335         # Update app's package-name inside plist and re-code-sign it
336         plist_path = destination + "/Contents/Info.plist"
337         command = "Set CFBundleIdentifier com.apple.iphonesimulator" + str(suffix)
338         subprocess.check_output(["/usr/libexec/PlistBuddy", "-c", command, plist_path])
339         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/Library/PrivateFrameworks/", destination + "/Contents/MacOS/Simulator"])
340         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/../Frameworks/", destination + "/Contents/MacOS/Simulator"])
341         subprocess.check_output(["codesign", "-fs", "-", destination])
342         subprocess.check_output([self.LSREGISTER_PATH, "-f", destination])