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