04d2d4bc03d4722b82fcbeaacf8b80d17299f093
[WebKit-https.git] / Tools / Scripts / webkitpy / port / ios.py
1 # Copyright (C) 2014-2016 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 itertools
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.common.system.executive import ScriptError
33 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
34 from webkitpy.port import config as port_config
35 from webkitpy.port import driver, image_diff
36 from webkitpy.port.darwin import DarwinPort
37 from webkitpy.xcode.simulator import Simulator, Runtime, DeviceType
38 from webkitpy.common.system.crashlogs import CrashLogs
39
40
41 _log = logging.getLogger(__name__)
42
43
44 class IOSPort(DarwinPort):
45     port_name = "ios"
46
47     ARCHITECTURES = ['armv7', 'armv7s', 'arm64']
48     DEFAULT_ARCHITECTURE = 'arm64'
49     VERSION_FALLBACK_ORDER = ['ios-7', 'ios-8', 'ios-9', 'ios-10']
50
51     @classmethod
52     def determine_full_port_name(cls, host, options, port_name):
53         if port_name == cls.port_name:
54             iphoneos_sdk_version = host.platform.xcode_sdk_version('iphoneos')
55             if not iphoneos_sdk_version:
56                 raise Exception("Please install the iOS SDK.")
57             major_version_number = iphoneos_sdk_version.split('.')[0]
58             port_name = port_name + '-' + major_version_number
59         return port_name
60
61     # Despite their names, these flags do not actually get passed all the way down to webkit-build.
62     def _build_driver_flags(self):
63         return ['--sdk', 'iphoneos'] + (['ARCHS=%s' % self.architecture()] if self.architecture() else [])
64
65     def operating_system(self):
66         return 'ios'
67
68
69 class IOSSimulatorPort(DarwinPort):
70     port_name = "ios-simulator"
71
72     FUTURE_VERSION = 'future'
73     ARCHITECTURES = ['x86_64', 'x86']
74     DEFAULT_ARCHITECTURE = 'x86_64'
75
76     DEFAULT_DEVICE_CLASS = 'iphone'
77     CUSTOM_DEVICE_CLASSES = ['ipad', 'iphone7']
78     SDK = 'iphonesimulator'
79
80     SIMULATOR_BUNDLE_ID = 'com.apple.iphonesimulator'
81     relay_name = 'LayoutTestRelay'
82     SIMULATOR_DIRECTORY = "/tmp/WebKitTestingSimulators/"
83     LSREGISTER_PATH = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister"
84     PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 100
85
86     DEVICE_CLASS_MAP = {
87         'x86_64': {
88             'iphone': 'iPhone 5s',
89             'iphone7': 'iPhone 7',
90             'ipad': 'iPad Air'
91         },
92         'x86': {
93             'iphone': 'iPhone 5',
94             'ipad': 'iPad Retina'
95         },
96     }
97
98     def __init__(self, host, port_name, **kwargs):
99         DarwinPort.__init__(self, host, port_name, **kwargs)
100
101         optional_device_class = self.get_option('device_class')
102         self._printing_cmd_line = False
103         self._device_class = optional_device_class if optional_device_class else self.DEFAULT_DEVICE_CLASS
104         _log.debug('IOSSimulatorPort _device_class is %s', self._device_class)
105
106     def driver_name(self):
107         if self.get_option('driver_name'):
108             return self.get_option('driver_name')
109         if self.get_option('webkit_test_runner'):
110             return 'WebKitTestRunnerApp.app'
111         return 'DumpRenderTree.app'
112
113     def driver_cmd_line_for_logging(self):
114         # Avoid spinning up devices just for logging the commandline.
115         self._printing_cmd_line = True
116         result = super(IOSSimulatorPort, self).driver_cmd_line_for_logging()
117         self._printing_cmd_line = False
118         return result
119
120     @property
121     @memoized
122     def simulator_runtime(self):
123         runtime_identifier = self.get_option('runtime')
124         if runtime_identifier:
125             runtime = Runtime.from_identifier(runtime_identifier)
126         else:
127             runtime = Runtime.from_version_string(self.host.platform.xcode_sdk_version('iphonesimulator'))
128         return runtime
129
130     def simulator_device_type(self):
131         device_type_identifier = self.get_option('device_type')
132         if device_type_identifier:
133             _log.debug('simulator_device_type for device identifier %s', device_type_identifier)
134             device_type = DeviceType.from_identifier(device_type_identifier)
135         else:
136             _log.debug('simulator_device_type for device %s', self._device_class)
137             device_name = self.DEVICE_CLASS_MAP[self.architecture()][self._device_class]
138             if not device_name:
139                 raise Exception('Failed to find device for architecture {} and device class {}'.format(self.architecture()), self._device_class)
140             device_type = DeviceType.from_name(device_name)
141         return device_type
142
143     @property
144     @memoized
145     def relay_path(self):
146         if self._root_was_set:
147             path = self._filesystem.abspath(self.get_option('root'))
148         else:
149             mac_config = port_config.Config(self._executive, self._filesystem, 'mac')
150             path = mac_config.build_directory(self.get_option('configuration'))
151         return self._filesystem.join(path, self.relay_name)
152
153     @memoized
154     def child_processes(self):
155         return int(self.get_option('child_processes'))
156
157     @memoized
158     def default_child_processes(self):
159         """Return the number of Simulators instances to use for this port."""
160         best_child_process_count_for_cpu = self._executive.cpu_count() / 2
161         system_process_count_limit = int(subprocess.check_output(["ulimit", "-u"]).strip())
162         current_process_count = len(subprocess.check_output(["ps", "aux"]).strip().split('\n'))
163         _log.debug('Process limit: %d, current #processes: %d' % (system_process_count_limit, current_process_count))
164         maximum_simulator_count_on_this_system = (system_process_count_limit - current_process_count) // self.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
165         # FIXME: We should also take into account the available RAM.
166
167         if (maximum_simulator_count_on_this_system < best_child_process_count_for_cpu):
168             _log.warn("This machine could support %s simulators, but is only configured for %s."
169                 % (best_child_process_count_for_cpu, maximum_simulator_count_on_this_system))
170             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
171
172         if maximum_simulator_count_on_this_system == 0:
173             maximum_simulator_count_on_this_system = 1
174
175         return min(maximum_simulator_count_on_this_system, best_child_process_count_for_cpu)
176
177     def _check_relay(self):
178         if not self._filesystem.exists(self.relay_path):
179             _log.error("%s was not found at %s" % (self.relay_name, self.relay_path))
180             return False
181         return True
182
183     def _check_port_build(self):
184         if not self._root_was_set and self.get_option('build') and not self._build_relay():
185             return False
186         if not self._check_relay():
187             return False
188         return True
189
190     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=time.time, sleep_fn=time.sleep, wait_for_log=True):
191         time_fn = time_fn or time.time
192         sleep_fn = sleep_fn or time.sleep
193
194         # FIXME: We should collect the actual crash log for DumpRenderTree.app because it includes more
195         # information (e.g. exception codes) than is available in the stack trace written to standard error.
196         stderr_lines = []
197         crashed_subprocess_name_and_pid = None  # e.g. ('DumpRenderTree.app', 1234)
198         for line in (stderr or '').splitlines():
199             if not crashed_subprocess_name_and_pid:
200                 match = self.SUBPROCESS_CRASH_REGEX.match(line)
201                 if match:
202                     crashed_subprocess_name_and_pid = (match.group('subprocess_name'), int(match.group('subprocess_pid')))
203                     continue
204             stderr_lines.append(line)
205
206         if crashed_subprocess_name_and_pid:
207             return self._get_crash_log(crashed_subprocess_name_and_pid[0], crashed_subprocess_name_and_pid[1], stdout,
208                 '\n'.join(stderr_lines), newer_than, time_fn, sleep_fn, wait_for_log)
209
210         # LayoutTestRelay crashed
211         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
212         crash_log = ''
213         crash_logs = CrashLogs(self.host)
214         now = time_fn()
215         deadline = now + 5 * int(self.get_option('child_processes', 1))
216         while not crash_log and now <= deadline:
217             crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
218             if not wait_for_log:
219                 break
220             if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
221                 sleep_fn(0.1)
222                 now = time_fn()
223
224         if not crash_log:
225             return stderr, None
226         return stderr, crash_log
227
228     def _build_relay(self):
229         environment = self.host.copy_current_environment()
230         environment.disable_gcc_smartquotes()
231         env = environment.to_dictionary()
232
233         try:
234             # FIXME: We should be passing _arguments_for_configuration(), which respects build configuration and port,
235             # instead of hardcoding --ios-simulator.
236             self._run_script("build-layouttestrelay", args=["--ios-simulator"], env=env)
237         except ScriptError, e:
238             _log.error(e.message_with_output(output_limit=None))
239             return False
240         return True
241
242     def _build_driver(self):
243         built_tool = super(IOSSimulatorPort, self)._build_driver()
244         built_relay = self._build_relay()
245         return built_tool and built_relay
246
247     def _build_driver_flags(self):
248         archs = ['ARCHS=i386'] if self.architecture() == 'x86' else []
249         sdk = ['--sdk', 'iphonesimulator']
250         return archs + sdk
251
252     def _generate_all_test_configurations(self):
253         configurations = []
254         for build_type in self.ALL_BUILD_TYPES:
255             for architecture in self.ARCHITECTURES:
256                 configurations.append(TestConfiguration(version=self._version, architecture=architecture, build_type=build_type))
257         return configurations
258
259     def _driver_class(self):
260         return driver.IOSSimulatorDriver
261
262     def default_baseline_search_path(self):
263         if self.get_option('webkit_test_runner'):
264             fallback_names = [self._wk2_port_name()] + [self.port_name] + ['wk2']
265         else:
266             fallback_names = [self.port_name + '-wk1'] + [self.port_name]
267
268         return map(self._webkit_baseline_path, fallback_names)
269
270     def _set_device_class(self, device_class):
271         self._device_class = device_class if device_class else self.DEFAULT_DEVICE_CLASS
272
273     def _create_simulators(self):
274         if (self.default_child_processes() < self.child_processes()):
275                 _log.warn("You have specified very high value({0}) for --child-processes".format(self.child_processes()))
276                 _log.warn("maximum child-processes which can be supported on this system are: {0}".format(self.default_child_processes()))
277                 _log.warn("This is very likely to fail.")
278
279         self._createSimulatorApps()
280
281         for i in xrange(self.child_processes()):
282             self._create_device(i)
283
284         for i in xrange(self.child_processes()):
285             device_udid = self._testing_device(i).udid
286             Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN)
287             Simulator.reset_device(device_udid)
288
289     def setup_test_run(self, device_class=None):
290         mac_os_version = self.host.platform.os_version
291
292         self._set_device_class(device_class)
293
294         _log.debug('')
295         _log.debug('setup_test_run for %s', self._device_class)
296
297         self._create_simulators()
298
299         for i in xrange(self.child_processes()):
300             device_udid = self._testing_device(i).udid
301             _log.debug('testing device %s has udid %s', i, device_udid)
302
303             # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator
304             self._executive.run_command([
305                 'open', '-g', '-b', self.SIMULATOR_BUNDLE_ID + str(i),
306                 '--args', '-CurrentDeviceUDID', device_udid])
307
308             if mac_os_version in ['elcapitan', 'yosemite', 'mavericks']:
309                 time.sleep(2.5)
310
311         _log.info('Waiting for all iOS Simulators to finish booting.')
312         for i in xrange(self.child_processes()):
313             Simulator.wait_until_device_is_booted(self._testing_device(i).udid)
314
315     def _quit_ios_simulator(self):
316         _log.debug("_quit_ios_simulator killing all Simulator processes")
317         # FIXME: We should kill only the Simulators we started.
318         subprocess.call(["killall", "-9", "-m", "Simulator"])
319
320     def clean_up_test_run(self):
321         super(IOSSimulatorPort, self).clean_up_test_run()
322         _log.debug("clean_up_test_run")
323         self._quit_ios_simulator()
324         fifos = [path for path in os.listdir('/tmp') if re.search('org.webkit.(DumpRenderTree|WebKitTestRunner).*_(IN|OUT|ERROR)', path)]
325         for fifo in fifos:
326             try:
327                 os.remove(os.path.join('/tmp', fifo))
328             except OSError:
329                 _log.warning('Unable to remove ' + fifo)
330                 pass
331
332         for i in xrange(self.child_processes()):
333             simulator_path = self.get_simulator_path(i)
334             device_udid = self._testing_device(i).udid
335             self._remove_device(i)
336
337             if not os.path.exists(simulator_path):
338                 continue
339             try:
340                 self._executive.run_command([self.LSREGISTER_PATH, "-u", simulator_path])
341
342                 _log.debug('rmtree %s', simulator_path)
343                 self._filesystem.rmtree(simulator_path)
344
345                 logs_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Logs/CoreSimulator/", device_udid)
346                 _log.debug('rmtree %s', logs_path)
347                 self._filesystem.rmtree(logs_path)
348
349                 saved_state_path = self._filesystem.join(self._filesystem.expanduser("~"), "Library/Saved Application State/", self.SIMULATOR_BUNDLE_ID + str(i) + ".savedState")
350                 _log.debug('rmtree %s', saved_state_path)
351                 self._filesystem.rmtree(saved_state_path)
352
353             except:
354                 _log.warning('Unable to remove Simulator' + str(i))
355
356     def setup_environ_for_server(self, server_name=None):
357         _log.debug("setup_environ_for_server")
358         env = super(IOSSimulatorPort, self).setup_environ_for_server(server_name)
359         if server_name == self.driver_name():
360             if self.get_option('leaks'):
361                 env['MallocStackLogging'] = '1'
362                 env['__XPC_MallocStackLogging'] = '1'
363                 env['MallocScribble'] = '1'
364                 env['__XPC_MallocScribble'] = '1'
365             if self.get_option('guard_malloc'):
366                 self._append_value_colon_separated(env, 'DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
367                 self._append_value_colon_separated(env, '__XPC_DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
368         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
369         return env
370
371     def operating_system(self):
372         return 'ios-simulator'
373
374     def check_sys_deps(self, needs_http):
375         if not self.simulator_runtime.available:
376             _log.error('The iOS Simulator runtime with identifier "{0}" cannot be used because it is unavailable.'.format(self.simulator_runtime.identifier))
377             return False
378         return super(IOSSimulatorPort, self).check_sys_deps(needs_http)
379
380     SUBPROCESS_CRASH_REGEX = re.compile('#CRASHED - (?P<subprocess_name>\S+) \(pid (?P<subprocess_pid>\d+)\)')
381
382     def _create_device(self, number):
383         return Simulator.create_device(number, self.simulator_device_type(), self.simulator_runtime)
384
385     def _remove_device(self, number):
386         Simulator.remove_device(number)
387
388     def _testing_device(self, number):
389         return Simulator.device_number(number)
390
391     # This is only exposed so that IOSSimulatorDriver can use it.
392     def device_id_for_worker_number(self, number):
393         if self._printing_cmd_line:
394             return '<dummy id>'
395         return self._testing_device(number).udid
396
397     def get_simulator_path(self, suffix=""):
398         return os.path.join(self.SIMULATOR_DIRECTORY, "Simulator" + str(suffix) + ".app")
399
400     def diff_image(self, expected_contents, actual_contents, tolerance=None):
401         if not actual_contents and not expected_contents:
402             return (None, 0, None)
403         if not actual_contents or not expected_contents:
404             return (True, 0, None)
405         if not self._image_differ:
406             self._image_differ = image_diff.IOSSimulatorImageDiffer(self)
407         self.set_option_default('tolerance', 0.1)
408         if tolerance is None:
409             tolerance = self.get_option('tolerance')
410         return self._image_differ.diff_image(expected_contents, actual_contents, tolerance)
411
412     def reset_preferences(self):
413         _log.debug("reset_preferences")
414         self._quit_ios_simulator()
415         # Maybe this should delete all devices that we've created?
416
417     def nm_command(self):
418         return self.xcrun_find('nm')
419
420     @property
421     @memoized
422     def developer_dir(self):
423         return self._executive.run_command(['xcode-select', '--print-path']).rstrip()
424
425     def logging_patterns_to_strip(self):
426         return []
427
428     def stderr_patterns_to_strip(self):
429         return []
430
431     def _createSimulatorApps(self):
432         for i in xrange(self.child_processes()):
433             self._createSimulatorApp(i)
434
435     def _createSimulatorApp(self, suffix):
436         destination = self.get_simulator_path(suffix)
437         _log.info("Creating app:" + destination)
438         if os.path.exists(destination):
439             shutil.rmtree(destination, ignore_errors=True)
440         simulator_app_path = self.developer_dir + "/Applications/Simulator.app"
441         shutil.copytree(simulator_app_path, destination)
442
443         # Update app's package-name inside plist and re-code-sign it
444         plist_path = destination + "/Contents/Info.plist"
445         command = "Set CFBundleIdentifier com.apple.iphonesimulator" + str(suffix)
446         subprocess.check_output(["/usr/libexec/PlistBuddy", "-c", command, plist_path])
447         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/Library/PrivateFrameworks/", destination + "/Contents/MacOS/Simulator"])
448         subprocess.check_output(["install_name_tool", "-add_rpath", self.developer_dir + "/../Frameworks/", destination + "/Contents/MacOS/Simulator"])
449         subprocess.check_output(["codesign", "-fs", "-", destination])
450         subprocess.check_output([self.LSREGISTER_PATH, "-f", destination])