Simulator launch fails intermittently due to failure in checking simulator boot status
[WebKit-https.git] / Tools / Scripts / webkitpy / xcode / simulator.py
1 # Copyright (C) 2014, 2015 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 ANY
13 # 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 ANY
16 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 import itertools
24 import logging
25 import os
26 import plistlib
27 import re
28 import subprocess
29 import time
30
31 from webkitpy.benchmark_runner.utils import timeout
32 from webkitpy.common.host import Host
33
34 _log = logging.getLogger(__name__)
35
36 """
37 Minimally wraps CoreSimulator functionality through simctl.
38
39 If possible, use real CoreSimulator.framework functionality by linking to the framework itself.
40 Do not use PyObjC to dlopen the framework.
41 """
42
43 class DeviceType(object):
44     """
45     Represents a CoreSimulator device type.
46     """
47     def __init__(self, name, identifier):
48         """
49         :param name: The device type's human-readable name
50         :type name: str
51         :param identifier: The CoreSimulator identifier.
52         :type identifier: str
53         """
54         self.name = name
55         self.identifier = identifier
56
57     @classmethod
58     def from_name(cls, name):
59         """
60         :param name: The name for the desired device type.
61         :type name: str
62         :returns: A `DeviceType` object with the specified identifier or throws a TypeError if it doesn't exist.
63         :rtype: DeviceType
64         """
65         identifier = None
66         for device_type in Simulator().device_types:
67             if device_type.name == name:
68                 identifier = device_type.identifier
69                 break
70
71         if identifier is None:
72             raise TypeError('A device type with name "{name}" does not exist.'.format(name=name))
73
74         return DeviceType(name, identifier)
75
76     @classmethod
77     def from_identifier(cls, identifier):
78         """
79         :param identifier: The CoreSimulator identifier for the desired runtime.
80         :type identifier: str
81         :returns: A `Runtime` object witht the specified identifier or throws a TypeError if it doesn't exist.
82         :rtype: DeviceType
83         """
84         name = None
85         for device_type in Simulator().device_types:
86             if device_type.identifier == identifier:
87                 name = device_type.name
88                 break
89
90         if name is None:
91             raise TypeError('A device type with identifier "{identifier}" does not exist.'.format(
92                 identifier=identifier))
93
94         return DeviceType(name, identifier)
95
96     def __eq__(self, other):
97         return (self.name == other.name) and (self.identifier == other.identifier)
98
99     def __ne__(self, other):
100         return not self.__eq__(other)
101
102     def __repr__(self):
103         return '<DeviceType "{name}": {identifier}>'.format(name=self.name, identifier=self.identifier)
104
105
106 class Runtime(object):
107     """
108     Represents a CoreSimulator runtime associated with an iOS SDK.
109     """
110
111     def __init__(self, version, identifier, available, devices=None, is_internal_runtime=False):
112         """
113         :param version: The iOS SDK version
114         :type version: tuple
115         :param identifier: The CoreSimulator runtime identifier
116         :type identifier: str
117         :param availability: Whether the runtime is available for use.
118         :type availability: bool
119         :param devices: A list of devices under this runtime
120         :type devices: list or None
121         :param is_internal_runtime: Whether the runtime is an Apple internal runtime
122         :type is_internal_runtime: bool
123         """
124         self.version = version
125         self.identifier = identifier
126         self.available = available
127         self.devices = devices or []
128         self.is_internal_runtime = is_internal_runtime
129
130     @classmethod
131     def from_version_string(cls, version):
132         return cls.from_identifier('com.apple.CoreSimulator.SimRuntime.iOS-' + version.replace('.', '-'))
133
134     @classmethod
135     def from_identifier(cls, identifier):
136         """
137         :param identifier: The identifier for the desired CoreSimulator runtime.
138         :type identifier: str
139         :returns: A `Runtime` object with the specified identifier or throws a TypeError if it doesn't exist.
140         :rtype: Runtime
141         """
142         for runtime in Simulator().runtimes:
143             if runtime.identifier == identifier:
144                 return runtime
145         raise TypeError('A runtime with identifier "{identifier}" does not exist.'.format(identifier=identifier))
146
147     def __eq__(self, other):
148         return (self.version == other.version) and (self.identifier == other.identifier) and (self.is_internal_runtime == other.is_internal_runtime)
149
150     def __ne__(self, other):
151         return not self.__eq__(other)
152
153     def __repr__(self):
154         version_suffix = ""
155         if self.is_internal_runtime:
156             version_suffix = " Internal"
157         return '<Runtime {version}: {identifier}. Available: {available}, {num_devices} devices>'.format(
158             version='.'.join(map(str, self.version)) + version_suffix,
159             identifier=self.identifier,
160             available=self.available,
161             num_devices=len(self.devices))
162
163
164 class Device(object):
165     """
166     Represents a CoreSimulator device underneath a runtime
167     """
168
169     def __init__(self, name, udid, available, runtime):
170         """
171         :param name: The device name
172         :type name: str
173         :param udid: The device UDID (a UUID string)
174         :type udid: str
175         :param available: Whether the device is available for use.
176         :type available: bool
177         :param runtime: The iOS Simulator runtime that hosts this device
178         :type runtime: Runtime
179         """
180         self.name = name
181         self.udid = udid
182         self.available = available
183         self.runtime = runtime
184
185     @property
186     def state(self):
187         """
188         :returns: The current state of the device.
189         :rtype: Simulator.DeviceState
190         """
191         return Simulator.device_state(self.udid)
192
193     @property
194     def path(self):
195         """
196         :returns: The filesystem path that contains the simulator device's data.
197         :rtype: str
198         """
199         return Simulator.device_directory(self.udid)
200
201     @classmethod
202     def create(cls, name, device_type, runtime):
203         """
204         Create a new CoreSimulator device.
205         :param name: The name of the device.
206         :type name: str
207         :param device_type: The CoreSimulatort device type.
208         :type device_type: DeviceType
209         :param runtime:  The CoreSimualtor runtime.
210         :type runtime: Runtime
211         :return: The new device or raises a CalledProcessError if ``simctl create`` failed.
212         :rtype: Device
213         """
214         device_udid = subprocess.check_output(['xcrun', 'simctl', 'create', name, device_type.identifier, runtime.identifier]).rstrip()
215         Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN)
216         return Simulator().find_device_by_udid(device_udid)
217
218     @classmethod
219     def delete(cls, udid):
220         """
221         Delete the given CoreSimulator device.
222         :param udid: The udid of the device.
223         :type udid: str
224         """
225         subprocess.call(['xcrun', 'simctl', 'delete', udid])
226
227     def __eq__(self, other):
228         return self.udid == other.udid
229
230     def __ne__(self, other):
231         return not self.__eq__(other)
232
233     def __repr__(self):
234         return '<Device "{name}": {udid}. State: {state}. Runtime: {runtime}, Available: {available}>'.format(
235             name=self.name,
236             udid=self.udid,
237             state=self.state,
238             available=self.available,
239             runtime=self.runtime.identifier)
240
241
242 # FIXME: This class is fragile because it parses the output of the simctl command line utility, which may change.
243 #        We should find a better way to query for simulator device state and capabilities. Maybe take a similiar
244 #        approach as in webkitdirs.pm and utilize the parsed output from the device.plist files in the sub-
245 #        directories of ~/Library/Developer/CoreSimulator/Devices?
246 #        Also, simctl has the option to output in JSON format (xcrun simctl list --json).
247 class Simulator(object):
248     """
249     Represents the iOS Simulator infrastructure under the currently select Xcode.app bundle.
250     """
251     device_type_re = re.compile('(?P<name>[^(]+)\((?P<identifier>[^)]+)\)')
252     # FIXME: runtime_re parses the version from the runtime name, but that does not contain the full version number
253     # (it can omit the revision). We should instead parse the version from the number contained in parentheses.
254     runtime_re = re.compile(
255         '(i|watch|tv)OS (?P<version>\d+\.\d)(?P<internal> Internal)? \(\d+\.\d+(\.\d+)? - (?P<build_version>[^)]+)\) \((?P<identifier>[^)]+)\)( \((?P<availability>[^)]+)\))?')
256     unavailable_version_re = re.compile('-- Unavailable: (?P<identifier>[^ ]+) --')
257     version_re = re.compile('-- (i|watch|tv)OS (?P<version>\d+\.\d+)(?P<internal> Internal)? --')
258     devices_re = re.compile(
259         '\s*(?P<name>[^(]+ )\((?P<udid>[^)]+)\) \((?P<state>[^)]+)\)( \((?P<availability>[^)]+)\))?')
260
261     def __init__(self, host=None):
262         self._host = host or Host()
263         self.runtimes = []
264         self.device_types = []
265         self.refresh()
266
267     # Keep these constants synchronized with the SimDeviceState constants in CoreSimulator/SimDevice.h.
268     class DeviceState:
269         DOES_NOT_EXIST = -1
270         CREATING = 0
271         SHUTDOWN = 1
272         BOOTING = 2
273         BOOTED = 3
274         SHUTTING_DOWN = 4
275
276     @staticmethod
277     def wait_until_device_is_booted(udid, timeout_seconds=60 * 5):
278         Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.BOOTED, timeout_seconds)
279         with timeout(seconds=timeout_seconds):
280             while True:
281                 try:
282                     state = subprocess.check_output(['xcrun', 'simctl', 'spawn', udid, 'launchctl', 'print', 'system']).strip()
283                     if re.search("A[\s]+com.apple.springboard.services", state):
284                         return
285                 except subprocess.CalledProcessError:
286                     _log.warn("Error in checking Simulator boot status.")
287                 time.sleep(1)
288
289     @staticmethod
290     def wait_until_device_is_in_state(udid, wait_until_state, timeout_seconds=60 * 5):
291         with timeout(seconds=timeout_seconds):
292             while (Simulator.device_state(udid) != wait_until_state):
293                 time.sleep(0.5)
294
295     @staticmethod
296     def device_state(udid):
297         device_plist = os.path.join(Simulator.device_directory(udid), 'device.plist')
298         if not os.path.isfile(device_plist):
299             return Simulator.DeviceState.DOES_NOT_EXIST
300         return plistlib.readPlist(device_plist)['state']
301
302     @staticmethod
303     def device_directory(udid):
304         return os.path.realpath(os.path.expanduser(os.path.join('~/Library/Developer/CoreSimulator/Devices', udid)))
305
306     def delete_device(self, udid):
307         Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.SHUTDOWN)
308         Device.delete(udid)
309
310     def refresh(self):
311         """
312         Refresh runtime and device type information from ``simctl list``.
313         """
314         lines = self._host.platform.xcode_simctl_list()
315         device_types_header = next(lines)
316         if device_types_header != '== Device Types ==':
317             raise RuntimeError('Expected == Device Types == header but got: "{}"'.format(device_types_header))
318         self._parse_device_types(lines)
319
320     def _parse_device_types(self, lines):
321         """
322         Parse device types from ``simctl list``.
323         :param lines: A generator for the output lines from ``simctl list``.
324         :type lines: genexpr
325         :return: None
326         """
327         for line in lines:
328             device_type_match = self.device_type_re.match(line)
329             if not device_type_match:
330                 if line != '== Runtimes ==':
331                     raise RuntimeError('Expected == Runtimes == header but got: "{}"'.format(line))
332                 break
333             device_type = DeviceType(name=device_type_match.group('name').rstrip(),
334                                      identifier=device_type_match.group('identifier'))
335             self.device_types.append(device_type)
336
337         self._parse_runtimes(lines)
338
339     def _parse_runtimes(self, lines):
340         """
341         Continue to parse runtimes from ``simctl list``.
342         :param lines: A generator for the output lines from ``simctl list``.
343         :type lines: genexpr
344         :return: None
345         """
346         for line in lines:
347             runtime_match = self.runtime_re.match(line)
348             if not runtime_match:
349                 if line != '== Devices ==':
350                     raise RuntimeError('Expected == Devices == header but got: "{}"'.format(line))
351                 break
352             version = tuple(map(int, runtime_match.group('version').split('.')))
353             runtime = Runtime(version=version,
354                               identifier=runtime_match.group('identifier'),
355                               available=runtime_match.group('availability') is None,
356                               is_internal_runtime=bool(runtime_match.group('internal')))
357             self.runtimes.append(runtime)
358         self._parse_devices(lines)
359
360     def _parse_devices(self, lines):
361         """
362         Continue to parse devices from ``simctl list``.
363         :param lines: A generator for the output lines from ``simctl list``.
364         :type lines: genexpr
365         :return: None
366         """
367         current_runtime = None
368         for line in lines:
369             version_match = self.version_re.match(line)
370             if version_match:
371                 version = tuple(map(int, version_match.group('version').split('.')))
372                 current_runtime = self.runtime(version=version, is_internal_runtime=bool(version_match.group('internal')))
373                 assert current_runtime
374                 continue
375
376             unavailable_version_match = self.unavailable_version_re.match(line)
377             if unavailable_version_match:
378                 current_runtime = None
379                 continue
380
381             device_match = self.devices_re.match(line)
382             if not device_match:
383                 if line != '== Device Pairs ==':
384                     raise RuntimeError('Expected == Device Pairs == header but got: "{}"'.format(line))
385                 break
386             if current_runtime:
387                 device = Device(name=device_match.group('name').rstrip(),
388                                 udid=device_match.group('udid'),
389                                 available=device_match.group('availability') is None,
390                                 runtime=current_runtime)
391                 current_runtime.devices.append(device)
392
393     def device_type(self, name=None, identifier=None):
394         """
395         :param name: The short name of the device type.
396         :type name: str
397         :param identifier: The CoreSimulator identifier of the desired device type.
398         :type identifier: str
399         :return: A device type with the specified name and/or identifier, or None if one doesn't exist as such.
400         :rtype: DeviceType
401         """
402         for device_type in self.device_types:
403             if name and device_type.name != name:
404                 continue
405             if identifier and device_type.identifier != identifier:
406                 continue
407             return device_type
408         return None
409
410     def runtime(self, version=None, identifier=None, is_internal_runtime=None):
411         """
412         :param version: The iOS version of the desired runtime.
413         :type version: tuple
414         :param identifier: The CoreSimulator identifier of the desired runtime.
415         :type identifier: str
416         :return: A runtime with the specified version and/or identifier, or None if one doesn't exist as such.
417         :rtype: Runtime or None
418         """
419         if version is None and identifier is None:
420             raise TypeError('Must supply version and/or identifier.')
421
422         for runtime in self.runtimes:
423             if version and runtime.version != version:
424                 continue
425             if is_internal_runtime and runtime.is_internal_runtime != is_internal_runtime:
426                 continue
427             if identifier and runtime.identifier != identifier:
428                 continue
429             return runtime
430         return None
431
432     def find_device_by_udid(self, udid):
433         """
434         :param udid: The UDID of the device to find.
435         :type udid: str
436         :return: The `Device` with the specified UDID.
437         :rtype: Device
438         """
439         for device in self.devices:
440             if device.udid == udid:
441                 return device
442         return None
443
444     # FIXME: We should find an existing device with respect to its name, device type and runtime.
445     def device(self, name=None, runtime=None, should_ignore_unavailable_devices=False):
446         """
447         :param name: The name of the desired device.
448         :type name: str
449         :param runtime: The runtime of the desired device.
450         :type runtime: Runtime
451         :return: A device with the specified name and/or runtime, or None if one doesn't exist as such
452         :rtype: Device or None
453         """
454         if name is None and runtime is None:
455             raise TypeError('Must supply name and/or runtime.')
456
457         for device in self.devices:
458             if should_ignore_unavailable_devices and not device.available:
459                 continue
460             if name and device.name != name:
461                 continue
462             if runtime and device.runtime != runtime:
463                 continue
464             return device
465         return None
466
467     @property
468     def available_runtimes(self):
469         """
470         :return: An iterator of all available runtimes.
471         :rtype: iter
472         """
473         return itertools.ifilter(lambda runtime: runtime.available, self.runtimes)
474
475     @property
476     def devices(self):
477         """
478         :return: An iterator of all devices from all runtimes.
479         :rtype: iter
480         """
481         return itertools.chain(*[runtime.devices for runtime in self.runtimes])
482
483     @property
484     def latest_available_runtime(self):
485         """
486         :return: Returns a Runtime object with the highest version.
487         :rtype: Runtime or None
488         """
489         if not self.runtimes:
490             return None
491         return sorted(self.available_runtimes, key=lambda runtime: runtime.version, reverse=True)[0]
492
493     def lookup_or_create_device(self, name, device_type, runtime):
494         """
495         Returns an available iOS Simulator device for testing.
496
497         This function will create a new simulator device with the specified name,
498         device type and runtime if one does not already exist.
499
500         :param name: The name of the simulator device to lookup or create.
501         :type name: str
502         :param device_type: The CoreSimulator device type.
503         :type device_type: DeviceType
504         :param runtime: The CoreSimulator runtime.
505         :type runtime: Runtime
506         :return: A dictionary describing the device.
507         :rtype: Device
508         """
509         assert(runtime.available)
510         testing_device = self.device(name=name, runtime=runtime, should_ignore_unavailable_devices=True)
511         if testing_device:
512             return testing_device
513         testing_device = Device.create(name, device_type, runtime)
514         assert(testing_device.available)
515         return testing_device
516
517     def __repr__(self):
518         return '<iOS Simulator: {num_runtimes} runtimes, {num_device_types} device types>'.format(
519             num_runtimes=len(self.runtimes),
520             num_device_types=len(self.device_types))
521
522     def __str__(self):
523         description = ['iOS Simulator:']
524         description += map(str, self.runtimes)
525         description += map(str, self.device_types)
526         description += map(str, self.devices)
527         return '\n'.join(description)