Workaround for simctl launch bug
[WebKit.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
44 class DeviceType(object):
45     """
46     Represents a CoreSimulator device type.
47     """
48     def __init__(self, name, identifier):
49         """
50         :param name: The device type's human-readable name
51         :type name: str
52         :param identifier: The CoreSimulator identifier.
53         :type identifier: str
54         """
55         self.name = name
56         self.identifier = identifier
57
58     @classmethod
59     def from_name(cls, name):
60         """
61         :param name: The name for the desired device type.
62         :type name: str
63         :returns: A `DeviceType` object with the specified identifier or throws a TypeError if it doesn't exist.
64         :rtype: DeviceType
65         """
66         identifier = None
67         for device_type in Simulator().device_types:
68             if device_type.name == name:
69                 identifier = device_type.identifier
70                 break
71
72         if identifier is None:
73             raise TypeError('A device type with name "{name}" does not exist.'.format(name=name))
74
75         return DeviceType(name, identifier)
76
77     @classmethod
78     def from_identifier(cls, identifier):
79         """
80         :param identifier: The CoreSimulator identifier for the desired runtime.
81         :type identifier: str
82         :returns: A `Runtime` object witht the specified identifier or throws a TypeError if it doesn't exist.
83         :rtype: DeviceType
84         """
85         name = None
86         for device_type in Simulator().device_types:
87             if device_type.identifier == identifier:
88                 name = device_type.name
89                 break
90
91         if name is None:
92             raise TypeError('A device type with identifier "{identifier}" does not exist.'.format(
93                 identifier=identifier))
94
95         return DeviceType(name, identifier)
96
97     def __eq__(self, other):
98         return (self.name == other.name) and (self.identifier == other.identifier)
99
100     def __ne__(self, other):
101         return not self.__eq__(other)
102
103     def __repr__(self):
104         return '<DeviceType "{name}": {identifier}>'.format(name=self.name, identifier=self.identifier)
105
106
107 class Runtime(object):
108     """
109     Represents a CoreSimulator runtime associated with an iOS SDK.
110     """
111
112     def __init__(self, version, identifier, available, devices=None, is_internal_runtime=False):
113         """
114         :param version: The iOS SDK version
115         :type version: tuple
116         :param identifier: The CoreSimulator runtime identifier
117         :type identifier: str
118         :param availability: Whether the runtime is available for use.
119         :type availability: bool
120         :param devices: A list of devices under this runtime
121         :type devices: list or None
122         :param is_internal_runtime: Whether the runtime is an Apple internal runtime
123         :type is_internal_runtime: bool
124         """
125         self.version = version
126         self.identifier = identifier
127         self.available = available
128         self.devices = devices or []
129         self.is_internal_runtime = is_internal_runtime
130
131     @classmethod
132     def from_version_string(cls, version):
133         return cls.from_identifier('com.apple.CoreSimulator.SimRuntime.iOS-' + version.replace('.', '-'))
134
135     @classmethod
136     def from_identifier(cls, identifier):
137         """
138         :param identifier: The identifier for the desired CoreSimulator runtime.
139         :type identifier: str
140         :returns: A `Runtime` object with the specified identifier or throws a TypeError if it doesn't exist.
141         :rtype: Runtime
142         """
143         for runtime in Simulator().runtimes:
144             if runtime.identifier == identifier:
145                 return runtime
146         raise TypeError('A runtime with identifier "{identifier}" does not exist.'.format(identifier=identifier))
147
148     def __eq__(self, other):
149         return (self.version == other.version) and (self.identifier == other.identifier) and (self.is_internal_runtime == other.is_internal_runtime)
150
151     def __ne__(self, other):
152         return not self.__eq__(other)
153
154     def __repr__(self):
155         version_suffix = ""
156         if self.is_internal_runtime:
157             version_suffix = " Internal"
158         return '<Runtime {version}: {identifier}. Available: {available}, {num_devices} devices>'.format(
159             version='.'.join(map(str, self.version)) + version_suffix,
160             identifier=self.identifier,
161             available=self.available,
162             num_devices=len(self.devices))
163
164
165 class Device(object):
166     """
167     Represents a CoreSimulator device underneath a runtime
168     """
169
170     def __init__(self, name, udid, available, runtime, host):
171         """
172         :param name: The device name
173         :type name: str
174         :param udid: The device UDID (a UUID string)
175         :type udid: str
176         :param available: Whether the device is available for use.
177         :type available: bool
178         :param runtime: The iOS Simulator runtime that hosts this device
179         :type runtime: Runtime
180         :param host: The host which can run command line commands
181         :type host: Host
182         """
183         self._host = host
184         self.name = name
185         self.udid = udid
186         self.available = available
187         self.runtime = runtime
188
189     @property
190     def state(self):
191         """
192         :returns: The current state of the device.
193         :rtype: Simulator.DeviceState
194         """
195         return Simulator.device_state(self.udid)
196
197     @property
198     def path(self):
199         """
200         :returns: The filesystem path that contains the simulator device's data.
201         :rtype: str
202         """
203         return Simulator.device_directory(self.udid)
204
205     @classmethod
206     def create(cls, name, device_type, runtime):
207         """
208         Create a new CoreSimulator device.
209         :param name: The name of the device.
210         :type name: str
211         :param device_type: The CoreSimulatort device type.
212         :type device_type: DeviceType
213         :param runtime:  The CoreSimualtor runtime.
214         :type runtime: Runtime
215         :return: The new device or raises a CalledProcessError if ``simctl create`` failed.
216         :rtype: Device
217         """
218         device_udid = subprocess.check_output(['xcrun', 'simctl', 'create', name, device_type.identifier, runtime.identifier]).rstrip()
219         _log.debug('"xcrun simctl create %s %s %s" returned %s', name, device_type.identifier, runtime.identifier, device_udid)
220         Simulator.wait_until_device_is_in_state(device_udid, Simulator.DeviceState.SHUTDOWN)
221         return Simulator().find_device_by_udid(device_udid)
222
223     @classmethod
224     def shutdown(cls, udid):
225         """
226         Shut down the given CoreSimulator device.
227         :param udid: The udid of the device.
228         :type udid: str
229         """
230         device_state = Simulator.device_state(udid)
231         if device_state == Simulator.DeviceState.BOOTING or device_state == Simulator.DeviceState.BOOTED:
232             _log.debug('xcrun simctl shutdown %s', udid)
233             # Don't throw on error. Device shutdown seems to be racy with Simulator app killing.
234             subprocess.call(['xcrun', 'simctl', 'shutdown', udid])
235
236         Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.SHUTDOWN)
237
238     @classmethod
239     def delete(cls, udid):
240         """
241         Delete the given CoreSimulator device.
242         :param udid: The udid of the device.
243         :type udid: str
244         """
245         Device.shutdown(udid)
246         try:
247             _log.debug('xcrun simctl delete %s', udid)
248             subprocess.check_call(['xcrun', 'simctl', 'delete', udid])
249         except subprocess.CalledProcessError:
250             raise RuntimeError('"xcrun simctl delete" failed: device state is {}'.format(Simulator.device_state(udid)))
251
252     @classmethod
253     def reset(cls, udid):
254         """
255         Reset the given CoreSimulator device.
256         :param udid: The udid of the device.
257         :type udid: str
258         """
259         Device.shutdown(udid)
260         try:
261             _log.debug('xcrun simctl erase %s', udid)
262             subprocess.check_call(['xcrun', 'simctl', 'erase', udid])
263         except subprocess.CalledProcessError:
264             raise RuntimeError('"xcrun simctl erase" failed: device state is {}'.format(Simulator.device_state(udid)))
265
266     def install_app(self, app_path):
267         return not self._host.executive.run_command(['xcrun', 'simctl', 'install', self.udid, app_path], return_exit_code=True)
268
269     def launch_app(self, bundle_id, args, env=None):
270         environment_to_use = {}
271         SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'
272         for value in (env or {}):
273             if not value.startswith(SIMCTL_ENV_PREFIX):
274                 environment_to_use[SIMCTL_ENV_PREFIX + value] = env[value]
275             else:
276                 environment_to_use[value] = env[value]
277
278         # FIXME: This is a workaround for <rdar://problem/30273973>, Racey failure of simctl launch.
279         @staticmethod
280         def _log_debug_error(error):
281             _log.debug(error.message_with_output())
282
283         output = None
284         for x in xrange(3):
285             output = self._host.executive.run_command(
286                 ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
287                 env=environment_to_use,
288                 error_handler=_log_debug_error,
289             )
290             match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
291             if match:
292                 break
293
294         if not match or match.group('bundle') != bundle_id:
295             raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
296         return int(match.group('pid'))
297
298     def terminate_app(self, bundle_id):
299         return not self._host.executive.run_command(['xcrun', 'simctl', 'terminate', self.udid, bundle_id], return_exit_code=True)
300
301     def __eq__(self, other):
302         return self.udid == other.udid
303
304     def __ne__(self, other):
305         return not self.__eq__(other)
306
307     def __repr__(self):
308         return '<Device "{name}": {udid}. State: {state}. Runtime: {runtime}, Available: {available}>'.format(
309             name=self.name,
310             udid=self.udid,
311             state=self.state,
312             available=self.available,
313             runtime=self.runtime.identifier)
314
315
316 # FIXME: This class is fragile because it parses the output of the simctl command line utility, which may change.
317 #        We should find a better way to query for simulator device state and capabilities. Maybe take a similiar
318 #        approach as in webkitdirs.pm and utilize the parsed output from the device.plist files in the sub-
319 #        directories of ~/Library/Developer/CoreSimulator/Devices?
320 #        Also, simctl has the option to output in JSON format (xcrun simctl list --json).
321 class Simulator(object):
322     """
323     Represents the iOS Simulator infrastructure under the currently select Xcode.app bundle.
324     """
325     device_type_re = re.compile('(?P<name>[^(]+)\((?P<identifier>[^)]+)\)')
326     # FIXME: runtime_re parses the version from the runtime name, but that does not contain the full version number
327     # (it can omit the revision). We should instead parse the version from the number contained in parentheses.
328     runtime_re = re.compile(
329         '(i|watch|tv)OS (?P<version>\d+\.\d)(?P<internal> Internal)? \(\d+\.\d+(\.\d+)? - (?P<build_version>[^)]+)\) \((?P<identifier>[^)]+)\)( \((?P<availability>[^)]+)\))?')
330     unavailable_version_re = re.compile('-- Unavailable: (?P<identifier>[^ ]+) --')
331     version_re = re.compile('-- (i|watch|tv)OS (?P<version>\d+\.\d+)(?P<internal> Internal)? --')
332     devices_re = re.compile(
333         '\s*(?P<name>[^(]+ )\((?P<udid>[^)]+)\) \((?P<state>[^)]+)\)( \((?P<availability>[^)]+)\))?')
334
335     _managed_devices = {}
336
337     def __init__(self, host=None):
338         self._host = host or Host()
339         self.runtimes = []
340         self.device_types = []
341         self.refresh()
342
343     # Keep these constants synchronized with the SimDeviceState constants in CoreSimulator/SimDevice.h.
344     class DeviceState:
345         DOES_NOT_EXIST = -1
346         CREATING = 0
347         SHUTDOWN = 1
348         BOOTING = 2
349         BOOTED = 3
350         SHUTTING_DOWN = 4
351
352     NAME_FOR_STATE = [
353         'CREATING',
354         'SHUTDOWN',
355         'BOOTING',
356         'BOOTED',
357         'SHUTTING_DOWN'
358     ]
359
360     @staticmethod
361     def create_device(number, device_type, runtime):
362         device = Simulator().lookup_or_create_device(device_type.name + ' WebKit Tester' + str(number), device_type, runtime)
363         _log.debug('created device {} {}'.format(number, device))
364         assert(len(Simulator._managed_devices) == number)
365         Simulator._managed_devices[number] = device
366
367     @staticmethod
368     def remove_device(number):
369         if not Simulator._managed_devices[number]:
370             return
371         device_udid = Simulator._managed_devices[number].udid
372         _log.debug('removing device {} {}'.format(number, device_udid))
373         del Simulator._managed_devices[number]
374         Simulator.delete_device(device_udid)
375
376     @staticmethod
377     def device_number(number):
378         return Simulator._managed_devices[number]
379
380     @staticmethod
381     def device_state_description(state):
382         if (state == Simulator.DeviceState.DOES_NOT_EXIST):
383             return 'DOES_NOT_EXIST'
384         return Simulator.NAME_FOR_STATE[state]
385
386     @staticmethod
387     def wait_until_device_is_booted(udid, timeout_seconds=60 * 5):
388         Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.BOOTED, timeout_seconds)
389         with timeout(seconds=timeout_seconds):
390             while True:
391                 try:
392                     state = subprocess.check_output(['xcrun', 'simctl', 'spawn', udid, 'launchctl', 'print', 'system']).strip()
393                     _log.debug('xcrun simctl spawn %s', udid)
394
395                     if re.search("A[\s]+com.apple.springboard.services", state):
396                         return
397                 except subprocess.CalledProcessError:
398                     if Simulator.device_state(udid) != Simulator.DeviceState.BOOTED:
399                         raise RuntimeError('Simuator device quit unexpectedly.')
400                     _log.warn("Error in checking Simulator boot status. Will retry in 1 second.")
401                 time.sleep(1)
402
403     @staticmethod
404     def wait_until_device_is_in_state(udid, wait_until_state, timeout_seconds=60 * 5):
405         _log.debug('waiting for device %s to enter state %s with timeout %s', udid, Simulator.device_state_description(wait_until_state), timeout_seconds)
406         with timeout(seconds=timeout_seconds):
407             device_state = Simulator.device_state(udid)
408             while (device_state != wait_until_state):
409                 device_state = Simulator.device_state(udid)
410                 _log.debug(' device state %s', Simulator.device_state_description(device_state))
411                 time.sleep(0.5)
412
413         end_state = Simulator.device_state(udid)
414         if (end_state != wait_until_state):
415             raise RuntimeError('Timed out waiting for simulator device to enter state {0}; current state is {1}'.format(Simulator.device_state_description(wait_until_state), Simulator.device_state_description(end_state)))
416
417     @staticmethod
418     def device_state(udid):
419         device_plist = os.path.join(Simulator.device_directory(udid), 'device.plist')
420         if not os.path.isfile(device_plist):
421             return Simulator.DeviceState.DOES_NOT_EXIST
422         return plistlib.readPlist(device_plist)['state']
423
424     @staticmethod
425     def device_directory(udid):
426         return os.path.realpath(os.path.expanduser(os.path.join('~/Library/Developer/CoreSimulator/Devices', udid)))
427
428     @staticmethod
429     def delete_device(udid):
430         Device.delete(udid)
431
432     @staticmethod
433     def reset_device(udid):
434         Device.reset(udid)
435
436     def refresh(self):
437         """
438         Refresh runtime and device type information from ``simctl list``.
439         """
440         lines = self._host.platform.xcode_simctl_list()
441         if not lines:
442             return
443         device_types_header = next(lines)
444         if device_types_header != '== Device Types ==':
445             raise RuntimeError('Expected == Device Types == header but got: "{}"'.format(device_types_header))
446         self._parse_device_types(lines)
447
448     def _parse_device_types(self, lines):
449         """
450         Parse device types from ``simctl list``.
451         :param lines: A generator for the output lines from ``simctl list``.
452         :type lines: genexpr
453         :return: None
454         """
455         for line in lines:
456             device_type_match = self.device_type_re.match(line)
457             if not device_type_match:
458                 if line != '== Runtimes ==':
459                     raise RuntimeError('Expected == Runtimes == header but got: "{}"'.format(line))
460                 break
461             device_type = DeviceType(name=device_type_match.group('name').rstrip(),
462                                      identifier=device_type_match.group('identifier'))
463             self.device_types.append(device_type)
464
465         self._parse_runtimes(lines)
466
467     def _parse_runtimes(self, lines):
468         """
469         Continue to parse runtimes from ``simctl list``.
470         :param lines: A generator for the output lines from ``simctl list``.
471         :type lines: genexpr
472         :return: None
473         """
474         for line in lines:
475             runtime_match = self.runtime_re.match(line)
476             if not runtime_match:
477                 if line != '== Devices ==':
478                     raise RuntimeError('Expected == Devices == header but got: "{}"'.format(line))
479                 break
480             version = tuple(map(int, runtime_match.group('version').split('.')))
481             runtime = Runtime(version=version,
482                               identifier=runtime_match.group('identifier'),
483                               available=runtime_match.group('availability') is None,
484                               is_internal_runtime=bool(runtime_match.group('internal')))
485             self.runtimes.append(runtime)
486         self._parse_devices(lines)
487
488     def _parse_devices(self, lines):
489         """
490         Continue to parse devices from ``simctl list``.
491         :param lines: A generator for the output lines from ``simctl list``.
492         :type lines: genexpr
493         :return: None
494         """
495         current_runtime = None
496         for line in lines:
497             version_match = self.version_re.match(line)
498             if version_match:
499                 version = tuple(map(int, version_match.group('version').split('.')))
500                 current_runtime = self.runtime(version=version, is_internal_runtime=bool(version_match.group('internal')))
501                 assert current_runtime
502                 continue
503
504             unavailable_version_match = self.unavailable_version_re.match(line)
505             if unavailable_version_match:
506                 current_runtime = None
507                 continue
508
509             device_match = self.devices_re.match(line)
510             if not device_match:
511                 if line != '== Device Pairs ==':
512                     raise RuntimeError('Expected == Device Pairs == header but got: "{}"'.format(line))
513                 break
514             if current_runtime:
515                 device = Device(name=device_match.group('name').rstrip(),
516                                 udid=device_match.group('udid'),
517                                 available=device_match.group('availability') is None,
518                                 runtime=current_runtime,
519                                 host=self._host)
520                 current_runtime.devices.append(device)
521
522     def device_type(self, name=None, identifier=None):
523         """
524         :param name: The short name of the device type.
525         :type name: str
526         :param identifier: The CoreSimulator identifier of the desired device type.
527         :type identifier: str
528         :return: A device type with the specified name and/or identifier, or None if one doesn't exist as such.
529         :rtype: DeviceType
530         """
531         for device_type in self.device_types:
532             if name and device_type.name != name:
533                 continue
534             if identifier and device_type.identifier != identifier:
535                 continue
536             return device_type
537         return None
538
539     def runtime(self, version=None, identifier=None, is_internal_runtime=None):
540         """
541         :param version: The iOS version of the desired runtime.
542         :type version: tuple
543         :param identifier: The CoreSimulator identifier of the desired runtime.
544         :type identifier: str
545         :return: A runtime with the specified version and/or identifier, or None if one doesn't exist as such.
546         :rtype: Runtime or None
547         """
548         if version is None and identifier is None:
549             raise TypeError('Must supply version and/or identifier.')
550
551         for runtime in self.runtimes:
552             if version and runtime.version != version:
553                 continue
554             if is_internal_runtime and runtime.is_internal_runtime != is_internal_runtime:
555                 continue
556             if identifier and runtime.identifier != identifier:
557                 continue
558             return runtime
559         return None
560
561     def find_device_by_udid(self, udid):
562         """
563         :param udid: The UDID of the device to find.
564         :type udid: str
565         :return: The `Device` with the specified UDID.
566         :rtype: Device
567         """
568         for device in self.devices:
569             if device.udid == udid:
570                 return device
571         return None
572
573     def current_device(self):
574         # FIXME: Find the simulator device that was booted by Simulator.app. For now, pick some booted simulator device, which
575         # may have been booted using the simctl command line tool.
576         for device in self.devices:
577             if device.state == Simulator.DeviceState.BOOTED:
578                 return device
579         return None
580
581     # FIXME: We should find an existing device with respect to its name, device type and runtime.
582     def device(self, name=None, runtime=None, should_ignore_unavailable_devices=False):
583         """
584         :param name: The name of the desired device.
585         :type name: str
586         :param runtime: The runtime of the desired device.
587         :type runtime: Runtime
588         :return: A device with the specified name and/or runtime, or None if one doesn't exist as such
589         :rtype: Device or None
590         """
591         if name is None and runtime is None:
592             raise TypeError('Must supply name and/or runtime.')
593
594         for device in self.devices:
595             if should_ignore_unavailable_devices and not device.available:
596                 continue
597             if name and device.name != name:
598                 continue
599             if runtime and device.runtime != runtime:
600                 continue
601             return device
602         return None
603
604     @property
605     def available_runtimes(self):
606         """
607         :return: An iterator of all available runtimes.
608         :rtype: iter
609         """
610         return itertools.ifilter(lambda runtime: runtime.available, self.runtimes)
611
612     @property
613     def devices(self):
614         """
615         :return: An iterator of all devices from all runtimes.
616         :rtype: iter
617         """
618         return itertools.chain(*[runtime.devices for runtime in self.runtimes])
619
620     @property
621     def latest_available_runtime(self):
622         """
623         :return: Returns a Runtime object with the highest version.
624         :rtype: Runtime or None
625         """
626         if not self.runtimes:
627             return None
628         return sorted(self.available_runtimes, key=lambda runtime: runtime.version, reverse=True)[0]
629
630     def lookup_or_create_device(self, name, device_type, runtime):
631         """
632         Returns an available iOS Simulator device for testing.
633
634         This function will create a new simulator device with the specified name,
635         device type and runtime if one does not already exist.
636
637         :param name: The name of the simulator device to lookup or create.
638         :type name: str
639         :param device_type: The CoreSimulator device type.
640         :type device_type: DeviceType
641         :param runtime: The CoreSimulator runtime.
642         :type runtime: Runtime
643         :return: A dictionary describing the device.
644         :rtype: Device
645         """
646         assert(runtime.available)
647         testing_device = self.device(name=name, runtime=runtime, should_ignore_unavailable_devices=True)
648         if testing_device:
649             _log.debug('lookup_or_create_device %s %s %s found %s', name, device_type, runtime, testing_device.name)
650             return testing_device
651         testing_device = Device.create(name, device_type, runtime)
652         _log.debug('lookup_or_create_device %s %s %s created %s', name, device_type, runtime, testing_device.name)
653         assert(testing_device.available)
654         return testing_device
655
656     def __repr__(self):
657         return '<iOS Simulator: {num_runtimes} runtimes, {num_device_types} device types>'.format(
658             num_runtimes=len(self.runtimes),
659             num_device_types=len(self.device_types))
660
661     def __str__(self):
662         description = ['iOS Simulator:']
663         description += map(str, self.runtimes)
664         description += map(str, self.device_types)
665         description += map(str, self.devices)
666         return '\n'.join(description)