error running layout tests on iOS simulator on latest build
[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                 state = subprocess.check_output(['xcrun', 'simctl', 'spawn', udid, 'launchctl', 'print', 'system']).strip()
282                 if re.search("A[\s]+com.apple.springboard.services", state):
283                     return
284                 time.sleep(1)
285
286     @staticmethod
287     def wait_until_device_is_in_state(udid, wait_until_state, timeout_seconds=60 * 5):
288         with timeout(seconds=timeout_seconds):
289             while (Simulator.device_state(udid) != wait_until_state):
290                 time.sleep(0.5)
291
292     @staticmethod
293     def device_state(udid):
294         device_plist = os.path.join(Simulator.device_directory(udid), 'device.plist')
295         if not os.path.isfile(device_plist):
296             return Simulator.DeviceState.DOES_NOT_EXIST
297         return plistlib.readPlist(device_plist)['state']
298
299     @staticmethod
300     def device_directory(udid):
301         return os.path.realpath(os.path.expanduser(os.path.join('~/Library/Developer/CoreSimulator/Devices', udid)))
302
303     def delete_device(self, udid):
304         Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.SHUTDOWN)
305         Device.delete(udid)
306
307     def refresh(self):
308         """
309         Refresh runtime and device type information from ``simctl list``.
310         """
311         lines = self._host.platform.xcode_simctl_list()
312         device_types_header = next(lines)
313         if device_types_header != '== Device Types ==':
314             raise RuntimeError('Expected == Device Types == header but got: "{}"'.format(device_types_header))
315         self._parse_device_types(lines)
316
317     def _parse_device_types(self, lines):
318         """
319         Parse device types from ``simctl list``.
320         :param lines: A generator for the output lines from ``simctl list``.
321         :type lines: genexpr
322         :return: None
323         """
324         for line in lines:
325             device_type_match = self.device_type_re.match(line)
326             if not device_type_match:
327                 if line != '== Runtimes ==':
328                     raise RuntimeError('Expected == Runtimes == header but got: "{}"'.format(line))
329                 break
330             device_type = DeviceType(name=device_type_match.group('name').rstrip(),
331                                      identifier=device_type_match.group('identifier'))
332             self.device_types.append(device_type)
333
334         self._parse_runtimes(lines)
335
336     def _parse_runtimes(self, lines):
337         """
338         Continue to parse runtimes from ``simctl list``.
339         :param lines: A generator for the output lines from ``simctl list``.
340         :type lines: genexpr
341         :return: None
342         """
343         for line in lines:
344             runtime_match = self.runtime_re.match(line)
345             if not runtime_match:
346                 if line != '== Devices ==':
347                     raise RuntimeError('Expected == Devices == header but got: "{}"'.format(line))
348                 break
349             version = tuple(map(int, runtime_match.group('version').split('.')))
350             runtime = Runtime(version=version,
351                               identifier=runtime_match.group('identifier'),
352                               available=runtime_match.group('availability') is None,
353                               is_internal_runtime=bool(runtime_match.group('internal')))
354             self.runtimes.append(runtime)
355         self._parse_devices(lines)
356
357     def _parse_devices(self, lines):
358         """
359         Continue to parse devices from ``simctl list``.
360         :param lines: A generator for the output lines from ``simctl list``.
361         :type lines: genexpr
362         :return: None
363         """
364         current_runtime = None
365         for line in lines:
366             version_match = self.version_re.match(line)
367             if version_match:
368                 version = tuple(map(int, version_match.group('version').split('.')))
369                 current_runtime = self.runtime(version=version, is_internal_runtime=bool(version_match.group('internal')))
370                 assert current_runtime
371                 continue
372
373             unavailable_version_match = self.unavailable_version_re.match(line)
374             if unavailable_version_match:
375                 current_runtime = None
376                 continue
377
378             device_match = self.devices_re.match(line)
379             if not device_match:
380                 if line != '== Device Pairs ==':
381                     raise RuntimeError('Expected == Device Pairs == header but got: "{}"'.format(line))
382                 break
383             if current_runtime:
384                 device = Device(name=device_match.group('name').rstrip(),
385                                 udid=device_match.group('udid'),
386                                 available=device_match.group('availability') is None,
387                                 runtime=current_runtime)
388                 current_runtime.devices.append(device)
389
390     def device_type(self, name=None, identifier=None):
391         """
392         :param name: The short name of the device type.
393         :type name: str
394         :param identifier: The CoreSimulator identifier of the desired device type.
395         :type identifier: str
396         :return: A device type with the specified name and/or identifier, or None if one doesn't exist as such.
397         :rtype: DeviceType
398         """
399         for device_type in self.device_types:
400             if name and device_type.name != name:
401                 continue
402             if identifier and device_type.identifier != identifier:
403                 continue
404             return device_type
405         return None
406
407     def runtime(self, version=None, identifier=None, is_internal_runtime=None):
408         """
409         :param version: The iOS version of the desired runtime.
410         :type version: tuple
411         :param identifier: The CoreSimulator identifier of the desired runtime.
412         :type identifier: str
413         :return: A runtime with the specified version and/or identifier, or None if one doesn't exist as such.
414         :rtype: Runtime or None
415         """
416         if version is None and identifier is None:
417             raise TypeError('Must supply version and/or identifier.')
418
419         for runtime in self.runtimes:
420             if version and runtime.version != version:
421                 continue
422             if is_internal_runtime and runtime.is_internal_runtime != is_internal_runtime:
423                 continue
424             if identifier and runtime.identifier != identifier:
425                 continue
426             return runtime
427         return None
428
429     def find_device_by_udid(self, udid):
430         """
431         :param udid: The UDID of the device to find.
432         :type udid: str
433         :return: The `Device` with the specified UDID.
434         :rtype: Device
435         """
436         for device in self.devices:
437             if device.udid == udid:
438                 return device
439         return None
440
441     # FIXME: We should find an existing device with respect to its name, device type and runtime.
442     def device(self, name=None, runtime=None, should_ignore_unavailable_devices=False):
443         """
444         :param name: The name of the desired device.
445         :type name: str
446         :param runtime: The runtime of the desired device.
447         :type runtime: Runtime
448         :return: A device with the specified name and/or runtime, or None if one doesn't exist as such
449         :rtype: Device or None
450         """
451         if name is None and runtime is None:
452             raise TypeError('Must supply name and/or runtime.')
453
454         for device in self.devices:
455             if should_ignore_unavailable_devices and not device.available:
456                 continue
457             if name and device.name != name:
458                 continue
459             if runtime and device.runtime != runtime:
460                 continue
461             return device
462         return None
463
464     @property
465     def available_runtimes(self):
466         """
467         :return: An iterator of all available runtimes.
468         :rtype: iter
469         """
470         return itertools.ifilter(lambda runtime: runtime.available, self.runtimes)
471
472     @property
473     def devices(self):
474         """
475         :return: An iterator of all devices from all runtimes.
476         :rtype: iter
477         """
478         return itertools.chain(*[runtime.devices for runtime in self.runtimes])
479
480     @property
481     def latest_available_runtime(self):
482         """
483         :return: Returns a Runtime object with the highest version.
484         :rtype: Runtime or None
485         """
486         if not self.runtimes:
487             return None
488         return sorted(self.available_runtimes, key=lambda runtime: runtime.version, reverse=True)[0]
489
490     def lookup_or_create_device(self, name, device_type, runtime):
491         """
492         Returns an available iOS Simulator device for testing.
493
494         This function will create a new simulator device with the specified name,
495         device type and runtime if one does not already exist.
496
497         :param name: The name of the simulator device to lookup or create.
498         :type name: str
499         :param device_type: The CoreSimulator device type.
500         :type device_type: DeviceType
501         :param runtime: The CoreSimulator runtime.
502         :type runtime: Runtime
503         :return: A dictionary describing the device.
504         :rtype: Device
505         """
506         assert(runtime.available)
507         testing_device = self.device(name=name, runtime=runtime, should_ignore_unavailable_devices=True)
508         if testing_device:
509             return testing_device
510         testing_device = Device.create(name, device_type, runtime)
511         assert(testing_device.available)
512         return testing_device
513
514     def __repr__(self):
515         return '<iOS Simulator: {num_runtimes} runtimes, {num_device_types} device types>'.format(
516             num_runtimes=len(self.runtimes),
517             num_device_types=len(self.device_types))
518
519     def __str__(self):
520         description = ['iOS Simulator:']
521         description += map(str, self.runtimes)
522         description += map(str, self.device_types)
523         description += map(str, self.devices)
524         return '\n'.join(description)