3612b6f186d0042aff6dd98c8f9e4ed10a327634
[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 logging
24 import os
25 import re
26 import shutil
27 import subprocess
28 import time
29
30 from webkitpy.common.memoized import memoized
31 from webkitpy.port import image_diff
32 from webkitpy.port.device import Device
33 from webkitpy.port.ios import IOSPort
34 from webkitpy.xcode.simulator import Simulator, Runtime, DeviceType
35 from webkitpy.common.system.crashlogs import CrashLogs
36
37
38 _log = logging.getLogger(__name__)
39
40
41 class IOSSimulatorPort(IOSPort):
42     port_name = "ios-simulator"
43
44     FUTURE_VERSION = 'future'
45     ARCHITECTURES = ['x86_64', 'x86']
46     DEFAULT_ARCHITECTURE = 'x86_64'
47
48     DEFAULT_DEVICE_CLASS = 'iphone'
49     CUSTOM_DEVICE_CLASSES = ['ipad', 'iphone7']
50     SDK = 'iphonesimulator'
51
52     SIMULATOR_BUNDLE_ID = 'com.apple.iphonesimulator'
53     SIMULATOR_DIRECTORY = "/tmp/WebKitTestingSimulators/"
54     LSREGISTER_PATH = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister"
55     PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 100
56
57     DEVICE_CLASS_MAP = {
58         'x86_64': {
59             'iphone': 'iPhone 5s',
60             'iphone7': 'iPhone 7',
61             'ipad': 'iPad Air',
62         },
63         'x86': {
64             'iphone': 'iPhone 5',
65             'ipad': 'iPad Retina',
66         },
67     }
68
69     #FIXME: Ports are recreated in each process. This is a problem for IOSSimulatorPort, it means devices are not
70     # persistent and devices hold a listening socket expected to be persistent across processes.
71     _DEVICE_MAP = {}
72     _CURRENT_DEVICE = None
73
74     def __init__(self, host, port_name, **kwargs):
75         super(IOSSimulatorPort, self).__init__(host, port_name, **kwargs)
76
77         optional_device_class = self.get_option('device_class')
78         self._device_class = optional_device_class if optional_device_class else self.DEFAULT_DEVICE_CLASS
79         _log.debug('IOSSimulatorPort _device_class is %s', self._device_class)
80
81         if not IOSSimulatorPort._CURRENT_DEVICE:
82             IOSSimulatorPort._CURRENT_DEVICE = Device(Simulator(host).current_device())
83         self._current_device = IOSSimulatorPort._CURRENT_DEVICE
84         if not self._current_device:
85             self.set_option('dedicated_simulators', True)
86         if not self.get_option('dedicated_simulators'):
87             if self.get_option('child_processes') > 1:
88                 _log.warn('Cannot have more than one child process when using a running simulator.  Setting child_processes to 1.')
89             self.set_option('child_processes', 1)
90
91     def _device_for_worker_number_map(self):
92         return IOSSimulatorPort._DEVICE_MAP
93
94     @property
95     @memoized
96     def simulator_runtime(self):
97         runtime_identifier = self.get_option('runtime')
98         if runtime_identifier:
99             runtime = Runtime.from_identifier(runtime_identifier)
100         else:
101             runtime = Runtime.from_version_string(self.host.platform.xcode_sdk_version('iphonesimulator'))
102         return runtime
103
104     def simulator_device_type(self):
105         device_type_identifier = self.get_option('device_type')
106         if device_type_identifier:
107             _log.debug('simulator_device_type for device identifier %s', device_type_identifier)
108             device_type = DeviceType.from_identifier(device_type_identifier)
109         else:
110             _log.debug('simulator_device_type for device %s', self._device_class)
111             device_name = self.DEVICE_CLASS_MAP[self.architecture()][self._device_class]
112             if not device_name:
113                 raise Exception('Failed to find device for architecture {} and device class {}'.format(self.architecture()), self._device_class)
114             device_type = DeviceType.from_name(device_name)
115         return device_type
116
117     @memoized
118     def default_child_processes(self):
119         """Return the number of Simulators instances to use for this port."""
120         best_child_process_count_for_cpu = self._executive.cpu_count() / 2
121         system_process_count_limit = int(subprocess.check_output(["ulimit", "-u"]).strip())
122         current_process_count = len(subprocess.check_output(["ps", "aux"]).strip().split('\n'))
123         _log.debug('Process limit: %d, current #processes: %d' % (system_process_count_limit, current_process_count))
124         maximum_simulator_count_on_this_system = (system_process_count_limit - current_process_count) // self.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
125         # FIXME: We should also take into account the available RAM.
126
127         if (maximum_simulator_count_on_this_system < best_child_process_count_for_cpu):
128             _log.warn("This machine could support %s simulators, but is only configured for %s."
129                 % (best_child_process_count_for_cpu, maximum_simulator_count_on_this_system))
130             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
131
132         if maximum_simulator_count_on_this_system == 0:
133             maximum_simulator_count_on_this_system = 1
134
135         return min(maximum_simulator_count_on_this_system, best_child_process_count_for_cpu)
136
137     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=time.time, sleep_fn=time.sleep, wait_for_log=True):
138         time_fn = time_fn or time.time
139         sleep_fn = sleep_fn or time.sleep
140
141         # FIXME: We should collect the actual crash log for DumpRenderTree.app because it includes more
142         # information (e.g. exception codes) than is available in the stack trace written to standard error.
143         stderr_lines = []
144         crashed_subprocess_name_and_pid = None  # e.g. ('DumpRenderTree.app', 1234)
145         for line in (stderr or '').splitlines():
146             if not crashed_subprocess_name_and_pid:
147                 match = self.SUBPROCESS_CRASH_REGEX.match(line)
148                 if match:
149                     crashed_subprocess_name_and_pid = (match.group('subprocess_name'), int(match.group('subprocess_pid')))
150                     continue
151             stderr_lines.append(line)
152
153         if crashed_subprocess_name_and_pid:
154             return self._get_crash_log(crashed_subprocess_name_and_pid[0], crashed_subprocess_name_and_pid[1], stdout,
155                 '\n'.join(stderr_lines), newer_than, time_fn, sleep_fn, wait_for_log)
156
157         # App crashed
158         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
159         crash_log = ''
160         crash_logs = CrashLogs(self.host)
161         now = time_fn()
162         deadline = now + 5 * int(self.get_option('child_processes', 1))
163         while not crash_log and now <= deadline:
164             crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
165             if not wait_for_log:
166                 break
167             if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
168                 sleep_fn(0.1)
169                 now = time_fn()
170
171         if not crash_log:
172             return stderr, None
173         return stderr, crash_log
174
175     def _build_driver_flags(self):
176         archs = ['ARCHS=i386'] if self.architecture() == 'x86' else []
177         sdk = ['--sdk', 'iphonesimulator']
178         return archs + sdk
179
180     def _set_device_class(self, device_class):
181         self._device_class = device_class if device_class else self.DEFAULT_DEVICE_CLASS
182
183     def _create_simulators(self):
184         if (self.default_child_processes() < self.child_processes()):
185             _log.warn('You have specified very high value({0}) for --child-processes'.format(self.child_processes()))
186             _log.warn('maximum child-processes which can be supported on this system are: {0}'.format(self.default_child_processes()))
187             _log.warn('This is very likely to fail.')
188
189         if self._using_dedicated_simulators():
190             self._createSimulatorApps()
191
192             for i in xrange(self.child_processes()):
193                 self._create_device(i)
194
195             for i in xrange(self.child_processes()):
196                 device_udid = Simulator.managed_devices[i].udid
197                 Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN)
198                 Simulator.reset_device(device_udid)
199         else:
200             assert(self._current_device)
201             if self._current_device.platform_device.name != self.simulator_device_type().name:
202                 _log.warn("Expected simulator of type '" + self.simulator_device_type().name + "' but found simulator of type '" + self._current_device.platform_device.name + "'")
203                 _log.warn('The next block of tests may fail due to device mis-match')
204
205     def _create_devices(self, device_class):
206         mac_os_version = self.host.platform.os_version
207
208         self._set_device_class(device_class)
209
210         _log.debug('')
211         _log.debug('setup_test_run for %s', self._device_class)
212
213         self._create_simulators()
214
215         if not self._using_dedicated_simulators():
216             return
217
218         for i in xrange(self.child_processes()):
219             device_udid = Simulator.managed_devices[i].udid
220             _log.debug('testing device %s has udid %s', i, device_udid)
221
222             # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator
223             self._executive.run_command([
224                 'open', '-g', '-b', self.SIMULATOR_BUNDLE_ID + str(i),
225                 '--args', '-CurrentDeviceUDID', device_udid])
226
227             if mac_os_version in ['elcapitan', 'yosemite', 'mavericks']:
228                 time.sleep(2.5)
229
230         _log.info('Waiting for all iOS Simulators to finish booting.')
231         for i in xrange(self.child_processes()):
232             Simulator.wait_until_device_is_booted(Simulator.managed_devices[i].udid)
233
234         IOSSimulatorPort._DEVICE_MAP = {}
235         for i in xrange(self.child_processes()):
236             IOSSimulatorPort._DEVICE_MAP[i] = Device(Simulator.managed_devices[i])
237
238     def _quit_ios_simulator(self):
239         if not self._using_dedicated_simulators():
240             return
241         _log.debug("_quit_ios_simulator killing all Simulator processes")
242         # FIXME: We should kill only the Simulators we started.
243         subprocess.call(["killall", "-9", "-m", "Simulator"])
244
245     def clean_up_test_run(self):
246         super(IOSSimulatorPort, self).clean_up_test_run()
247         _log.debug("clean_up_test_run")
248         self._quit_ios_simulator()
249         fifos = [path for path in os.listdir('/tmp') if re.search('org.webkit.(DumpRenderTree|WebKitTestRunner).*_(IN|OUT|ERROR)', path)]
250         for fifo in fifos:
251             try:
252                 os.remove(os.path.join('/tmp', fifo))
253             except OSError:
254                 _log.warning('Unable to remove ' + fifo)
255                 pass
256
257         if not self._using_dedicated_simulators():
258             return
259
260         for i in xrange(self.child_processes()):
261             simulator_path = self.get_simulator_path(i)
262             device_udid = self._testing_device(i).udid
263             self._remove_device(i)
264
265             if not os.path.exists(simulator_path):
266                 continue
267             try:
268                 self._executive.run_command([self.LSREGISTER_PATH, "-u", simulator_path])
269
270                 _log.debug('rmtree %s', simulator_path)
271                 self._filesystem.rmtree(simulator_path)
272
273                 logs_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Logs/CoreSimulator/", device_udid)
274                 _log.debug('rmtree %s', logs_path)
275                 self._filesystem.rmtree(logs_path)
276
277                 saved_state_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Saved Application State/", self.SIMULATOR_BUNDLE_ID + str(i) + ".savedState")
278                 _log.debug('rmtree %s', saved_state_path)
279                 self._filesystem.rmtree(saved_state_path)
280
281             except:
282                 _log.warning('Unable to remove Simulator' + str(i))
283         IOSSimulatorPort._DEVICE_MAP = {}
284
285     def setup_environ_for_server(self, server_name=None):
286         _log.debug("setup_environ_for_server")
287         env = super(IOSSimulatorPort, self).setup_environ_for_server(server_name)
288         if server_name == self.driver_name():
289             if self.get_option('leaks'):
290                 env['MallocStackLogging'] = '1'
291                 env['__XPC_MallocStackLogging'] = '1'
292                 env['MallocScribble'] = '1'
293                 env['__XPC_MallocScribble'] = '1'
294             if self.get_option('guard_malloc'):
295                 self._append_value_colon_separated(env, 'DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
296                 self._append_value_colon_separated(env, '__XPC_DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
297         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
298         return env
299
300     def operating_system(self):
301         return 'ios-simulator'
302
303     def check_sys_deps(self, needs_http):
304         if not self.simulator_runtime.available:
305             _log.error('The iOS Simulator runtime with identifier "{0}" cannot be used because it is unavailable.'.format(self.simulator_runtime.identifier))
306             return False
307         return super(IOSSimulatorPort, self).check_sys_deps(needs_http)
308
309     SUBPROCESS_CRASH_REGEX = re.compile('#CRASHED - (?P<subprocess_name>\S+) \(pid (?P<subprocess_pid>\d+)\)')
310
311     def _using_dedicated_simulators(self):
312         return self.get_option('dedicated_simulators')
313
314     def using_multiple_devices(self):
315         return self._using_dedicated_simulators()
316
317     def _create_device(self, number):
318         return Simulator.create_device(number, self.simulator_device_type(), self.simulator_runtime)
319
320     def _remove_device(self, number):
321         Simulator.remove_device(number)
322
323     def get_simulator_path(self, suffix=""):
324         return os.path.join(self.SIMULATOR_DIRECTORY, "Simulator" + str(suffix) + ".app")
325
326     def diff_image(self, expected_contents, actual_contents, tolerance=None):
327         if not actual_contents and not expected_contents:
328             return (None, 0, None)
329         if not actual_contents or not expected_contents:
330             return (True, 0, None)
331         if not self._image_differ:
332             self._image_differ = image_diff.IOSSimulatorImageDiffer(self)
333         self.set_option_default('tolerance', 0.1)
334         if tolerance is None:
335             tolerance = self.get_option('tolerance')
336         return self._image_differ.diff_image(expected_contents, actual_contents, tolerance)
337
338     def reset_preferences(self):
339         _log.debug("reset_preferences")
340         self._quit_ios_simulator()
341         # Maybe this should delete all devices that we've created?
342
343     def nm_command(self):
344         return self.xcrun_find('nm')
345
346     @property
347     @memoized
348     def developer_dir(self):
349         return self._executive.run_command(['xcode-select', '--print-path']).rstrip()
350
351     def logging_patterns_to_strip(self):
352         return []
353
354     def stderr_patterns_to_strip(self):
355         return []
356
357     def _createSimulatorApps(self):
358         for i in xrange(self.child_processes()):
359             self._createSimulatorApp(i)
360
361     def _createSimulatorApp(self, suffix):
362         destination = self.get_simulator_path(suffix)
363         _log.info("Creating app:" + destination)
364         if os.path.exists(destination):
365             shutil.rmtree(destination, ignore_errors=True)
366         simulator_app_path = self.developer_dir + "/Applications/Simulator.app"
367         shutil.copytree(simulator_app_path, destination)
368
369         # Update app's package-name inside plist and re-code-sign it
370         plist_path = destination + "/Contents/Info.plist"
371         command = "Set CFBundleIdentifier com.apple.iphonesimulator" + str(suffix)
372         subprocess.check_output(["/usr/libexec/PlistBuddy", "-c", command, plist_path])
373         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/Library/PrivateFrameworks/", destination + "/Contents/MacOS/Simulator"])
374         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/../Frameworks/", destination + "/Contents/MacOS/Simulator"])
375         subprocess.check_output(["codesign", "-fs", "-", destination])
376         subprocess.check_output([self.LSREGISTER_PATH, "-f", destination])