webkitpy: Refactor simulator code (Part 1)
authorjbedard@apple.com <jbedard@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 22 Dec 2017 14:43:38 +0000 (14:43 +0000)
committerjbedard@apple.com <jbedard@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 22 Dec 2017 14:43:38 +0000 (14:43 +0000)
https://bugs.webkit.org/show_bug.cgi?id=180555
<rdar://problem/36131381>

Reviewed by Alexey Proskuryakov.

The new SimulatedDeviceManager defined in this patch is designed to
act as a singleton. Consumers may request devices with certain
attributes, the the manager will then return a list of devices fulfilling the
request. The manager will pick between:
        - Existing and booted simulators
        - Existing simulators currently shut down
        - Custom constructed simulators
These simulators will then be globally available.

This patch duplicates code in webkitpy/xcode/simulated_device.py,
webkitpy/xcode/simulator.py and webkitpy/port/ios_simulator.py. This duplicated
code will be removed in parts 2 and 3.

* Scripts/webkitpy/common/version_name_map.py:
(VersionNameMap.__init__): Add tvOS and watchOS.
* Scripts/webkitpy/xcode/device_type.py: Added.
(DeviceType): Holds the software and hardware information for a specific device.
Designed to allow for partial matching.
(DeviceType.from_string): Parses a string (such as iPhone 6s) into a DeviceType
object.
(DeviceType._define_software_variant_from_hardware_family): The software_variant
of a device can usually be implied from its hardware_family.
(DeviceType.check_consistency): Verify that the software_variant matches the
hardware_family and that software_version and hardware_variant have their
respective prerequisites.
(DeviceType.__init__):
(DeviceType.__str__): Converts a DeviceType object into a human readable string.
(DeviceType.__eq__): Compares two DeviceType objects treating None as a wildcard.
(DeviceType.__contains__): Allow partial version mapping.
* Scripts/webkitpy/xcode/device_type_unittest.py: Added.
(DeviceTypeTest):
(DeviceTypeTest.test_iphone_initialization):
(DeviceTypeTest.test_ipad_initialization):
(DeviceTypeTest.test_generic_ios_device):
(DeviceTypeTest.test_watch_initialization):
(DeviceTypeTest.test_tv_initialization):
(DeviceTypeTest.test_from_string):
(DeviceTypeTest.test_comparison):
(DeviceTypeTest.test_contained_in):
* Scripts/webkitpy/xcode/new_simulated_device.py: Added.
(DeviceRequest): Adds additional options to be used when requesting a device.
(DeviceRequest.__init__):
(SimulatedDeviceManager):
(SimulatedDeviceManager.Runtime): Representation of a supported simulated runtime.
Designed as an internal representation, not to be used outside this class.
(SimulatedDeviceManager.Runtime.__init__):
(SimulatedDeviceManager._create_runtimes): Extract a list of available runtimes.
(SimulatedDeviceManager._create_device_with_runtime): Return a new or existing device
with the provided runtime matching the provided dictionary.
(SimulatedDeviceManager.populate_available_devices): Use simctl to update the list of devices
available on this machine.
(SimulatedDeviceManager.available_devices): Populate the list of available device if empty
and return that list.
(SimulatedDeviceManager._find_exisiting_device_for_request): Return an existing device matching
the provided request if one exists.
(SimulatedDeviceManager._find_available_name): Given a name base, return a string which
does not match the name of an initialized device.
(SimulatedDeviceManager. get_runtime_for_device_type): Map device type to runtime object.
(SimulatedDeviceManager._disambiguate_device_type): The user may have requested a DeviceType
with only partial definitions. In this case, use existing runtimes and devices to generate
a DeviceType to build.
(SimulatedDeviceManager._get_device_identifier_for_type): Return a simctl device type string
given a DeviceType object.
(SimulatedDeviceManager._create_or_find_device_for_request): Given a DeviceRequest object,
either find an existing device which matches the request or create one matching
(SimulatedDeviceManager._does_fulfill_request): Given a device and list of requests, return
the request which the provided device matches, if there are any.
(SimulatedDeviceManager._wait_until_device_in_state): Wait until a device enters a specific
state.
(SimulatedDeviceManager.initialize_devices): Given a list of requests, return a list
of devices which fulfill the requests. This function will use both existing devices
and create devices. This function will also start the Simulator.app.
(SimulatedDeviceManager.max_supported_simulators): Uses the number of available cores,
maximum number of processes and the available RAM to calculate the number of
simulated devices this machine can support.
(SimulatorManager.swap): Shuts down the specified device and boots a new one which
matches the specified request.
(SimulatorManager.tear_down): Shut down any simulators managed by this class.
(SimulatedDevice):
(SimulatedDevice.DeviceState): Copied from webkitpy/xcode/simulator.py.
(SimulatedDevice.__init__):
(SimulatedDevice.state): Use the device's plist to determine the current state of
the device. Note that this function will cache the result for a second.
(SimulatedDevice.is_booted_or_booting):
(SimulatedDevice._shut_down): Shut down this device and remove it from the list of
initialized devices.
(SimulatedDevice._delete): Delete this device and remove it from the list of
available devices.
(SimulatedDevice._tear_down): Shut down and delete this device, if it is managed by
the SimulatorManager.
(SimulatedDevice.install_app): Copied from webkitpy/xcode/simulated_device.py.
(SimulatedDevice.launch_app): Ditto.
(SimulatedDevice.__eq__): Ditto.
(SimulatedDevice.__ne__): Ditto.
(SimulatedDevice.__repr__): Map state to string and include device type.
* Scripts/webkitpy/xcode/new_simulated_device_unittest.py: Added.
(SimulatedDeviceTest):
(SimulatedDeviceTest.reset_simulated_device_manager):
(SimulatedDeviceTest.tear_down):
(SimulatedDeviceTest.mock_host_for_simctl):
(device_by_criteria):
(test_available_devices):
(test_existing_simulator):
(change_state_to):
(test_swapping_devices):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@226263 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Tools/ChangeLog
Tools/Scripts/webkitpy/common/version_name_map.py
Tools/Scripts/webkitpy/xcode/device_type.py [new file with mode: 0644]
Tools/Scripts/webkitpy/xcode/device_type_unittest.py [new file with mode: 0644]
Tools/Scripts/webkitpy/xcode/new_simulated_device.py [new file with mode: 0644]
Tools/Scripts/webkitpy/xcode/new_simulated_device_unittest.py [new file with mode: 0644]

index 1453404..4941adc 100644 (file)
@@ -1,3 +1,117 @@
+2017-12-22  Jonathan Bedard  <jbedard@apple.com>
+
+        webkitpy: Refactor simulator code (Part 1)
+        https://bugs.webkit.org/show_bug.cgi?id=180555
+        <rdar://problem/36131381>
+
+        Reviewed by Alexey Proskuryakov.
+
+        The new SimulatedDeviceManager defined in this patch is designed to
+        act as a singleton. Consumers may request devices with certain
+        attributes, the the manager will then return a list of devices fulfilling the
+        request. The manager will pick between:
+                - Existing and booted simulators
+                - Existing simulators currently shut down
+                - Custom constructed simulators
+        These simulators will then be globally available.
+
+        This patch duplicates code in webkitpy/xcode/simulated_device.py,
+        webkitpy/xcode/simulator.py and webkitpy/port/ios_simulator.py. This duplicated
+        code will be removed in parts 2 and 3.
+
+        * Scripts/webkitpy/common/version_name_map.py:
+        (VersionNameMap.__init__): Add tvOS and watchOS.
+        * Scripts/webkitpy/xcode/device_type.py: Added.
+        (DeviceType): Holds the software and hardware information for a specific device.
+        Designed to allow for partial matching.
+        (DeviceType.from_string): Parses a string (such as iPhone 6s) into a DeviceType
+        object.
+        (DeviceType._define_software_variant_from_hardware_family): The software_variant
+        of a device can usually be implied from its hardware_family.
+        (DeviceType.check_consistency): Verify that the software_variant matches the
+        hardware_family and that software_version and hardware_variant have their
+        respective prerequisites.
+        (DeviceType.__init__):
+        (DeviceType.__str__): Converts a DeviceType object into a human readable string.
+        (DeviceType.__eq__): Compares two DeviceType objects treating None as a wildcard.
+        (DeviceType.__contains__): Allow partial version mapping.
+        * Scripts/webkitpy/xcode/device_type_unittest.py: Added.
+        (DeviceTypeTest):
+        (DeviceTypeTest.test_iphone_initialization):
+        (DeviceTypeTest.test_ipad_initialization):
+        (DeviceTypeTest.test_generic_ios_device):
+        (DeviceTypeTest.test_watch_initialization):
+        (DeviceTypeTest.test_tv_initialization):
+        (DeviceTypeTest.test_from_string):
+        (DeviceTypeTest.test_comparison):
+        (DeviceTypeTest.test_contained_in):
+        * Scripts/webkitpy/xcode/new_simulated_device.py: Added.
+        (DeviceRequest): Adds additional options to be used when requesting a device.
+        (DeviceRequest.__init__):
+        (SimulatedDeviceManager):
+        (SimulatedDeviceManager.Runtime): Representation of a supported simulated runtime.
+        Designed as an internal representation, not to be used outside this class.
+        (SimulatedDeviceManager.Runtime.__init__):
+        (SimulatedDeviceManager._create_runtimes): Extract a list of available runtimes.
+        (SimulatedDeviceManager._create_device_with_runtime): Return a new or existing device
+        with the provided runtime matching the provided dictionary.
+        (SimulatedDeviceManager.populate_available_devices): Use simctl to update the list of devices
+        available on this machine.
+        (SimulatedDeviceManager.available_devices): Populate the list of available device if empty
+        and return that list.
+        (SimulatedDeviceManager._find_exisiting_device_for_request): Return an existing device matching
+        the provided request if one exists.
+        (SimulatedDeviceManager._find_available_name): Given a name base, return a string which
+        does not match the name of an initialized device.
+        (SimulatedDeviceManager. get_runtime_for_device_type): Map device type to runtime object.
+        (SimulatedDeviceManager._disambiguate_device_type): The user may have requested a DeviceType
+        with only partial definitions. In this case, use existing runtimes and devices to generate
+        a DeviceType to build.
+        (SimulatedDeviceManager._get_device_identifier_for_type): Return a simctl device type string
+        given a DeviceType object.
+        (SimulatedDeviceManager._create_or_find_device_for_request): Given a DeviceRequest object,
+        either find an existing device which matches the request or create one matching
+        (SimulatedDeviceManager._does_fulfill_request): Given a device and list of requests, return
+        the request which the provided device matches, if there are any.
+        (SimulatedDeviceManager._wait_until_device_in_state): Wait until a device enters a specific
+        state.
+        (SimulatedDeviceManager.initialize_devices): Given a list of requests, return a list
+        of devices which fulfill the requests. This function will use both existing devices
+        and create devices. This function will also start the Simulator.app.
+        (SimulatedDeviceManager.max_supported_simulators): Uses the number of available cores,
+        maximum number of processes and the available RAM to calculate the number of
+        simulated devices this machine can support.
+        (SimulatorManager.swap): Shuts down the specified device and boots a new one which
+        matches the specified request.
+        (SimulatorManager.tear_down): Shut down any simulators managed by this class.
+        (SimulatedDevice):
+        (SimulatedDevice.DeviceState): Copied from webkitpy/xcode/simulator.py.
+        (SimulatedDevice.__init__):
+        (SimulatedDevice.state): Use the device's plist to determine the current state of
+        the device. Note that this function will cache the result for a second.
+        (SimulatedDevice.is_booted_or_booting):
+        (SimulatedDevice._shut_down): Shut down this device and remove it from the list of
+        initialized devices.
+        (SimulatedDevice._delete): Delete this device and remove it from the list of
+        available devices.
+        (SimulatedDevice._tear_down): Shut down and delete this device, if it is managed by
+        the SimulatorManager.
+        (SimulatedDevice.install_app): Copied from webkitpy/xcode/simulated_device.py.
+        (SimulatedDevice.launch_app): Ditto.
+        (SimulatedDevice.__eq__): Ditto.
+        (SimulatedDevice.__ne__): Ditto.
+        (SimulatedDevice.__repr__): Map state to string and include device type.
+        * Scripts/webkitpy/xcode/new_simulated_device_unittest.py: Added.
+        (SimulatedDeviceTest):
+        (SimulatedDeviceTest.reset_simulated_device_manager):
+        (SimulatedDeviceTest.tear_down):
+        (SimulatedDeviceTest.mock_host_for_simctl):
+        (device_by_criteria):
+        (test_available_devices):
+        (test_existing_simulator):
+        (change_state_to):
+        (test_swapping_devices):
+
 2017-12-21  Ling Ho <lingcherd_ho@apple.com>
 
         "Release 32-bit Build EWS" queue should be moved up to macOS High Sierra on Bot Watcher's Dashboard
index 92987b4..b3bdd38 100644 (file)
@@ -61,6 +61,8 @@ class VersionNameMap(object):
                 'Future': Version(10, 14),
             },
             'ios': self._automap_to_major_version('iOS', minimum=Version(10), maximum=Version(12)),
+            'tvos': self._automap_to_major_version('tvOS', minimum=Version(10), maximum=Version(12)),
+            'watchos': self._automap_to_major_version('watchOS', minimum=Version(1), maximum=Version(5)),
             'win': {
                 'Win10': Version(10),
                 '8.1': Version(6, 3),
diff --git a/Tools/Scripts/webkitpy/xcode/device_type.py b/Tools/Scripts/webkitpy/xcode/device_type.py
new file mode 100644 (file)
index 0000000..fb83ee5
--- /dev/null
@@ -0,0 +1,140 @@
+# Copyright (C) 2017 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.common.version_name_map import VersionNameMap
+
+
+# This class is designed to match device types. Because it is used for matching, 'None' is treated as a wild-card.
+class DeviceType(object):
+
+    @classmethod
+    def from_string(cls, device_string, version=None):
+        """
+        Converts a string into a DeviceType object. These strings should be of the form
+        '<hardware_family> <hardware_type>', where <hardware_family> is mandatory and
+        <hardware_family> is optional.
+
+        Example input + output:
+        ('iPhone 6 Plus') -> DeviceType(hardware_family='iPhone', hardware_type='6 Plus', software_version=None)
+        ('iPhone', Version(11)) -> DeviceType(hardware_family='iPhone', hardware_type=None, software_version=Version(11))
+        ('Apple TV 4K') -> DeviceType(hardware_family='TV', hardware_type='4K', software_version=None)
+
+        :param device_string: String representing a device.
+        :type device_string: str
+        :param version: Version object of software run on the device.
+        :type version: Version
+        :returns: DeviceType object
+        :rtype: DeviceType
+        """
+        split_str = device_string.split(' ')
+        if len(split_str) == 1:
+            return cls(hardware_family=device_string, software_version=version)
+        family_index = 0
+        if split_str[family_index].lower() == 'apple':
+            family_index = 1
+        return cls(
+            hardware_family=split_str[family_index],
+            hardware_type=' '.join(split_str[family_index + 1:]) if len(split_str) > family_index + 1 else None,
+            software_version=version)
+
+    def _define_software_variant_from_hardware_family(self):
+        if self.hardware_family is None:
+            return
+        if self.software_variant:
+            return
+
+        self.software_variant = 'iOS'
+        if self.hardware_family.lower().split(' ')[-1].startswith('watch'):
+            self.hardware_family = 'Apple Watch'
+            self.software_variant = 'watchOS'
+        elif self.hardware_family.lower().split(' ')[-1].startswith('tv'):
+            self.hardware_family = 'Apple TV'
+            self.software_variant = 'tvOS'
+
+    def check_consistency(self):
+        if self.hardware_family is not None:
+            assert self.software_variant is not None
+            if self.hardware_family == 'Apple Watch':
+                assert self.software_variant is 'watchOS'
+            elif self.hardware_family == 'Apple TV':
+                assert self.software_variant == 'tvOS'
+            else:
+                assert self.software_variant == 'iOS'
+
+        if self.hardware_type is not None:
+            assert self.hardware_family is not None
+        if self.software_version:
+            assert self.software_variant is not None
+
+    def __init__(self, hardware_family=None, hardware_type=None, software_version=None, software_variant=None):
+        """
+        :param hardware_family: iPhone, iPad, Apple Watch and Apple TV are all examples.
+        :type hardware_family: str
+        :param hardware_type: 6s, Series 2 - 42mm, 4k are all examples
+        :type hardware_type: str
+        :param software_version: Version object representing software the device is running.
+        :type software_version: Version
+        :param software_variant: Groups together hardware families which share an OS, like iPad and iPhone. iOS, tvOS and watchOS are examples.
+        :type software_variant: str
+        """
+        if hardware_family is None and hardware_type is None and software_version is None and software_variant is None:
+            raise ValueError('Cannot instantiate DeviceType with no arguments')
+
+        self.hardware_family = hardware_family
+        self.hardware_type = hardware_type
+        self.software_version = software_version
+        self.software_variant = software_variant
+
+        self._define_software_variant_from_hardware_family()
+        self.check_consistency()
+
+    def __str__(self):
+        return '{hardware_family}{hardware_type} running {version}'.format(
+            hardware_family=self.hardware_family if self.hardware_family else 'Device',
+            hardware_type=' {}'.format(self.hardware_type) if self.hardware_type else '',
+            version=VersionNameMap.map().to_name(self.software_version, platform=self.software_variant.lower()) if self.software_version else self.software_variant,
+        )
+
+    # This technique of matching treats 'None' a wild-card.
+    def __eq__(self, other):
+        assert isinstance(other, DeviceType)
+        if self.hardware_family is not None and other.hardware_family is not None and self.hardware_family != other.hardware_family:
+            return False
+        if self.hardware_type is not None and other.hardware_type is not None and self.hardware_type != other.hardware_type:
+            return False
+        if self.software_variant is not None and other.software_variant is not None and self.software_variant != other.software_variant:
+            return False
+        if self.software_version is not None and other.software_version is not None and self.software_version != other.software_version:
+            return False
+        return True
+
+    def __contains__(self, other):
+        assert isinstance(other, DeviceType)
+        if self.hardware_family is not None and self.hardware_family != other.hardware_family:
+            return False
+        if self.hardware_type is not None and self.hardware_type != other.hardware_type:
+            return False
+        if self.software_variant is not None and self.software_variant != other.software_variant:
+            return False
+        if self.software_version is not None and other.software_version is not None and not other.software_version in self.software_version:
+            return False
+        return True
diff --git a/Tools/Scripts/webkitpy/xcode/device_type_unittest.py b/Tools/Scripts/webkitpy/xcode/device_type_unittest.py
new file mode 100644 (file)
index 0000000..5a4520b
--- /dev/null
@@ -0,0 +1,143 @@
+# Copyright (C) 2017 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.version import Version
+from webkitpy.xcode.device_type import DeviceType
+
+
+class DeviceTypeTest(unittest.TestCase):
+
+    def test_iphone_initialization(self):
+        type = DeviceType(hardware_family='iPhone')
+        self.assertEquals('iPhone', type.hardware_family)
+        self.assertEquals(None, type.hardware_type)
+        self.assertEquals('iOS', type.software_variant)
+        self.assertEquals(None, type.software_version)
+        self.assertEqual('iPhone running iOS', str(type))
+
+        type = DeviceType('iPhone', '6s', Version(11))
+        self.assertEquals('iPhone', type.hardware_family)
+        self.assertEquals('6s', type.hardware_type)
+        self.assertEquals('iOS', type.software_variant)
+        self.assertEquals(Version(11), type.software_version)
+        self.assertEqual('iPhone 6s running iOS 11', str(type))
+
+    def test_ipad_initialization(self):
+        type = DeviceType(hardware_family='iPad')
+        self.assertEquals('iPad', type.hardware_family)
+        self.assertEquals(None, type.hardware_type)
+        self.assertEquals('iOS', type.software_variant)
+        self.assertEquals(None, type.software_version)
+        self.assertEqual('iPad running iOS', str(type))
+
+        type = DeviceType('iPad', 'Air 2', Version(11))
+        self.assertEquals('iPad', type.hardware_family)
+        self.assertEquals('Air 2', type.hardware_type)
+        self.assertEquals('iOS', type.software_variant)
+        self.assertEquals(Version(11), type.software_version)
+        self.assertEqual('iPad Air 2 running iOS 11', str(type))
+
+    def test_generic_ios_device(self):
+        type = DeviceType(software_variant='iOS')
+        self.assertEquals(None, type.hardware_family)
+        self.assertEquals(None, type.hardware_type)
+        self.assertEquals('iOS', type.software_variant)
+        self.assertEquals(None, type.software_version)
+        self.assertEqual('Device running iOS', str(type))
+
+    def test_watch_initialization(self):
+        type = DeviceType(hardware_family='Watch')
+        self.assertEquals('Apple Watch', type.hardware_family)
+        self.assertEquals(None, type.hardware_type)
+        self.assertEquals('watchOS', type.software_variant)
+        self.assertEquals(None, type.software_version)
+        self.assertEqual('Apple Watch running watchOS', str(type))
+
+        type = DeviceType('Apple Watch', 'Series 2 - 42mm', Version(4))
+        self.assertEquals('Apple Watch', type.hardware_family)
+        self.assertEquals('Series 2 - 42mm', type.hardware_type)
+        self.assertEquals('watchOS', type.software_variant)
+        self.assertEquals(Version(4), type.software_version)
+        self.assertEqual('Apple Watch Series 2 - 42mm running watchOS 4', str(type))
+
+    def test_tv_initialization(self):
+        type = DeviceType(hardware_family='TV')
+        self.assertEquals('Apple TV', type.hardware_family)
+        self.assertEquals(None, type.hardware_type)
+        self.assertEquals('tvOS', type.software_variant)
+        self.assertEquals(None, type.software_version)
+        self.assertEqual('Apple TV running tvOS', str(type))
+
+        type = DeviceType('Apple TV', '4K', Version(11))
+        self.assertEquals('Apple TV', type.hardware_family)
+        self.assertEquals('4K', type.hardware_type)
+        self.assertEquals('tvOS', type.software_variant)
+        self.assertEquals(Version(11), type.software_version)
+        self.assertEqual('Apple TV 4K running tvOS 11', str(type))
+
+    def test_from_string(self):
+        self.assertEqual('iPhone 6s running iOS', str(DeviceType.from_string('iPhone 6s')))
+        self.assertEqual('iPhone 6 Plus running iOS', str(DeviceType.from_string('iPhone 6 Plus')))
+        self.assertEqual('iPhone running iOS 11', str(DeviceType.from_string('iPhone', Version(11))))
+        self.assertEqual('iPad Air 2 running iOS', str(DeviceType.from_string('iPad Air 2')))
+        self.assertEqual('Apple Watch Series 2 - 42mm running watchOS', str(DeviceType.from_string('Apple Watch Series 2 - 42mm')))
+        self.assertEqual('Apple TV 4K running tvOS', str(DeviceType.from_string('Apple TV 4K')))
+
+    def test_comparison(self):
+        # iPhone comparisons
+        self.assertEqual(DeviceType.from_string('iPhone 6s'), DeviceType.from_string('iPhone'))
+        self.assertEqual(DeviceType.from_string('iPhone X'), DeviceType.from_string('iPhone'))
+        self.assertNotEqual(DeviceType.from_string('iPhone 6s'), DeviceType.from_string('iPhone X'))
+
+        # iPad comparisons
+        self.assertEqual(DeviceType.from_string('iPad Air 2'), DeviceType.from_string('iPad'))
+        self.assertEqual(DeviceType.from_string('iPad Pro (12.9-inch)'), DeviceType.from_string('iPad'))
+        self.assertNotEqual(DeviceType.from_string('iPad Air 2'), DeviceType.from_string('iPad Pro (12.9-inch)'))
+
+        # Apple Watch comparisons
+        self.assertEqual(DeviceType.from_string('Apple Watch Series 2 - 38mm'), DeviceType.from_string('Apple Watch'))
+        self.assertEqual(DeviceType.from_string('Apple Watch Series 2 - 42mm'), DeviceType.from_string('Apple Watch'))
+        self.assertNotEqual(DeviceType.from_string('Apple Watch Series 2 - 38mm'), DeviceType.from_string('Apple Watch Series 2 - 42mm'))
+
+        # Apple TV comparisons
+        self.assertEqual(DeviceType.from_string('Apple TV 4K'), DeviceType.from_string('Apple TV'))
+        self.assertEqual(DeviceType.from_string('Apple TV 4K (at 1080p)'), DeviceType.from_string('Apple TV'))
+        self.assertNotEqual(DeviceType.from_string('Apple TV 4K'), DeviceType.from_string('Apple TV 4K (at 1080p)'))
+
+        # Match by software_variant
+        self.assertEqual(DeviceType.from_string('iPhone 6s'), DeviceType(software_variant='iOS'))
+        self.assertEqual(DeviceType.from_string('iPad Air 2'), DeviceType(software_variant='iOS'))
+        self.assertNotEqual(DeviceType.from_string('Apple Watch Series 2 - 42mm'), DeviceType(software_variant='iOS'))
+        self.assertNotEqual(DeviceType.from_string('Apple TV 4K'), DeviceType(software_variant='iOS'))
+
+        # Cross-device comparisons
+        self.assertNotEqual(DeviceType.from_string('iPad'), DeviceType.from_string('iPhone'))
+        self.assertNotEqual(DeviceType.from_string('Apple Watch'), DeviceType.from_string('iPhone'))
+        self.assertNotEqual(DeviceType.from_string('Apple Watch'), DeviceType.from_string('Apple TV'))
+
+    def test_contained_in(self):
+        self.assertTrue(DeviceType.from_string('iPhone 6s') in DeviceType.from_string('iPhone'))
+        self.assertFalse(DeviceType.from_string('iPhone') in DeviceType.from_string('iPhone 6s'))
+        self.assertTrue(DeviceType.from_string('iPhone', Version(11, 1)) in DeviceType.from_string('iPhone', Version(11)))
+        self.assertFalse(DeviceType.from_string('iPhone', Version(11)) in DeviceType.from_string('iPhone', Version(11, 1)))
diff --git a/Tools/Scripts/webkitpy/xcode/new_simulated_device.py b/Tools/Scripts/webkitpy/xcode/new_simulated_device.py
new file mode 100644 (file)
index 0000000..71bc86e
--- /dev/null
@@ -0,0 +1,568 @@
+# Copyright (C) 2017 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import atexit
+import json
+import logging
+import plistlib
+import re
+import time
+
+from webkitpy.common.memoized import memoized
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.systemhost import SystemHost
+from webkitpy.common.timeout_context import Timeout
+from webkitpy.common.version import Version
+from webkitpy.port.device import Device
+from webkitpy.xcode.device_type import DeviceType
+
+_log = logging.getLogger(__name__)
+
+
+class DeviceRequest(object):
+
+    def __init__(self, device_type, use_booted_simulator=True, use_existing_simulator=True, allow_incomplete_match=False, merge_requests=False):
+        self.device_type = device_type
+        self.use_booted_simulator = use_booted_simulator
+        self.use_existing_simulator = use_existing_simulator
+        self.allow_incomplete_match = allow_incomplete_match  # When matching booted simulators, only force the software_variant to match.
+        self.merge_requests = merge_requests  # Allow a single booted simulator to fullfil multiple requests.
+
+
+class SimulatedDeviceManager(object):
+    class Runtime(object):
+        def __init__(self, runtime_dict):
+            self.build_version = runtime_dict['buildversion']
+            self.os_variant = runtime_dict['name'].split(' ')[0]
+            self.version = Version.from_string(runtime_dict['version'])
+            self.identifier = runtime_dict['identifier']
+            self.name = runtime_dict['name']
+
+    AVAILABLE_RUNTIMES = []
+    AVAILABLE_DEVICES = []
+    INITIALIZED_DEVICES = None
+
+    MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE = 2 * (1024 ** 3)  # 2GB a simulator.
+    PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 125
+
+    xcrun = '/usr/bin/xcrun'
+    simulator_device_path = '~/Library/Developer/CoreSimulator/Devices'
+    simulator_bundle_id = 'com.apple.iphonesimulator'
+    _device_identifier_to_name = {}
+    _managing_simulator_app = False
+
+    @staticmethod
+    def _create_runtimes(runtimes):
+        result = []
+        for runtime in runtimes:
+            if runtime['availability'] != '(available)':
+                continue
+            try:
+                result.append(SimulatedDeviceManager.Runtime(runtime))
+            except (ValueError, AssertionError):
+                continue
+        return result
+
+    @staticmethod
+    def _create_device_with_runtime(host, runtime, device_info):
+        if device_info['availability'] != '(available)':
+            return None
+
+        # Check existing devices.
+        for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
+            if device.udid == device_info['udid']:
+                return device
+
+        # Check that the device.plist exists
+        device_plist = host.filesystem.expanduser(host.filesystem.join(SimulatedDeviceManager.simulator_device_path, device_info['udid'], 'device.plist'))
+        if not host.filesystem.isfile(device_plist):
+            return None
+
+        # Find device type. If we can't parse the device type, ignore this device.
+        try:
+            device_type_string = SimulatedDeviceManager._device_identifier_to_name[plistlib.readPlist(host.filesystem.open_binary_file_for_reading(device_plist))['deviceType']]
+            device_type = DeviceType.from_string(device_type_string, runtime.version)
+            assert device_type.software_variant == runtime.os_variant
+        except (ValueError, AssertionError):
+            return None
+
+        result = Device(SimulatedDevice(
+            name=device_info['name'],
+            udid=device_info['udid'],
+            host=host,
+            device_type=device_type,
+        ))
+        SimulatedDeviceManager.AVAILABLE_DEVICES.append(result)
+        return result
+
+    @staticmethod
+    def populate_available_devices(host=SystemHost()):
+        if not host.platform.is_mac():
+            return
+
+        try:
+            simctl_json = json.loads(host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'list', '--json']))
+        except (ValueError, ScriptError):
+            return
+
+        SimulatedDeviceManager._device_identifier_to_name = {device['identifier']: device['name'] for device in simctl_json['devicetypes']}
+        SimulatedDeviceManager.AVAILABLE_RUNTIMES = SimulatedDeviceManager._create_runtimes(simctl_json['runtimes'])
+
+        for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
+            for device_json in simctl_json['devices'][runtime.name]:
+                device = SimulatedDeviceManager._create_device_with_runtime(host, runtime, device_json)
+                if not device:
+                    continue
+
+                # Update device state from simctl output.
+                device.platform_device._state = SimulatedDevice.NAME_FOR_STATE.index(device_json['state'].upper())
+                device.platform_device._last_updated_state = time.time()
+        return
+
+    @staticmethod
+    def available_devices(host=SystemHost()):
+        if SimulatedDeviceManager.AVAILABLE_DEVICES == []:
+            SimulatedDeviceManager.populate_available_devices(host)
+        return SimulatedDeviceManager.AVAILABLE_DEVICES
+
+    @staticmethod
+    def device_by_filter(filter, host=SystemHost()):
+        result = []
+        for device in SimulatedDeviceManager.available_devices(host):
+            if filter(device):
+                result.append(device)
+        return result
+
+    @staticmethod
+    def _find_exisiting_device_for_request(request):
+        if not request.use_existing_simulator:
+            return None
+        for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
+            # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
+            for initialized_device in SimulatedDeviceManager.INITIALIZED_DEVICES:
+                if isinstance(initialized_device, Device) and device == initialized_device:
+                    device = None
+                    break
+            if device and request.device_type == device.platform_device.device_type:
+                return device
+        return None
+
+    @staticmethod
+    def _find_available_name(name_base):
+        created_index = 0
+        while True:
+            name = '{} {}'.format(name_base, created_index)
+            created_index += 1
+            for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
+                if device is None:
+                    continue
+                if device.platform_device.name == name:
+                    break
+            else:
+                return name
+
+    @staticmethod
+    def get_runtime_for_device_type(device_type):
+        for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
+            if runtime.os_variant == device_type.software_variant and (device_type.software_version is None or device_type.software_version == runtime.version):
+                return runtime
+
+        # Allow for a partial version match.
+        for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
+            if runtime.os_variant == device_type.software_variant and runtime.version in device_type.software_version:
+                return runtime
+        return None
+
+    @staticmethod
+    def _disambiguate_device_type(device_type):
+        full_type = DeviceType(
+            hardware_family=device_type.hardware_family,
+            hardware_type=device_type.hardware_type,
+            software_version=device_type.software_version,
+            software_variant=device_type.software_variant)
+
+        runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
+        assert runtime is not None
+        full_type.software_version = runtime.version
+
+        if device_type.hardware_family is None:
+            # We use the existing devices to determine a legal family if no family is specified
+            for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
+                if device.platform_device.device_type == device_type:
+                    full_type.hardware_family = device.platform_device.device_type.hardware_family
+                    break
+
+        if device_type.hardware_type is None:
+            # Again, we use the existing devices to determine a legal hardware type
+            for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
+                if device.platform_device.device_type == device_type:
+                    full_type.hardware_type = device.platform_device.device_type.hardware_type
+                    break
+
+        full_type.check_consistency()
+        return full_type
+
+    @staticmethod
+    def _get_device_identifier_for_type(device_type):
+        for type_id, type_name in SimulatedDeviceManager._device_identifier_to_name.iteritems():
+            if type_name == '{} {}'.format(device_type.hardware_family, device_type.hardware_type):
+                return type_id
+        return None
+
+    @staticmethod
+    def _create_or_find_device_for_request(request, host=SystemHost(), name_base='Managed'):
+        assert isinstance(request, DeviceRequest)
+
+        device = SimulatedDeviceManager._find_exisiting_device_for_request(request)
+        if device:
+            return device
+
+        name = SimulatedDeviceManager._find_available_name(name_base)
+        device_type = SimulatedDeviceManager._disambiguate_device_type(request.device_type)
+        runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
+        device_identifier = SimulatedDeviceManager._get_device_identifier_for_type(device_type)
+
+        assert runtime is not None
+        assert device_identifier is not None
+
+        for device in SimulatedDeviceManager.available_devices(host):
+            if device.platform_device.name == name:
+                device.platform_device.delete()
+                break
+
+        _log.debug("Creating device '{}', of type {}".format(name, device_type))
+        host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'create', name, device_identifier, runtime.identifier])
+
+        # We just added a device, so our list of _available_devices needs to be re-synced.
+        SimulatedDeviceManager.populate_available_devices(host)
+        for device in SimulatedDeviceManager.available_devices(host):
+            if device.platform_device.name == name:
+                device.platform_device.managed_by_script = True
+                return device
+        return None
+
+    @staticmethod
+    def _does_fulfill_request(device, requests):
+        if not device.platform_device.is_booted_or_booting():
+            return None
+
+        # Exact match.
+        for request in requests:
+            if not request.use_booted_simulator:
+                continue
+            if request.device_type == device.platform_device.device_type:
+                _log.debug("The request for '{}' matched {} exactly".format(request.device_type, device))
+                return request
+
+        # Contained-in match.
+        for request in requests:
+            if not request.use_booted_simulator:
+                continue
+            if device.platform_device.device_type in request.device_type:
+                _log.debug("The request for '{}' fuzzy-matched {}".format(request.device_type, device))
+                return request
+
+        # DeviceRequests are compared by reference
+        requests_copy = [request for request in requests]
+
+        # Check for an incomplete match
+        # This is usually used when we don't want to take the time to start a simulator and would
+        # rather use the one the user has already started, even if it isn't quite what we're looking for.
+        for request in requests_copy:
+            if not request.use_booted_simulator or not request.allow_incomplete_match:
+                continue
+            if request.device_type.software_variant == device.platform_device.device_type.software_variant:
+                _log.warn("The request for '{}' incomplete-matched {}".format(request.device_type, device))
+                _log.warn("This may cause unexpected behavior in code that expected the device type {}".format(request))
+                return request
+        return None
+
+    @staticmethod
+    def _wait_until_device_in_state(device, state, deadline):
+        while device.platform_device.state(force_update=True) != state:
+            _log.debug('Waiting on {} to enter state {}...'.format(device, SimulatedDevice.NAME_FOR_STATE[state]))
+            time.sleep(1)
+            if time.time() > deadline:
+                raise RuntimeError('Timed out while waiting for all devices to boot')
+
+    @staticmethod
+    def _boot_device(device, host=SystemHost()):
+        _log.debug("Booting device '{}'".format(device.udid))
+        device.platform_device.booted_by_script = True
+        host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
+        SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
+
+    @staticmethod
+    def initialize_devices(requests, host=SystemHost(), name_base='Managed', simulator_ui=True, timeout=60, **kwargs):
+        if SimulatedDeviceManager.INITIALIZED_DEVICES is not None:
+            return SimulatedDeviceManager.INITIALIZED_DEVICES
+
+        if not host.platform.is_mac():
+            return None
+
+        SimulatedDeviceManager.INITIALIZED_DEVICES = []
+        atexit.register(SimulatedDeviceManager.tear_down)
+
+        # Convert to iterable type
+        if not hasattr(requests, '__iter__'):
+            requests = [requests]
+
+        # Check running sims
+        for device in SimulatedDeviceManager.available_devices(host):
+            matched_request = SimulatedDeviceManager._does_fulfill_request(device, requests)
+            if matched_request is None:
+                continue
+            requests.remove(matched_request)
+            _log.debug('Attached to running simulator {}'.format(device))
+            SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
+
+            # DeviceRequests are compared by reference
+            requests_copy = [request for request in requests]
+
+            # Merging requests means that if 4 devices are requested, but only one is running, these
+            # 4 requests will be fulfilled by the 1 running device.
+            for request in requests_copy:
+                if not request.merge_requests:
+                    continue
+                if not request.use_booted_simulator:
+                    continue
+                if request.device_type != device.platform_device.device_type and not request.allow_incomplete_match:
+                    continue
+                if request.device_type.software_variant != device.platform_device.device_type.software_variant:
+                    continue
+                requests.remove(request)
+
+        for request in requests:
+            device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
+            assert device is not None
+
+            SimulatedDeviceManager._boot_device(device, host)
+
+        if simulator_ui and host.executive.run_command(['killall', '-0', 'Simulator'], return_exit_code=True) != 0:
+            SimulatedDeviceManager._managing_simulator_app = not host.executive.run_command(['open', '-g', '-b', SimulatedDeviceManager.simulator_bundle_id], return_exit_code=True)
+
+        deadline = time.time() + timeout
+        for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
+            SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
+
+        return SimulatedDeviceManager.INITIALIZED_DEVICES
+
+    @staticmethod
+    @memoized
+    def max_supported_simulators(host=SystemHost()):
+        if not host.platform.is_mac():
+            return 0
+
+        try:
+            system_process_count_limit = int(host.executive.run_command(['/usr/bin/ulimit', '-u']).strip())
+            current_process_count = len(host.executive.run_command(['/bin/ps', 'aux']).strip().split('\n'))
+            _log.debug('Process limit: {}, current #processes: {}'.format(system_process_count_limit, current_process_count))
+        except (ValueError, ScriptError):
+            return 0
+
+        max_supported_simulators_for_hardware = min(host.executive.cpu_count() / 2, host.platform.total_bytes_memory() // SimulatedDeviceManager.MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE)
+        max_supported_simulators_locally = (system_process_count_limit - current_process_count) // SimulatedDeviceManager.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
+
+        if (max_supported_simulators_locally < max_supported_simulators_for_hardware):
+            _log.warn('This machine could support {} simulators, but is only configured for {}.'.format(max_supported_simulators_for_hardware, max_supported_simulators_locally))
+            _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
+
+        if max_supported_simulators_locally == 0:
+            max_supported_simulators_locally = 1
+
+        return min(max_supported_simulators_locally, max_supported_simulators_for_hardware)
+
+    @staticmethod
+    def swap(device, request, host=SystemHost(), name_base='Managed', timeout=60):
+        if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
+            raise RuntimeError('Cannot swap when there are no initialized devices')
+        if device not in SimulatedDeviceManager.INITIALIZED_DEVICES:
+            raise RuntimeError('{} is not initialized, cannot swap it'.format(device))
+
+        index = SimulatedDeviceManager.INITIALIZED_DEVICES.index(device)
+        SimulatedDeviceManager.INITIALIZED_DEVICES[index] = None
+        device.platform_device._tear_down()
+
+        device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
+        assert device
+
+        if not device.platform_device.is_booted_or_booting(force_update=True):
+            device.platform_device.booted_by_script = True
+            _log.debug("Booting device '{}'".format(device.udid))
+            host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
+        SimulatedDeviceManager.INITIALIZED_DEVICES[index] = device
+
+        deadline = time.time() + timeout
+        SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
+
+    @staticmethod
+    def tear_down(host=SystemHost(), timeout=60):
+        if SimulatedDeviceManager._managing_simulator_app:
+            host.executive.run_command(['killall', '-9', 'Simulator'], return_exit_code=True)
+            SimulatedDeviceManager._managing_simulator_app = False
+
+        if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
+            return
+
+        deadline = time.time() + timeout
+        while SimulatedDeviceManager.INITIALIZED_DEVICES:
+            device = SimulatedDeviceManager.INITIALIZED_DEVICES[0]
+            if device is None:
+                SimulatedDeviceManager.INITIALIZED_DEVICES.remove(None)
+                continue
+            device.platform_device._tear_down(deadline - time.time())
+
+        SimulatedDeviceManager.INITIALIZED_DEVICES = None
+
+
+class SimulatedDevice(object):
+    class DeviceState:
+        CREATING = 0
+        SHUT_DOWN = 1
+        BOOTING = 2
+        BOOTED = 3
+        SHUTTING_DOWN = 4
+
+    NAME_FOR_STATE = [
+        'CREATING',
+        'SHUTDOWN',
+        'BOOTING',
+        'BOOTED',
+        'SHUTTING DOWN',
+    ]
+
+    def __init__(self, name, udid, host, device_type):
+        assert device_type.software_version
+
+        self.name = name
+        self.udid = udid
+        self.device_type = device_type
+        self._state = SimulatedDevice.DeviceState.SHUTTING_DOWN
+        self._last_updated_state = time.time()
+
+        self.executive = host.executive
+        self.filesystem = host.filesystem
+        self.platform = host.platform
+
+        # Determine tear down behavior
+        self.booted_by_script = False
+        self.managed_by_script = False
+
+    def state(self, force_update=False):
+        # Don't allow state to get stale
+        if not force_update and time.time() < self._last_updated_state + 1:
+            return self._state
+
+        device_plist = self.filesystem.expanduser(self.filesystem.join(SimulatedDeviceManager.simulator_device_path, self.udid, 'device.plist'))
+        self._state = int(plistlib.readPlist(self.filesystem.open_binary_file_for_reading(device_plist))['state'])
+        self._last_updated_state = time.time()
+        return self._state
+
+    def is_booted_or_booting(self, force_update=False):
+        if self.state(force_update=force_update) == SimulatedDevice.DeviceState.BOOTING or self.state() == SimulatedDevice.DeviceState.BOOTED:
+            return True
+        return False
+
+    def _shut_down(self, timeout=10.0):
+        deadline = time.time() + timeout
+
+        if self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN and self.state != SimulatedDevice.DeviceState.SHUTTING_DOWN:
+            self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'shutdown', self.udid])
+
+        while self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN:
+            time.sleep(.5)
+            if time.time() > deadline:
+                raise RuntimeError('Timed out while waiting for {} to shut down'.format(self.udid))
+
+    def _delete(self, timeout=10.0):
+        deadline = time.time() + timeout
+        self._shut_down(deadline - time.time())
+        _log.debug("Removing device '{}'".format(self.name))
+        self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'delete', self.udid])
+
+        # This will (by design) fail if run more than once on the same SimulatedDevice
+        SimulatedDeviceManager.AVAILABLE_DEVICES.remove(self)
+
+    def _tear_down(self, timeout=10.0):
+        deadline = time.time() + timeout
+        if self.booted_by_script:
+            self._shut_down(deadline - time.time())
+        if self.managed_by_script:
+            self._delete(deadline - time.time())
+
+        # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
+        for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
+            if isinstance(device, Device) and device.platform_device == self:
+                SimulatedDeviceManager.INITIALIZED_DEVICES.remove(device)
+
+    def install_app(self, app_path, env=None):
+        return not self.executive.run_command(['xcrun', 'simctl', 'install', self.udid, app_path], return_exit_code=True)
+
+    # FIXME: Increase timeout for <rdar://problem/31331576>
+    def launch_app(self, bundle_id, args, env=None, timeout=300):
+        environment_to_use = {}
+        SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'
+        for value in (env or {}):
+            if not value.startswith(SIMCTL_ENV_PREFIX):
+                environment_to_use[SIMCTL_ENV_PREFIX + value] = env[value]
+            else:
+                environment_to_use[value] = env[value]
+
+        # FIXME: This is a workaround for <rdar://problem/30172453>.
+        def _log_debug_error(error):
+            _log.debug(error.message_with_output())
+
+        output = None
+
+        with Timeout(timeout, RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))):
+            while True:
+                output = self.executive.run_command(
+                    ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
+                    env=environment_to_use,
+                    error_handler=_log_debug_error,
+                )
+                match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
+                # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
+                if match and self.executive.check_running_pid(int(match.group('pid'))):
+                    break
+                if match:
+                    _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
+                else:
+                    _log.debug('simctl launch did not report a pid')
+
+        if match.group('bundle') != bundle_id:
+            raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
+        _log.debug('Returning pid {} of launched process'.format(match.group('pid')))
+        return int(match.group('pid'))
+
+    def __eq__(self, other):
+        return self.udid == other.udid
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return '<Device "{name}": {udid}. State: {state}. Type: {type}>'.format(
+            name=self.name,
+            udid=self.udid,
+            state=SimulatedDevice.NAME_FOR_STATE[self.state()],
+            type=self.device_type)
diff --git a/Tools/Scripts/webkitpy/xcode/new_simulated_device_unittest.py b/Tools/Scripts/webkitpy/xcode/new_simulated_device_unittest.py
new file mode 100644 (file)
index 0000000..deb3e7a
--- /dev/null
@@ -0,0 +1,621 @@
+# Copyright (C) 2017 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+simctl_json_output = """{
+ "devicetypes" : [
+   {
+     "name" : "iPhone 4s",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-4s"
+   },
+   {
+     "name" : "iPhone 5",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-5"
+   },
+   {
+     "name" : "iPhone 5s",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-5s"
+   },
+   {
+     "name" : "iPhone 6",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6"
+   },
+   {
+     "name" : "iPhone 6 Plus",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus"
+   },
+   {
+     "name" : "iPhone 6s",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s"
+   },
+   {
+     "name" : "iPhone 6s Plus",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus"
+   },
+   {
+     "name" : "iPhone 7",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-7"
+   },
+   {
+     "name" : "iPhone 7 Plus",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-7-Plus"
+   },
+   {
+     "name" : "iPhone 8",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8"
+   },
+   {
+     "name" : "iPhone 8 Plus",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus"
+   },
+   {
+     "name" : "iPhone SE",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-SE"
+   },
+   {
+     "name" : "iPhone X",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-X"
+   },
+   {
+     "name" : "iPad 2",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-2"
+   },
+   {
+     "name" : "iPad Retina",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Retina"
+   },
+   {
+     "name" : "iPad Air",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Air"
+   },
+   {
+     "name" : "iPad Air 2",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Air-2"
+   },
+   {
+     "name" : "iPad (5th generation)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad--5th-generation-"
+   },
+   {
+     "name" : "iPad Pro (9.7-inch)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--9-7-inch-"
+   },
+   {
+     "name" : "iPad Pro (12.9-inch)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro"
+   },
+   {
+     "name" : "iPad Pro (12.9-inch) (2nd generation)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--12-9-inch---2nd-generation-"
+   },
+   {
+     "name" : "iPad Pro (10.5-inch)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch-"
+   },
+   {
+     "name" : "Apple TV",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-1080p"
+   },
+   {
+     "name" : "Apple TV 4K",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-4K"
+   },
+   {
+     "name" : "Apple TV 4K (at 1080p)",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-1080p"
+   },
+   {
+     "name" : "Apple Watch - 38mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm"
+   },
+   {
+     "name" : "Apple Watch - 42mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-42mm"
+   },
+   {
+     "name" : "Apple Watch Series 2 - 38mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-2-38mm"
+   },
+   {
+     "name" : "Apple Watch Series 2 - 42mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-2-42mm"
+   },
+   {
+     "name" : "Watch2017 - 38mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-3-38mm"
+   },
+   {
+     "name" : "Watch2017 - 42mm",
+     "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-3-42mm"
+   }
+ ],
+ "runtimes" : [
+   {
+     "buildversion" : "13E233",
+     "availability" : "(available)",
+     "name" : "iOS 9.3",
+     "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-9-3",
+     "version" : "9.3"
+   },
+   {
+     "buildversion" : "15A8401",
+     "availability" : "(available)",
+     "name" : "iOS 11.0",
+     "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-11-0",
+     "version" : "11.0.1"
+   },
+   {
+     "buildversion" : "15J380",
+     "availability" : "(available)",
+     "name" : "tvOS 11.0",
+     "identifier" : "com.apple.CoreSimulator.SimRuntime.tvOS-11-0",
+     "version" : "11.0"
+   },
+   {
+     "buildversion" : "15R372",
+     "availability" : "(available)",
+     "name" : "watchOS 4.0",
+     "identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-4-0",
+     "version" : "4.0"
+   }
+ ],
+ "devices" : {
+   "watchOS 4.0" : [
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple Watch - 38mm",
+       "udid" : "ACCA529B-DAED-4684-ACE5-0BB3A6245064"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple Watch - 42mm",
+       "udid" : "46948CF4-B5E3-485B-87CA-DD303FFA7F9B"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple Watch Series 2 - 38mm",
+       "udid" : "A0A989D0-C5B8-432D-869F-54640FD6739D"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple Watch Series 2 - 42mm",
+       "udid" : "AB05A1C2-1049-4087-BEDB-326B42D58CDD"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Watch2017 - 38mm",
+       "udid" : "8EFAD24B-2EB3-48AF-9484-F97AA418C5D6"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Watch2017 - 42mm",
+       "udid" : "3B477D07-65AD-481A-9506-3776817A6293"
+     }
+   ],
+   "iOS 9.3" : [
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 4s",
+       "udid" : "837FF579-72A0-4D30-B95B-956E89CE6CDC"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 5",
+       "udid" : "46C5F828-1394-4F98-83CA-3CE18020DA5B"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 5s",
+       "udid" : "7760B62D-26D9-4E1E-B429-18CED8CC71E4"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6",
+       "udid" : "19135EEB-2792-4ED6-82AD-374C3F1F5DAC"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6 Plus",
+       "udid" : "60691334-2C32-4366-B489-F13FA3579066"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6s",
+       "udid" : "696BE729-5C61-42FE-9502-E183DB7222C5"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6s Plus",
+       "udid" : "32CBF7F4-36E4-417B-929C-9C3863E6C7FD"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad 2",
+       "udid" : "4043B3B9-8FDE-4ABA-A942-7C7C7126E9AC"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Retina",
+       "udid" : "35FCFEEC-577F-46C1-8389-5195D17D9D76"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Air",
+       "udid" : "867884CE-1B74-4912-B216-8E750BF15699"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Air 2",
+       "udid" : "AB7731B7-9BC5-4EA4-B9C1-3DA6E826D7CC"
+     }
+   ],
+   "tvOS 11.0" : [
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple TV",
+       "udid" : "7BC43B9B-EF0E-4A0A-A3CD-6040688C1D64"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple TV 4K",
+       "udid" : "7C6B05C9-2E4E-4C4A-A1B0-FF842DFD686F"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "Apple TV 4K (at 1080p)",
+       "udid" : "DB10825C-04DD-4A50-8C37-E96C7148076A"
+     }
+   ],
+   "iOS 11.0" : [
+     {
+       "state" : "Booted",
+       "availability" : "(available)",
+       "name" : "iPhone 5s",
+       "udid" : "34FB476C-6FA0-43C8-8945-1BD7A4EBF0DE"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6",
+       "udid" : "0045E516-F2E1-484E-B95D-73E8AA7663A4"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6 Plus",
+       "udid" : "4A518A18-508B-4160-8BF8-EB96F3769834"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6s",
+       "udid" : "9E4697DC-1166-4C49-A4EB-36DEAA14BA55"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 6s Plus",
+       "udid" : "BE8E0A96-F456-4504-BCD2-D8AD9D9267BA"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 7",
+       "udid" : "B5E3E0D2-FFED-44CD-AF8D-AFCB3EBA59DA"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 7 Plus",
+       "udid" : "CD9A6D80-9013-4782-8CC7-F111309DB0E6"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 8",
+       "udid" : "17104B4F-E77D-4019-98E6-621FE3CC3653"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone 8 Plus",
+       "udid" : "51B74402-D1D9-496E-93F5-161D31C83CCD"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone SE",
+       "udid" : "DB46D0DB-510E-4928-BDB4-1A0192ED4A38"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPhone X",
+       "udid" : "4E6E7393-C4E3-4323-AA8B-4A42A45AE7B8"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Air",
+       "udid" : "CC6E7B6D-1A88-4F24-9009-DB8A7B28D234"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Air 2",
+       "udid" : "116F49B6-4ED4-4F8E-B736-226E6915A580"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad (5th generation)",
+       "udid" : "1805162F-861B-40CA-8468-8B7DC0922D62"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Pro (9.7-inch)",
+       "udid" : "5B77D232-EF20-48FE-BC73-2D500F3DF162"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Pro (12.9-inch)",
+       "udid" : "5C46CD8C-07AD-4F41-8314-226CB5D62C30"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Pro (12.9-inch) (2nd generation)",
+       "udid" : "56DFB13A-D2FC-48CD-8D97-B90441999208"
+     },
+     {
+       "state" : "Shutdown",
+       "availability" : "(available)",
+       "name" : "iPad Pro (10.5-inch)",
+       "udid" : "C92DDBB6-14AE-4B19-B9E5-4365FADE66E0"
+     }
+   ]
+ },
+ "pairs" : {
+   "6C37B862-BBB1-4A2F-9D64-174CC38C7A9D" : {
+     "watch" : {
+       "name" : "Apple Watch Series 3 - 38mm",
+       "udid" : "8EFAD24B-2EB3-48AF-9484-F97AA418C5D6",
+       "state" : "Shutdown"
+     },
+     "phone" : {
+       "name" : "iPhone 8",
+       "udid" : "17104B4F-E77D-4019-98E6-621FE3CC3653",
+       "state" : "Shutdown"
+     },
+     "state" : "(active, disconnected)"
+   },
+   "018AB114-B3E5-45FE-8598-6524870D8D5E" : {
+     "watch" : {
+       "name" : "Apple Watch Series 2 - 38mm",
+       "udid" : "A0A989D0-C5B8-432D-869F-54640FD6739D",
+       "state" : "Shutdown"
+     },
+     "phone" : {
+       "name" : "iPhone 7",
+       "udid" : "B5E3E0D2-FFED-44CD-AF8D-AFCB3EBA59DA",
+       "state" : "Shutdown"
+     },
+     "state" : "(active, disconnected)"
+   },
+   "540DD594-89E9-4DFA-87AC-7AD7DDCB9DE8" : {
+     "watch" : {
+       "name" : "Apple Watch Series 2 - 42mm",
+       "udid" : "AB05A1C2-1049-4087-BEDB-326B42D58CDD",
+       "state" : "Shutdown"
+     },
+     "phone" : {
+       "name" : "iPhone 7 Plus",
+       "udid" : "CD9A6D80-9013-4782-8CC7-F111309DB0E6",
+       "state" : "Shutdown"
+     },
+     "state" : "(active, disconnected)"
+   },
+   "2DFF3E54-84B0-4D2F-BF4A-40F3273E44F1" : {
+     "watch" : {
+       "name" : "Apple Watch Series 3 - 42mm",
+       "udid" : "3B477D07-65AD-481A-9506-3776817A6293",
+       "state" : "Shutdown"
+     },
+     "phone" : {
+       "name" : "iPhone 8 Plus",
+       "udid" : "51B74402-D1D9-496E-93F5-161D31C83CCD",
+       "state" : "Shutdown"
+     },
+     "state" : "(active, disconnected)"
+   }
+ }
+}"""
+
+import json
+import unittest
+
+from webkitpy.common.system.executive_mock import MockExecutive2
+from webkitpy.common.system.filesystem_mock import MockFileSystem
+from webkitpy.common.system.systemhost_mock import MockSystemHost
+from webkitpy.common.version import Version
+from webkitpy.xcode.device_type import DeviceType
+from webkitpy.xcode.new_simulated_device import DeviceRequest, SimulatedDeviceManager, SimulatedDevice
+
+
+class SimulatedDeviceTest(unittest.TestCase):
+
+    @staticmethod
+    def reset_simulated_device_manager():
+        SimulatedDeviceManager.AVAILABLE_RUNTIMES = []
+        SimulatedDeviceManager.AVAILABLE_DEVICES = []
+        SimulatedDeviceManager.INITIALIZED_DEVICES = None
+        SimulatedDeviceManager._device_identifier_to_name = False
+        SimulatedDeviceManager._managing_simulator_app = False
+
+    def tearDown(self):
+        SimulatedDeviceTest.reset_simulated_device_manager()
+
+    @staticmethod
+    def mock_host_for_simctl():
+        simctl_json = json.loads(simctl_json_output)  # Construct enough of a filesystem for all our simctl code to work.
+        filesystem_map = {}
+        runtime_name_to_id = {}
+
+        # Runtime mapping
+        for runtime_group in simctl_json['runtimes']:
+            runtime_name_to_id[runtime_group['name']] = runtime_group['identifier']
+
+        # Device type mapping
+        device_type_name_to_id = {}
+        for device_type in simctl_json['devicetypes']:
+            device_type_name_to_id[device_type['name']] = device_type['identifier']
+
+        for runtime, device_groups in simctl_json['devices'].iteritems():
+            for device in device_groups:
+                file_path = '/Users/mock' + SimulatedDeviceManager.simulator_device_path[1:] + '/' + device['udid'] + '/device.plist'
+                # We're taking advantage the fact that the names of the devices match the names of their runtimes in the
+                # provided JSON ouput. This is not generally true, which is why we're only using this fact to build up
+                # a mock filesystem that is used by the actual simctl parsing code.
+                filesystem_map[file_path] = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist SYSTEM "{}">
+<plist version="1.0">
+<dict>
+    <key>UDID</key>
+    <string>{}</string>
+    <key>deviceType</key>
+    <string>{}</string>
+    <key>name</key>
+    <string>{}</string>
+    <key>runtime</key>
+    <string>{}</string>
+    <key>state</key>
+    <integer>{}</integer>
+</dict>
+</plist>""".format(file_path, device['udid'], device_type_name_to_id[device['name']], device['name'], runtime_name_to_id[runtime], SimulatedDevice.NAME_FOR_STATE.index(device['state'].upper()))
+
+        return MockSystemHost(
+            executive=MockExecutive2(output=simctl_json_output),
+            filesystem=MockFileSystem(files=filesystem_map),
+        )
+
+    def test_available_devices(self):
+        SimulatedDeviceTest.reset_simulated_device_manager()
+        host = SimulatedDeviceTest.mock_host_for_simctl()
+        SimulatedDeviceManager.available_devices(host)
+
+        # There should only be 1 iPhone X, iPhone 8 and iPhone SE
+        self.assertEquals(1, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone X'), host)))
+        self.assertEquals(1, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone 8'), host)))
+
+        # There should be 2 5s and 6s
+        self.assertEquals(2, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone 5s'), host)))
+        self.assertEquals(2, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone 6s'), host)))
+
+        # 18 iPhones
+        self.assertEquals(18, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone'), host)))
+
+        # 11 iPads
+        self.assertEquals(11, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPad'), host)))
+
+        # 18 Apple watches
+        self.assertEquals(6, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('Apple Watch'), host)))
+
+        # 3 Apple TVs
+        self.assertEquals(3, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('Apple TV'), host)))
+
+        # 18 devices running iOS 11.0
+        self.assertEquals(18, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType(software_variant='iOS', software_version=Version(11, 0, 1)), host)))
+
+        # 11 iPhones running iOS 11.0
+        self.assertEquals(11, len(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType(hardware_family='iPhone', software_version=Version(11, 0, 1)), host)))
+
+    def test_existing_simulator(self):
+        SimulatedDeviceTest.reset_simulated_device_manager()
+        host = SimulatedDeviceTest.mock_host_for_simctl()
+        SimulatedDeviceManager.available_devices(host)
+
+        SimulatedDeviceManager.initialize_devices(DeviceRequest(DeviceType.from_string('iPhone', Version(11))), host=host)
+
+        self.assertEquals(1, len(SimulatedDeviceManager.INITIALIZED_DEVICES))
+        self.assertEquals('34FB476C-6FA0-43C8-8945-1BD7A4EBF0DE', SimulatedDeviceManager.INITIALIZED_DEVICES[0].udid)
+        self.assertEquals(SimulatedDevice.DeviceState.BOOTED, SimulatedDeviceManager.INITIALIZED_DEVICES[0].platform_device.state())
+
+        SimulatedDeviceManager.tear_down(host)
+        self.assertIsNone(SimulatedDeviceManager.INITIALIZED_DEVICES)
+
+    @staticmethod
+    def change_state_to(device, state):
+        assert isinstance(state, int)
+
+        # Reaching into device.plist to change device state. Note that this will not change the initial state of the device
+        # as determined from the .json output.
+        device_plist = device.filesystem.expanduser(device.filesystem.join(SimulatedDeviceManager.simulator_device_path, device.udid, 'device.plist'))
+        index_position = device.filesystem.files[device_plist].index('</integer>') - 1
+        device.filesystem.files[device_plist] = device.filesystem.files[device_plist][:index_position] + str(state) + device.filesystem.files[device_plist][index_position + 1:]
+
+    def test_swapping_devices(self):
+        SimulatedDeviceTest.reset_simulated_device_manager()
+        host = SimulatedDeviceTest.mock_host_for_simctl()
+        SimulatedDeviceManager.available_devices(host)
+
+        # We won't test the creation and deletion of simulators, only managing existing sims
+        SimulatedDeviceTest.change_state_to(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone 8'), host)[0], SimulatedDevice.DeviceState.BOOTED)
+        SimulatedDeviceTest.change_state_to(SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.device_type == DeviceType.from_string('iPhone X'), host)[0], SimulatedDevice.DeviceState.BOOTED)
+
+        SimulatedDeviceManager.initialize_devices(DeviceRequest(DeviceType.from_string('iPhone 8')), host=host)
+
+        self.assertEquals(1, len(SimulatedDeviceManager.INITIALIZED_DEVICES))
+        self.assertEquals('17104B4F-E77D-4019-98E6-621FE3CC3653', SimulatedDeviceManager.INITIALIZED_DEVICES[0].udid)
+        self.assertEquals(SimulatedDevice.DeviceState.BOOTED, SimulatedDeviceManager.INITIALIZED_DEVICES[0].platform_device.state())
+
+        # Now swap for the X
+        SimulatedDeviceTest.change_state_to(SimulatedDeviceManager.INITIALIZED_DEVICES[0], SimulatedDevice.DeviceState.SHUT_DOWN)
+        SimulatedDeviceManager.swap(SimulatedDeviceManager.INITIALIZED_DEVICES[0], DeviceRequest(DeviceType.from_string('iPhone X')), host)
+
+        self.assertEquals(1, len(SimulatedDeviceManager.INITIALIZED_DEVICES))
+        self.assertEquals('4E6E7393-C4E3-4323-AA8B-4A42A45AE7B8', SimulatedDeviceManager.INITIALIZED_DEVICES[0].udid)
+        self.assertEquals(SimulatedDevice.DeviceState.BOOTED,  SimulatedDeviceManager.INITIALIZED_DEVICES[0].platform_device.state())
+
+        SimulatedDeviceTest.change_state_to(SimulatedDeviceManager.INITIALIZED_DEVICES[0], SimulatedDevice.DeviceState.SHUT_DOWN)
+        SimulatedDeviceManager.tear_down(host)
+        self.assertIsNone(SimulatedDeviceManager.INITIALIZED_DEVICES)