<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[173937] trunk/Tools</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/173937">173937</a></dd>
<dt>Author</dt> <dd>dfarler@apple.com</dd>
<dt>Date</dt> <dd>2014-09-24 16:28:13 -0700 (Wed, 24 Sep 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>[iOS] run-webkit-tests should support minor versions under devices and create a testing device under the right runtime
https://bugs.webkit.org/show_bug.cgi?id=136895

Reviewed by David Kilzer.

Create Device, DeviceType, and Runtime data classes.
Create Simulator class represent simctl output.

Wherever possible, use structured data classes anywhere a raw
identifier or UDID string was used for a cleaner implementation
and to encapsulate the inherent fragility of scraping simctl output.

Create a suitably named testing device if one doesn't exist.

Finally, accurately handle having multiple simulator runtimes (SDKs)
installed in the active Xcode.app bundle.

* Scripts/webkitpy/layout_tests/run_webkit_tests.py:
(_set_up_derived_options):
* Scripts/webkitpy/port/driver.py:
(IOSSimulatorDriver.cmd_line):
Construct DeviceType and Runtime objects from identifiers passed at the
command line, still providing sensible defaults for 32- and 64-bit testing.
* Scripts/webkitpy/port/ios.py:
(IOSSimulatorPort.__init__):
(IOSSimulatorPort.setup_test_run):
(IOSSimulatorPort):
(IOSSimulatorPort.testing_device):
Cache the testing device once it is created or found.
(IOSSimulatorPort.reset_preferences):
Get the device path from the Device object instead of consructing it
in the port class.
(IOSSimulatorPort.simulator_udid): Deleted.
Get the UDID from the testing_device :: Device object itself.
* Scripts/webkitpy/xcode/simulator.py:
Created Device, DeviceType, Runtime, and Simulator classes.
(get_runtimes): Deleted.
(get_devices): Deleted.
(get_device_types): Deleted.
(get_latest_runtime): Deleted.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsScriptswebkitpylayout_testsrun_webkit_testspy">trunk/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py</a></li>
<li><a href="#trunkToolsScriptswebkitpyportdriverpy">trunk/Tools/Scripts/webkitpy/port/driver.py</a></li>
<li><a href="#trunkToolsScriptswebkitpyportiospy">trunk/Tools/Scripts/webkitpy/port/ios.py</a></li>
<li><a href="#trunkToolsScriptswebkitpyxcodesimulatorpy">trunk/Tools/Scripts/webkitpy/xcode/simulator.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (173936 => 173937)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog        2014-09-24 23:11:46 UTC (rev 173936)
+++ trunk/Tools/ChangeLog        2014-09-24 23:28:13 UTC (rev 173937)
</span><span class="lines">@@ -1,3 +1,46 @@
</span><ins>+2014-09-23  David Farler  &lt;dfarler@apple.com&gt;
+
+        [iOS] run-webkit-tests should support minor versions under devices and create a testing device under the right runtime
+        https://bugs.webkit.org/show_bug.cgi?id=136895
+
+        Reviewed by David Kilzer.
+
+        Create Device, DeviceType, and Runtime data classes.
+        Create Simulator class represent simctl output.
+
+        Wherever possible, use structured data classes anywhere a raw
+        identifier or UDID string was used for a cleaner implementation
+        and to encapsulate the inherent fragility of scraping simctl output.
+
+        Create a suitably named testing device if one doesn't exist.
+
+        Finally, accurately handle having multiple simulator runtimes (SDKs)
+        installed in the active Xcode.app bundle.
+
+        * Scripts/webkitpy/layout_tests/run_webkit_tests.py:
+        (_set_up_derived_options):
+        * Scripts/webkitpy/port/driver.py:
+        (IOSSimulatorDriver.cmd_line):
+        Construct DeviceType and Runtime objects from identifiers passed at the
+        command line, still providing sensible defaults for 32- and 64-bit testing.
+        * Scripts/webkitpy/port/ios.py:
+        (IOSSimulatorPort.__init__):
+        (IOSSimulatorPort.setup_test_run):
+        (IOSSimulatorPort):
+        (IOSSimulatorPort.testing_device):
+        Cache the testing device once it is created or found.
+        (IOSSimulatorPort.reset_preferences):
+        Get the device path from the Device object instead of consructing it
+        in the port class.
+        (IOSSimulatorPort.simulator_udid): Deleted.
+        Get the UDID from the testing_device :: Device object itself.
+        * Scripts/webkitpy/xcode/simulator.py:
+        Created Device, DeviceType, Runtime, and Simulator classes.
+        (get_runtimes): Deleted.
+        (get_devices): Deleted.
+        (get_device_types): Deleted.
+        (get_latest_runtime): Deleted.
+
</ins><span class="cx"> 2014-09-24  Roger Fong  &lt;roger_fong@apple.com&gt;
</span><span class="cx"> 
</span><span class="cx">         [Windows] Tentative fix for Windows test bots.
</span></span></pre></div>
<a id="trunkToolsScriptswebkitpylayout_testsrun_webkit_testspy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py (173936 => 173937)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py        2014-09-24 23:11:46 UTC (rev 173936)
+++ trunk/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py        2014-09-24 23:28:13 UTC (rev 173937)
</span><span class="lines">@@ -394,10 +394,15 @@
</span><span class="cx">     if options.platform == 'ios-simulator':
</span><span class="cx">         from webkitpy import xcode
</span><span class="cx">         if options.runtime is None:
</span><del>-            options.runtime = xcode.simulator.get_latest_runtime()['identifier']
</del><ins>+            options.runtime = xcode.simulator.Simulator().latest_runtime
+        else:
+            options.runtime = xcode.simulator.Runtime.from_identifier(options.runtime)
</ins><span class="cx">         if options.device_type is None:
</span><del>-            device_types = xcode.simulator.get_device_types()
-            options.device_type = device_types['iPhone 5'] if options.architecture == 'x86' else device_types['iPhone 5s']
</del><ins>+            iphone5 = xcode.simulator.DeviceType.from_name('iPhone 5')
+            iphone5s = xcode.simulator.DeviceType.from_name('iPhone 5s')
+            options.device_type = iphone5 if options.architecture == 'x86' else iphone5s
+        else:
+            options.device_type = xcode.simulator.DeviceType.from_identifier(options.device_type)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> def run(port, options, args, logging_stream):
</span></span></pre></div>
<a id="trunkToolsScriptswebkitpyportdriverpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/port/driver.py (173936 => 173937)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/port/driver.py        2014-09-24 23:11:46 UTC (rev 173936)
+++ trunk/Tools/Scripts/webkitpy/port/driver.py        2014-09-24 23:28:13 UTC (rev 173937)
</span><span class="lines">@@ -511,8 +511,8 @@
</span><span class="cx">         runtime = self._port.get_option('runtime')
</span><span class="cx">         device_type = self._port.get_option('device_type')
</span><span class="cx">         relay_args = [
</span><del>-            '-runtime', runtime,
-            '-deviceType', device_type,
</del><ins>+            '-runtime', runtime.identifier,
+            '-deviceType', device_type.identifier,
</ins><span class="cx">             '-suffix', str(self._worker_number),
</span><span class="cx">             '-productDir', product_dir,
</span><span class="cx">             '-app', dump_tool,
</span></span></pre></div>
<a id="trunkToolsScriptswebkitpyportiospy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/port/ios.py (173936 => 173937)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/port/ios.py        2014-09-24 23:11:46 UTC (rev 173936)
+++ trunk/Tools/Scripts/webkitpy/port/ios.py        2014-09-24 23:28:13 UTC (rev 173937)
</span><span class="lines">@@ -35,6 +35,7 @@
</span><span class="cx"> from webkitpy.port.base import Port
</span><span class="cx"> from webkitpy.port.leakdetector import LeakDetector
</span><span class="cx"> from webkitpy.port import config as port_config
</span><ins>+from webkitpy.xcode import simulator
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> _log = logging.getLogger(__name__)
</span><span class="lines">@@ -67,6 +68,8 @@
</span><span class="cx">         mac_config = port_config.Config(self._executive, self._filesystem, 'mac')
</span><span class="cx">         self._mac_build_directory = mac_config.build_directory(self.get_option('configuration'))
</span><span class="cx"> 
</span><ins>+        self._testing_device = None
+
</ins><span class="cx">     def driver_name(self):
</span><span class="cx">         if self.get_option('driver_name'):
</span><span class="cx">             return self.get_option('driver_name')
</span><span class="lines">@@ -157,7 +160,7 @@
</span><span class="cx">         time.sleep(2)
</span><span class="cx">         self._executive.run_command([
</span><span class="cx">             'open', '-a', os.path.join(self.developer_dir, 'Applications', 'iOS Simulator.app'),
</span><del>-            '--args', '-CurrentDeviceUDID', self.simulator_udid()])
</del><ins>+            '--args', '-CurrentDeviceUDID', self.testing_device.udid])
</ins><span class="cx"> 
</span><span class="cx">     def clean_up_test_run(self):
</span><span class="cx">         super(IOSSimulatorPort, self).clean_up_test_run()
</span><span class="lines">@@ -252,22 +255,16 @@
</span><span class="cx">             return stderr, None
</span><span class="cx">         return stderr, crash_log
</span><span class="cx"> 
</span><del>-    def simulator_udid(self):
-        device_name = self.get_option('device_type').split('.')[-1].replace('-', ' ') + ' WebKit Tester'
-        stdout = subprocess.check_output(['xcrun', '--sdk', 'iphonesimulator', 'simctl', 'list'])
-        lines = stdout.splitlines()
-        try:
-            devices_index = lines.index('== Devices ==')
-            device_regex = re.compile('(?P&lt;device_name&gt;[^(]+) \((?P&lt;udid&gt;[^)]+)\) \((?P&lt;state&gt;[^)]+)\)')
-            for device_line in itertools.takewhile(lambda line: not line.startswith('=='), lines[devices_index + 1:]):
-                device = device_regex.match(device_line.lstrip().rstrip())
-                if not device:
-                    continue
-                if device.group('device_name') == device_name:
-                    return device.group('udid')
-        except ValueError:
-            pass
</del><ins>+    @property
+    def testing_device(self):
+        if self._testing_device is not None:
+            return self._testing_device
</ins><span class="cx"> 
</span><ins>+        device_type = self.get_option('device_type')
+        runtime = self.get_option('runtime')
+        self._testing_device = simulator.Simulator().testing_device(device_type, runtime)
+        return self.testing_device
+
</ins><span class="cx">     def simulator_path(self, udid):
</span><span class="cx">         if udid:
</span><span class="cx">             return os.path.realpath(os.path.expanduser(os.path.join('~/Library/Developer/CoreSimulator/Devices', udid)))
</span><span class="lines">@@ -323,9 +320,7 @@
</span><span class="cx">         return self._image_differ.diff_image(expected_contents, actual_contents, tolerance)
</span><span class="cx"> 
</span><span class="cx">     def reset_preferences(self):
</span><del>-        simulator_path = self.simulator_path(self.simulator_udid())
-        if not simulator_path:
-            return
</del><ins>+        simulator_path = self.testing_device.path
</ins><span class="cx">         data_path = os.path.join(simulator_path, 'data')
</span><span class="cx">         if os.path.isdir(data_path):
</span><span class="cx">             shutil.rmtree(data_path)
</span></span></pre></div>
<a id="trunkToolsScriptswebkitpyxcodesimulatorpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/xcode/simulator.py (173936 => 173937)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/xcode/simulator.py        2014-09-24 23:11:46 UTC (rev 173936)
+++ trunk/Tools/Scripts/webkitpy/xcode/simulator.py        2014-09-24 23:28:13 UTC (rev 173937)
</span><span class="lines">@@ -1,6 +1,12 @@
</span><ins>+import itertools
+import logging
+import os
+import re
</ins><span class="cx"> import subprocess
</span><del>-import re
</del><ins>+import time
</ins><span class="cx"> 
</span><ins>+_log = logging.getLogger(__name__)
+
</ins><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> Minimally wraps CoreSimulator functionality through simctl.
</span><span class="cx"> 
</span><span class="lines">@@ -9,97 +15,389 @@
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def get_runtimes(only_available=True):
</del><ins>+class DeviceType(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Give a dictionary mapping
-    :return: A dictionary mapping iOS version string to runtime identifier.
-    :rtype: dict
</del><ins>+    Represents a CoreSimulator device type.
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    runtimes = {}
-    runtime_re = re.compile(b'iOS (?P&lt;version&gt;[0-9]+\.[0-9]) \([0-9]+\.[0-9]+ - (?P&lt;update&gt;[^)]+)\) \((?P&lt;identifier&gt;[^)]+)\)( \((?P&lt;unavailable&gt;[^)]+)\))?')
-    stdout = subprocess.check_output(['xcrun', '-sdk', 'iphonesimulator', 'simctl', 'list', 'runtimes'])
-    lines = iter(stdout.splitlines())
-    header = next(lines)
-    if header != '== Runtimes ==':
-        return None
</del><ins>+    def __init__(self, name, identifier):
+        &quot;&quot;&quot;
+        :param name: The device type's human-readable name
+        :type name: str
+        :param identifier: The CoreSimulator identifier.
+        :type identifier: str
+        &quot;&quot;&quot;
+        self.name = name
+        self.identifier = identifier
</ins><span class="cx"> 
</span><del>-    for line in lines:
-        runtime_match = runtime_re.match(line)
-        if not runtime_match:
-            continue
-        runtime = runtime_match.groupdict()
-        version = tuple([int(component) for component in runtime_match.group('version').split('.')])
-        runtime = {
-            'identifier': runtime['identifier'],
-            'available': runtime['unavailable'] is None,
-            'version': version,
-        }
-        if only_available and not runtime['available']:
-            continue
</del><ins>+    @classmethod
+    def from_name(cls, name):
+        &quot;&quot;&quot;
+        :param name: The name for the desired device type.
+        :type name: str
+        :returns: A `DeviceType` object with the specified identifier or throws a TypeError if it doesn't exist.
+        :rtype: DeviceType
+        &quot;&quot;&quot;
+        identifier = None
+        for device_type in Simulator().device_types:
+            if device_type.name == name:
+                identifier = device_type.identifier
+                break
</ins><span class="cx"> 
</span><del>-        runtimes[version] = runtime
</del><ins>+        if identifier is None:
+            raise TypeError('A device type with name &quot;{name}&quot; does not exist.'.format(name=name))
</ins><span class="cx"> 
</span><del>-    return runtimes
</del><ins>+        return DeviceType(name, identifier)
</ins><span class="cx"> 
</span><ins>+    @classmethod
+    def from_identifier(cls, identifier):
+        &quot;&quot;&quot;
+        :param identifier: The CoreSimulator identifier for the desired runtime.
+        :type identifier: str
+        :returns: A `Runtime` object witht the specified identifier or throws a TypeError if it doesn't exist.
+        :rtype: DeviceType
+        &quot;&quot;&quot;
+        name = None
+        for device_type in Simulator().device_types:
+            if device_type.identifier == identifier:
+                name = device_type.name
+                break
</ins><span class="cx"> 
</span><del>-def get_devices():
</del><ins>+        if name is None:
+            raise TypeError('A device type with identifier &quot;{identifier}&quot; does not exist.'.format(
+                identifier=identifier))
+
+        return DeviceType(name, identifier)
+
+    def __eq__(self, other):
+        return (self.name == other.name) and (self.identifier == other.identifier)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return '&lt;DeviceType &quot;{name}&quot;: {identifier}&gt;'.format(name=self.name, identifier=self.identifier)
+
+
+class Runtime(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    :return: A dictionary mapping iOS version to device hardware model, simulator UDID, and state.
-    :rtype: dict
</del><ins>+    Represents a CoreSimulator runtime associated with an iOS SDK.
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    devices = {}
-    version_re = re.compile('-- iOS (?P&lt;version&gt;[0-9]+\.[0-9]+) --')
-    devices_re = re.compile('\s*(?P&lt;name&gt;[^(]+ )\((?P&lt;udid&gt;[^)]+)\) \((?P&lt;state&gt;[^)]+)\)')
-    stdout = subprocess.check_output(['xcrun', '-sdk', 'iphonesimulator', 'simctl', 'list', 'devices'])
-    lines = iter(stdout.splitlines())
-    header = next(lines)
-    version = None
-    if header != '== Devices ==':
-        return None
</del><span class="cx"> 
</span><del>-    for line in lines:
-        version_match = version_re.match(line)
-        if version_match:
-            version = tuple([int(component) for component in version_match.group('version').split('.')])
-            continue
-        device_match = devices_re.match(line)
-        if not device_match:
-            raise RuntimeError()
-        device = device_match.groupdict()
-        device['name'] = device['name'].rstrip()
</del><ins>+    def __init__(self, version, identifier, available, devices=None):
+        &quot;&quot;&quot;
+        :param version: The iOS SDK version
+        :type version: tuple
+        :param identifier: The CoreSimualtor runtime identifier
+        :type identifier: str
+        :param availability: Whether the runtime is available for use.
+        :type availability: bool
+        :param devices: A list of devices under this runtime
+        :type devices: list or None
+        &quot;&quot;&quot;
+        self.version = version
+        self.identifier = identifier
+        self.available = available
+        self.devices = devices or []
</ins><span class="cx"> 
</span><del>-        devices[version][device['udid']] = device
</del><ins>+    @classmethod
+    def from_identifier(cls, identifier):
+        &quot;&quot;&quot;
+        :param identifier: The identifier for the desired CoreSimulator runtime.
+        :type identifier: str
+        :returns: A `Runtime` object with the specified identifier or throws a TypeError if it doesn't exist.
+        :rtype: Runtime
+        &quot;&quot;&quot;
+        runtime = None
+        for runtime in Simulator().runtimes:
+            if runtime.identifier == identifier:
+                break
+        if runtime is None:
+            raise TypeError('A runtime with identifier &quot;{identifier}&quot; does not exist.'.format(identifier=identifier))
+        return runtime
</ins><span class="cx"> 
</span><del>-    return devices
</del><ins>+    def __eq__(self, other):
+        return (self.version == other.version) and (self.identifier == other.identifier)
</ins><span class="cx"> 
</span><ins>+    def __ne__(self, other):
+        return not self.__eq__(other)
</ins><span class="cx"> 
</span><del>-def get_device_types():
</del><ins>+    def __repr__(self):
+        return '&lt;Runtime {version}: {identifier}. Available: {available}, {num_devices} devices&gt;'.format(
+            version='.'.join(map(str, self.version)),
+            identifier=self.identifier,
+            available=self.available,
+            num_devices=len(self.devices))
+
+
+class Device(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    :return: A dictionary mapping of device name -&gt; identifier
-    :rtype: dict
</del><ins>+    Represents a CoreSimulator device underneath a runtime
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    device_types = {}
</del><ins>+
+    def __init__(self, name, udid, state, available, runtime):
+        &quot;&quot;&quot;
+        :param name: The device name
+        :type name: str
+        :param udid: The device UDID (a UUID string)
+        :type udid: str
+        :param state: The last known device state
+        :type state: str
+        :param available: Whether the device is available for use.
+        :type available: bool
+        :param runtime: The iOS Simulator runtime that hosts this device
+        :type runtime: Runtime
+        &quot;&quot;&quot;
+        self.name = name
+        self.udid = udid
+        self.state = state
+        self.available = available
+        self.runtime = runtime
+
+    @property
+    def path(self):
+        &quot;&quot;&quot;
+        :returns: The filesystem path that contains the simulator device's data.
+        :rtype: str
+        &quot;&quot;&quot;
+        return os.path.realpath(
+            os.path.expanduser(
+                os.path.join('~/Library/Developer/CoreSimulator/Devices', self.udid)))
+
+    @classmethod
+    def create(cls, name, device_type, runtime):
+        &quot;&quot;&quot;
+        Create a new CoreSimulator device.
+        :param name: The name of the device.
+        :type name: str
+        :param device_type: The CoreSimulatort device type.
+        :type device_type: DeviceType
+        :param runtime:  The CoreSimualtor runtime.
+        :type runtime: Runtime
+        :return: The new device or raises a CalledProcessError if ``simctl create`` failed.
+        :rtype: Device
+        &quot;&quot;&quot;
+        sim = Simulator()
+        subprocess.check_call(['xcrun', 'simctl', 'create', name, device_type.identifier, runtime.identifier])
+
+        device = None
+        while device is None:
+            sim.refresh()
+            device = sim.device(name, runtime)
+            if device is None or device.state == 'Creating':
+                time.sleep(2)
+            else:
+                break
+        return device
+
+    def __eq__(self, other):
+        return self.udid == other.udid
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return '&lt;Device &quot;{name}&quot;: {udid}. State: {state}. Runtime: {runtime}, Available: {available}&gt;'.format(
+            name=self.name,
+            udid=self.udid,
+            state=self.state,
+            available=self.available,
+            runtime=self.runtime.identifier)
+
+
+class Simulator(object):
+    &quot;&quot;&quot;
+    Represents the iOS Simulator infrastructure under the currently select Xcode.app bundle.
+    &quot;&quot;&quot;
</ins><span class="cx">     device_type_re = re.compile('(?P&lt;name&gt;[^(]+)\((?P&lt;identifier&gt;[^)]+)\)')
</span><del>-    stdout = subprocess.check_output(['xcrun', '-sdk', 'iphonesimulator', 'simctl', 'list', 'devicetypes'])
-    lines = iter(stdout.splitlines())
-    header = next(lines)
-    if header != '== Device Types ==':
</del><ins>+    runtime_re = re.compile(
+        'iOS (?P&lt;version&gt;[0-9]+\.[0-9]) \([0-9]+\.[0-9]+ - (?P&lt;build_version&gt;[^)]+)\) \((?P&lt;identifier&gt;[^)]+)\)( \((?P&lt;availability&gt;[^)]+)\))?')
+    version_re = re.compile('-- iOS (?P&lt;version&gt;[0-9]+\.[0-9]+) --')
+    devices_re = re.compile(
+        '\s*(?P&lt;name&gt;[^(]+ )\((?P&lt;udid&gt;[^)]+)\) \((?P&lt;state&gt;[^)]+)\)( \((?P&lt;availability&gt;[^)]+)\))?')
+
+    def __init__(self):
+        self.runtimes = []
+        self.device_types = []
+        self.refresh()
+
+    def refresh(self):
+        &quot;&quot;&quot;
+        Refresh runtime and device type information from ``simctl list``.
+        &quot;&quot;&quot;
+        command = ['xcrun', 'simctl', 'list']
+        simctl_p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        stdout, stderr = simctl_p.communicate()
+        if simctl_p.returncode != 0:
+            raise RuntimeError(
+                '{command} failed:\n{stdout}\n{stderr}'.format(command=' '.join(command), stdout=stdout, stderr=stderr))
+
+        lines = (line for line in stdout.splitlines())
+        device_types_header = next(lines)
+        if device_types_header != '== Device Types ==':
+            raise RuntimeError('Expected == Device Types == header but got: &quot;{}&quot;'.format(device_types_header))
+        self._parse_device_types(lines)
+
+    def _parse_device_types(self, lines):
+        &quot;&quot;&quot;
+        Parse device types from ``simctl list``.
+        :param lines: A generator for the output lines from ``simctl list``.
+        :type lines: genexpr
+        :return: None
+        &quot;&quot;&quot;
+        for line in lines:
+            device_type_match = self.device_type_re.match(line)
+            if not device_type_match:
+                if line != '== Runtimes ==':
+                    raise RuntimeError('Expected == Runtimes == header but got: &quot;{}&quot;'.format(line))
+                break
+            device_type = DeviceType(name=device_type_match.group('name').rstrip(),
+                                     identifier=device_type_match.group('identifier'))
+            self.device_types.append(device_type)
+
+        self._parse_runtimes(lines)
+
+    def _parse_runtimes(self, lines):
+        &quot;&quot;&quot;
+        Continue to parse runtimes from ``simctl list``.
+        :param lines: A generator for the output lines from ``simctl list``.
+        :type lines: genexpr
+        :return: None
+        &quot;&quot;&quot;
+        for line in lines:
+            runtime_match = self.runtime_re.match(line)
+            if not runtime_match:
+                if line != '== Devices ==':
+                    raise RuntimeError('Expected == Devices == header but got: &quot;{}&quot;'.format(line))
+                break
+            version = tuple(map(int, runtime_match.group('version').split('.')))
+            runtime = Runtime(version=version,
+                              identifier=runtime_match.group('identifier'),
+                              available=runtime_match.group('availability') is None)
+            self.runtimes.append(runtime)
+        self._parse_devices(lines)
+
+    def _parse_devices(self, lines):
+        &quot;&quot;&quot;
+        Continue to parse devices from ``simctl list``.
+        :param lines: A generator for the output lines from ``simctl list``.
+        :type lines: genexpr
+        :return: None
+        &quot;&quot;&quot;
+        current_runtime = None
+        for line in lines:
+            version_match = self.version_re.match(line)
+            if version_match:
+                version = tuple(map(int, version_match.group('version').split('.')))
+                current_runtime = self.runtime(version=version)
+                assert current_runtime
+                continue
+            device_match = self.devices_re.match(line)
+            if not device_match:
+                raise RuntimeError('Expected an iOS Simulator device line, got &quot;{}&quot;'.format(line))
+            device = Device(name=device_match.group('name').rstrip(),
+                            udid=device_match.group('udid'),
+                            state=device_match.group('state'),
+                            available=device_match.group('availability') is None,
+                            runtime=current_runtime)
+            current_runtime.devices.append(device)
+
+    def device_type(self, name=None, identifier=None):
+        &quot;&quot;&quot;
+        :param name: The short name of the device type.
+        :type name: str
+        :param identifier: The CoreSimulator identifier of the desired device type.
+        :type identifier: str
+        :return: A device type with the specified name and/or identifier, or None if one doesn't exist as such.
+        :rtype: DeviceType
+        &quot;&quot;&quot;
+        for device_type in self.device_types:
+            if name and device_type.name != name:
+                continue
+            if identifier and device_type.identifier != identifier:
+                continue
+            return device_type
</ins><span class="cx">         return None
</span><span class="cx"> 
</span><del>-    for line in lines:
-        device_type_match = device_type_re.match(line)
-        if not device_type_match:
-            continue
-        device_type = device_type_match.groupdict()
-        device_type['name'] = device_type['name'].rstrip()
-        device_types[device_type['name']] = device_type['identifier']
</del><ins>+    def runtime(self, version=None, identifier=None):
+        &quot;&quot;&quot;
+        :param version: The iOS version of the desired runtime.
+        :type version: tuple
+        :param identifier: The CoreSimulator identifier of the desired runtime.
+        :type identifier: str
+        :return: A runtime with the specified version and/or identifier, or None if one doesn't exist as such.
+        :rtype: Runtime or None
+        &quot;&quot;&quot;
+        if version is None and identifier is None:
+            raise TypeError('Must supply version and/or identifier.')
</ins><span class="cx"> 
</span><del>-    return device_types
</del><ins>+        for runtime in self.runtimes:
+            if version and runtime.version != version:
+                continue
+            if identifier and runtime.identifier != identifier:
+                continue
+            return runtime
+        return None
</ins><span class="cx"> 
</span><ins>+    def device(self, name=None, runtime=None):
+        &quot;&quot;&quot;
+        :param name: The name of the desired device.
+        :type name: str
+        :param runtime: The runtime of the desired device.
+        :type runtime: Runtime
+        :return: A device with the specified name and/or runtime, or None if one doesn't exist as such
+        :rtype: Device or None
+        &quot;&quot;&quot;
+        if name is None and runtime is None:
+            raise TypeError('Must supply name and/or runtime.')
</ins><span class="cx"> 
</span><del>-def get_latest_runtime():
-    runtimes = get_runtimes()
-    if not runtimes:
</del><ins>+        for device in self.devices:
+            if name and device.name != name:
+                continue
+            if runtime and device.runtime != runtime:
+                continue
+            return device
</ins><span class="cx">         return None
</span><del>-    latest_version = sorted(runtimes.keys())[0]
-    return runtimes[latest_version]
</del><ins>+
+    @property
+    def devices(self):
+        &quot;&quot;&quot;
+        :return: An iterator of all devices from all runtimes.
+        :rtype: iter
+        &quot;&quot;&quot;
+        return itertools.chain(*[runtime.devices for runtime in self.runtimes])
+
+    @property
+    def latest_runtime(self):
+        &quot;&quot;&quot;
+        :return: Returns a Runtime object with the highest version.
+        :rtype: Runtime or None
+        &quot;&quot;&quot;
+        if not self.runtimes:
+            return None
+        return sorted(self.runtimes, key=lambda runtime: runtime.version)[-1]
+
+    def testing_device(self, device_type, runtime):
+        &quot;&quot;&quot;
+        Get an iOS Simulator device for testing.
+        :param device_type: The CoreSimulator device type.
+        :type device_type: DeviceType
+        :param runtime: The CoreSimulator runtime.
+        :type runtime: Runtime
+        :return: A dictionary describing the device.
+        :rtype: Device
+        &quot;&quot;&quot;
+        # Check to see if the testing device already exists
+        name = device_type.name + ' WebKit Tester'
+        return self.device(name=name, runtime=runtime) or Device.create(name, device_type, runtime)
+
+    def __repr__(self):
+        return '&lt;iOS Simulator: {num_runtimes} runtimes, {num_device_types} device types&gt;'.format(
+            num_runtimes=len(self.runtimes),
+            num_device_types=len(self.device_types))
+
+    def __str__(self):
+        description = ['iOS Simulator:']
+        description += map(str, self.runtimes)
+        description += map(str, self.device_types)
+        description += map(str, self.devices)
+        return '\n'.join(description)
</ins></span></pre>
</div>
</div>

</body>
</html>