webkitpy: Sort tests by associated device type
authorjbedard@apple.com <jbedard@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Dec 2018 19:30:47 +0000 (19:30 +0000)
committerjbedard@apple.com <jbedard@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 5 Dec 2018 19:30:47 +0000 (19:30 +0000)
https://bugs.webkit.org/show_bug.cgi?id=192161
<rdar://problem/46345392>

Reviewed by Lucas Forschler.

Sort tests by device type and make an effort to run each specified device type.
Do not run tests if their specified device is not available.

* Scripts/webkitpy/common/system/platforminfo.py:
(PlatformInfo.is_watchos): Add watchos bits to platform info.
(PlatformInfo._determine_os_name): Ditto.
* Scripts/webkitpy/layout_tests/controllers/manager.py:
(Manager.run): Assign each test a device type. Then, generate a list of
device types to sequentially iterate through. Note that a test will run
on the first device which matches.
(Manager._end_test_run): Handle the case where no devices are available and no
tests are run.
* Scripts/webkitpy/layout_tests/run_webkit_tests.py:
(_set_up_derived_options): Manage child processes in manager.
* Scripts/webkitpy/port/base.py:
(Port):
(Port.default_child_processes): Accept additional arguments.
(Port.max_child_processes): Add upper limit for number of child processes.
* Scripts/webkitpy/port/device_port.py:
(DevicePort):
(DevicePort._device_type_with_version): Adds version to the DeviceType.
(DevicePort.default_child_processes): Allows default_child_processes to be
attached to a device type.
(DevicePort.max_child_processes): Add upper limit to the maximum number of child processes.
(DevicePort.setup_test_run): Use _device_type_with_version.
* Scripts/webkitpy/port/ios_device.py:
(IOSDevicePort):
(IOSDevicePort.default_child_processes): Deleted.
* Scripts/webkitpy/port/ios_simulator.py:
(IOSSimulatorPort.default_child_processes): Deleted.
(IOSSimulatorPort.check_sys_deps): Deleted.
* Scripts/webkitpy/port/mac.py:
(MacPort.default_child_processes): Accept additional arguments.
* Scripts/webkitpy/port/test.py:
* Scripts/webkitpy/port/watch_device.py:
(WatchDevicePort):
(WatchDevicePort.default_child_processes): Deleted.
* Scripts/webkitpy/port/watch_simulator.py:
(WatchSimulatorPort.default_child_processes): Deleted.
(WatchSimulatorPort.check_sys_deps): Deleted.
* Scripts/webkitpy/xcode/simulated_device.py:
(SimulatedDeviceManager):
(SimulatedDeviceManager.device_count_for_type): Count the number of devices
available for a specific device type.

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

13 files changed:
Tools/ChangeLog
Tools/Scripts/webkitpy/common/system/platforminfo.py
Tools/Scripts/webkitpy/layout_tests/controllers/manager.py
Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
Tools/Scripts/webkitpy/port/base.py
Tools/Scripts/webkitpy/port/device_port.py
Tools/Scripts/webkitpy/port/ios_device.py
Tools/Scripts/webkitpy/port/ios_simulator.py
Tools/Scripts/webkitpy/port/mac.py
Tools/Scripts/webkitpy/port/test.py
Tools/Scripts/webkitpy/port/watch_device.py
Tools/Scripts/webkitpy/port/watch_simulator.py
Tools/Scripts/webkitpy/xcode/simulated_device.py

index 2d60ef4..1935499 100644 (file)
@@ -1,3 +1,56 @@
+2018-12-05  Jonathan Bedard  <jbedard@apple.com>
+
+        webkitpy: Sort tests by associated device type
+        https://bugs.webkit.org/show_bug.cgi?id=192161
+        <rdar://problem/46345392>
+
+        Reviewed by Lucas Forschler.
+
+        Sort tests by device type and make an effort to run each specified device type.
+        Do not run tests if their specified device is not available.
+
+        * Scripts/webkitpy/common/system/platforminfo.py:
+        (PlatformInfo.is_watchos): Add watchos bits to platform info.
+        (PlatformInfo._determine_os_name): Ditto.
+        * Scripts/webkitpy/layout_tests/controllers/manager.py:
+        (Manager.run): Assign each test a device type. Then, generate a list of
+        device types to sequentially iterate through. Note that a test will run
+        on the first device which matches.
+        (Manager._end_test_run): Handle the case where no devices are available and no
+        tests are run.
+        * Scripts/webkitpy/layout_tests/run_webkit_tests.py:
+        (_set_up_derived_options): Manage child processes in manager.
+        * Scripts/webkitpy/port/base.py:
+        (Port):
+        (Port.default_child_processes): Accept additional arguments.
+        (Port.max_child_processes): Add upper limit for number of child processes.
+        * Scripts/webkitpy/port/device_port.py:
+        (DevicePort):
+        (DevicePort._device_type_with_version): Adds version to the DeviceType.
+        (DevicePort.default_child_processes): Allows default_child_processes to be
+        attached to a device type.
+        (DevicePort.max_child_processes): Add upper limit to the maximum number of child processes.
+        (DevicePort.setup_test_run): Use _device_type_with_version.
+        * Scripts/webkitpy/port/ios_device.py:
+        (IOSDevicePort):
+        (IOSDevicePort.default_child_processes): Deleted.
+        * Scripts/webkitpy/port/ios_simulator.py:
+        (IOSSimulatorPort.default_child_processes): Deleted.
+        (IOSSimulatorPort.check_sys_deps): Deleted.
+        * Scripts/webkitpy/port/mac.py:
+        (MacPort.default_child_processes): Accept additional arguments.
+        * Scripts/webkitpy/port/test.py:
+        * Scripts/webkitpy/port/watch_device.py:
+        (WatchDevicePort):
+        (WatchDevicePort.default_child_processes): Deleted.
+        * Scripts/webkitpy/port/watch_simulator.py:
+        (WatchSimulatorPort.default_child_processes): Deleted.
+        (WatchSimulatorPort.check_sys_deps): Deleted.
+        * Scripts/webkitpy/xcode/simulated_device.py:
+        (SimulatedDeviceManager):
+        (SimulatedDeviceManager.device_count_for_type): Count the number of devices
+        available for a specific device type.
+
 2018-12-05  Devin Rousso  <drousso@apple.com>
 
         Unreviewed, add myself to some watch lists.
index 599bdc8..d8de317 100644 (file)
@@ -72,6 +72,9 @@ class PlatformInfo(object):
     def is_ios(self):
         return self.os_name == 'ios'
 
+    def is_watchos(self):
+        return self.os_name == 'watchos'
+
     def is_win(self):
         return self.os_name == 'win'
 
@@ -179,7 +182,7 @@ class PlatformInfo(object):
     def _determine_os_name(self, sys_platform):
         if sys_platform == 'darwin':
             return 'mac'
-        if sys_platform == 'ios':
+        if sys_platform == 'ios' or sys_platform == 'watchos':
             return 'ios'
         if sys_platform.startswith('linux'):
             return 'linux'
index 961898d..f77843b 100644 (file)
@@ -104,7 +104,6 @@ class Manager(object):
         return self.web_platform_test_subdir in test or self.webkit_specific_web_platform_test_subdir in test
 
     def _custom_device_for_test(self, test):
-        # FIXME: Use available devices instead of CUSTOM_DEVICE_TYPES https://bugs.webkit.org/show_bug.cgi?id=192161
         # FIXME: This is a terrible way to do device-specific expected results https://bugs.webkit.org/show_bug.cgi?id=192162
         for device_type in self._port.CUSTOM_DEVICE_TYPES:
             if device_type.hardware_family and device_type.hardware_family.lower() + self._port.TEST_PATH_SEPARATOR in test:
@@ -206,24 +205,24 @@ class Manager(object):
             _log.critical('No tests to run.')
             return test_run_results.RunDetails(exit_code=-1)
 
-        default_device_tests = []
-
         # Look for tests with custom device requirements.
-        custom_device_tests = defaultdict(list)
+        test_device_mapping = defaultdict(list)
         for test_file in tests_to_run:
-            custom_device = self._custom_device_for_test(test_file)
-            if custom_device:
-                custom_device_tests[custom_device].append(test_file)
+            test_device_mapping[self._custom_device_for_test(test_file) or self._port.DEFAULT_DEVICE_TYPE].append(test_file)
+
+        # Order device types from most specific to least specific in the hopes that some of the more specific device
+        # types will match the less specific device types.
+        device_type_order = []
+        types_with_family = []
+        remaining_types = []
+        for device_type in test_device_mapping.iterkeys():
+            if device_type and device_type.hardware_family and device_type.hardware_type:
+                device_type_order.append(device_type)
+            elif device_type and device_type.hardware_family:
+                types_with_family.append(device_type)
             else:
-                default_device_tests.append(test_file)
-
-        if custom_device_tests:
-            for device_type, tests in custom_device_tests.iteritems():
-                _log.debug('{} tests use device {}'.format(len(tests), device_type))
-
-        initial_results = None
-        retry_results = None
-        enabled_pixel_tests_in_retry = False
+                remaining_types.append(device_type)
+        device_type_order.extend(types_with_family + remaining_types)
 
         needs_http = any((self._is_http_test(test) and not self._needs_web_platform_test(test)) for test in tests_to_run)
         needs_web_platform_test_server = any(self._needs_web_platform_test(test) for test in tests_to_run)
@@ -242,33 +241,48 @@ class Manager(object):
         # Create the output directory if it doesn't already exist.
         self._port.host.filesystem.maybe_make_directory(self._results_directory)
 
-        if default_device_tests:
-            _log.info('')
-            _log.info("Running %s", pluralize(len(tests_to_run), "test"))
-            _log.info('')
-            if not self._set_up_run(tests_to_run):
-                return test_run_results.RunDetails(exit_code=-1)
+        initial_results = None
+        retry_results = None
+        enabled_pixel_tests_in_retry = False
 
-            initial_results, retry_results, enabled_pixel_tests_in_retry = self._run_test_subset(default_device_tests, tests_to_skip)
+        child_processes_option_value = self._options.child_processes
 
-        # Only use a single worker for custom device classes
-        self._options.child_processes = 1
-        for device_type in custom_device_tests:
-            device_tests = custom_device_tests[device_type]
-            if device_tests:
-                _log.info('')
-                _log.info('Running %s for %s', pluralize(len(device_tests), "test"), device_type)
+        while device_type_order:
+            device_type = device_type_order[0]
+            tests = test_device_mapping[device_type]
+            del device_type_order[0]
+
+            self._options.child_processes = min(self._port.max_child_processes(device_type=device_type), int(child_processes_option_value or self._port.default_child_processes(device_type=device_type)))
+
+            _log.info('')
+            if not self._options.child_processes:
+                _log.info('Skipping {} because {} is not available'.format(pluralize(len(test_device_mapping[device_type]), 'test'), str(device_type)))
                 _log.info('')
-                if not self._set_up_run(device_tests, device_type):
-                    return test_run_results.RunDetails(exit_code=-1)
+                continue
 
-                device_initial_results, device_retry_results, device_enabled_pixel_tests_in_retry = self._run_test_subset(device_tests, tests_to_skip)
+            # This loop looks for any less-specific device types which match the current device type
+            index = 0
+            while index < len(device_type_order):
+                if device_type_order[index] == device_type:
+                    tests.extend(test_device_mapping[device_type_order[index]])
 
-                initial_results = initial_results.merge(device_initial_results) if initial_results else device_initial_results
-                retry_results = retry_results.merge(device_retry_results) if retry_results else device_retry_results
-                enabled_pixel_tests_in_retry |= device_enabled_pixel_tests_in_retry
+                    # Remove devices types from device_type_order once tests associated with that type have been claimed.
+                    del device_type_order[index]
+                else:
+                    index += 1
+
+            _log.info('Running {}{}'.format(pluralize(len(tests), 'test'), ' for {}'.format(str(device_type)) if device_type else ''))
+            _log.info('')
+            if not self._set_up_run(tests, device_type):
+                return test_run_results.RunDetails(exit_code=-1)
+
+            temp_initial_results, temp_retry_results, temp_enabled_pixel_tests_in_retry = self._run_test_subset(tests, tests_to_skip)
+            initial_results = initial_results.merge(temp_initial_results) if initial_results else temp_initial_results
+            retry_results = retry_results.merge(temp_retry_results) if retry_results else temp_retry_results
+            enabled_pixel_tests_in_retry |= temp_enabled_pixel_tests_in_retry
 
         self._runner.stop_servers()
+
         end_time = time.time()
         return self._end_test_run(start_time, end_time, initial_results, retry_results, enabled_pixel_tests_in_retry)
 
@@ -298,9 +312,12 @@ class Manager(object):
         return (initial_results, retry_results, enabled_pixel_tests_in_retry)
 
     def _end_test_run(self, start_time, end_time, initial_results, retry_results, enabled_pixel_tests_in_retry):
+        if initial_results is None:
+            _log.error('No results generated')
+            return test_run_results.RunDetails(exit_code=-1)
+
         # Some crash logs can take a long time to be written out so look
         # for new logs after the test run finishes.
-
         _log.debug("looking for new crash logs")
         self._look_for_new_crash_logs(initial_results, start_time)
         if retry_results:
index 4e8e315..747f009 100755 (executable)
@@ -370,10 +370,6 @@ def _print_expectations(port, options, args, logging_stream):
 
 def _set_up_derived_options(port, options):
     """Sets the options values that depend on other options values."""
-    if not options.child_processes:
-        options.child_processes = os.environ.get("WEBKIT_TEST_CHILD_PROCESSES",
-                                                 str(port.default_child_processes()))
-
     if not options.configuration:
         options.configuration = port.default_configuration()
 
index 1b071a5..4310829 100644 (file)
@@ -81,6 +81,7 @@ class Port(object):
 
     DEFAULT_ARCHITECTURE = 'x86'
 
+    DEFAULT_DEVICE_TYPE = None
     CUSTOM_DEVICE_TYPES = []
 
     @classmethod
@@ -176,10 +177,14 @@ class Port(object):
     def should_retry_crashes(self):
         return False
 
-    def default_child_processes(self):
+    def default_child_processes(self, **kwargs):
         """Return the number of DumpRenderTree instances to use for this port."""
         return self._executive.cpu_count()
 
+    def max_child_processes(self, **kwargs):
+        """Forbid the user from specifying more than this number of child processes"""
+        return float('inf')
+
     def worker_startup_delay_secs(self):
         # FIXME: If we start workers up too quickly, DumpRenderTree appears
         # to thrash on something and time out its first few tests. Until
index 19b913c..1734e33 100644 (file)
 import logging
 import traceback
 
-from webkitpy.common.memoized import memoized
 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
 from webkitpy.port.darwin import DarwinPort
 from webkitpy.port.simulator_process import SimulatorProcess
 from webkitpy.xcode.device_type import DeviceType
-from webkitpy.xcode.simulated_device import DeviceRequest
+from webkitpy.xcode.simulated_device import DeviceRequest, SimulatedDeviceManager
 
 
 _log = logging.getLogger(__name__)
@@ -37,7 +36,6 @@ _log = logging.getLogger(__name__)
 class DevicePort(DarwinPort):
 
     DEVICE_MANAGER = None
-    DEFAULT_DEVICE_TYPE = None
     NO_DEVICE_MANAGER = 'No device manager found for port'
 
     def __init__(self, *args, **kwargs):
@@ -59,7 +57,6 @@ class DevicePort(DarwinPort):
                 configurations.append(TestConfiguration(version=self.version_name(), architecture=architecture, build_type=build_type))
         return configurations
 
-    @memoized
     def child_processes(self):
         return int(self.get_option('child_processes'))
 
@@ -106,17 +103,36 @@ class DevicePort(DarwinPort):
             if not device.install_dylibs(self._build_path()):
                 raise RuntimeError('Failed to install dylibs at {} on device {}'.format(self._build_path(), device.udid))
 
-    def setup_test_run(self, device_type=None):
-        if not self.DEVICE_MANAGER:
-            raise RuntimeError(self.NO_DEVICE_MANAGER)
-
+    def _device_type_with_version(self, device_type=None):
         device_type = device_type if device_type else self.DEFAULT_DEVICE_TYPE
-        device_type = DeviceType(
+        return DeviceType(
             hardware_family=device_type.hardware_family,
             hardware_type=device_type.hardware_type,
             software_version=self.device_version(),
             software_variant=device_type.software_variant,
         )
+
+    def default_child_processes(self, device_type=None):
+        if not self.DEVICE_MANAGER:
+            raise RuntimeError(self.NO_DEVICE_MANAGER)
+        if self.DEVICE_MANAGER.INITIALIZED_DEVICES:
+            return len(self.DEVICE_MANAGER.INITIALIZED_DEVICES)
+        return self.DEVICE_MANAGER.device_count_for_type(
+            self._device_type_with_version(device_type),
+            host=self.host,
+            dedicated_simulators=not self.get_option('dedicated_simulators', False),
+        )
+
+    def max_child_processes(self, device_type=None):
+        if self.DEVICE_MANAGER == SimulatedDeviceManager:
+            return super(DevicePort, self).max_child_processes(device_type=device_type)
+        return self.default_child_processes(device_type=device_type)
+
+    def setup_test_run(self, device_type=None):
+        if not self.DEVICE_MANAGER:
+            raise RuntimeError(self.NO_DEVICE_MANAGER)
+
+        device_type = self._device_type_with_version(device_type)
         _log.debug('\nCreating devices for {}'.format(device_type))
 
         request = DeviceRequest(
@@ -125,7 +141,14 @@ class DevicePort(DarwinPort):
             use_existing_simulator=False,
             allow_incomplete_match=True,
         )
-        self.DEVICE_MANAGER.initialize_devices([request] * self.child_processes(), self.host)
+        self.DEVICE_MANAGER.initialize_devices(
+            [request] * self.child_processes(),
+            self.host,
+            layout_test_dir=self.layout_tests_dir(),
+            pin=self.get_option('pin', None),
+            use_nfs=self.get_option('use_nfs', True),
+            reboot=self.get_option('reboot', False),
+        )
 
         if not self.devices():
             raise RuntimeError('No devices are available for testing')
index 6d3a8fd..139422e 100644 (file)
@@ -44,12 +44,6 @@ class IOSDevicePort(IOSPort):
     NO_ON_DEVICE_TESTING = 'On-device testing is not supported in this configuration'
     NO_DEVICE_MANAGER = NO_ON_DEVICE_TESTING
 
-    @memoized
-    def default_child_processes(self):
-        if apple_additions():
-            return apple_additions().ios_device_default_child_processes(self)
-        return 1
-
     def _driver_class(self):
         if apple_additions():
             return apple_additions().ios_device_driver()
@@ -92,13 +86,15 @@ class IOSDevicePort(IOSPort):
         if self.get_option('version'):
             return Version.from_string(self.get_option('version'))
 
-        if not apple_additions():
+        if not self.DEVICE_MANAGER:
             raise RuntimeError(self.NO_ON_DEVICE_TESTING)
 
-        if not self.devices():
+        if not self.DEVICE_MANAGER.available_devices(host=self.host):
             raise RuntimeError('No devices are available')
         version = None
-        for device in self.devices():
+        for device in self.DEVICE_MANAGER.available_devices(host=self.host):
+            if not device.platform.is_ios():
+                continue
             if not version:
                 version = device.platform.os_version
             else:
index da8cef0..6ceda0e 100644 (file)
@@ -58,19 +58,6 @@ class IOSSimulatorPort(IOSPort):
             return Version.from_string(self.get_option('version'))
         return IOSSimulatorPort._version_from_name(self._name) if IOSSimulatorPort._version_from_name(self._name) else self.host.platform.xcode_sdk_version('iphonesimulator')
 
-    @memoized
-    def default_child_processes(self):
-        def booted_ios_devices_filter(device):
-            if not device.platform_device.is_booted_or_booting():
-                return False
-            return device.platform_device.device_type in DeviceType(software_variant='iOS', software_version=self.device_version())
-
-        if not self.get_option('dedicated_simulators', False):
-            num_booted_sims = len(SimulatedDeviceManager.device_by_filter(booted_ios_devices_filter, host=self.host))
-            if num_booted_sims:
-                return num_booted_sims
-        return SimulatedDeviceManager.max_supported_simulators(self.host)
-
     def clean_up_test_run(self):
         super(IOSSimulatorPort, self).clean_up_test_run()
         _log.debug("clean_up_test_run")
@@ -102,14 +89,6 @@ class IOSSimulatorPort(IOSPort):
     def operating_system(self):
         return 'ios-simulator'
 
-    def check_sys_deps(self):
-        target_device_type = DeviceType(software_variant='iOS', software_version=self.device_version())
-        for device in SimulatedDeviceManager.available_devices(self.host):
-            if device.platform_device.device_type in target_device_type:
-                return super(IOSSimulatorPort, self).check_sys_deps()
-        _log.error('No Simulated device matching "{}" defined in Xcode iOS SDK'.format(str(target_device_type)))
-        return False
-
     def reset_preferences(self):
         _log.debug("reset_preferences")
         SimulatedDeviceManager.tear_down(self.host)
index 40a5eac..66b929e 100644 (file)
@@ -188,7 +188,7 @@ class MacPort(DarwinPort):
     def is_mavericks(self):
         return self._version == 'mavericks'
 
-    def default_child_processes(self):
+    def default_child_processes(self, **kwargs):
         default_count = super(MacPort, self).default_child_processes()
 
         # FIXME: https://bugs.webkit.org/show_bug.cgi?id=95906  With too many WebProcess WK2 tests get stuck in resource contention.
index c0664fb..686b6a1 100644 (file)
@@ -418,7 +418,7 @@ class TestPort(Port):
         }
         return [self._webkit_baseline_path(d) for d in search_paths[self.name()]]
 
-    def default_child_processes(self):
+    def default_child_processes(self, **kwargs):
         return 1
 
     def worker_startup_delay_secs(self):
index 18e671b..36ef7e3 100644 (file)
@@ -42,12 +42,6 @@ class WatchDevicePort(WatchPort):
     NO_ON_DEVICE_TESTING = 'On-device testing is not supported in this configuration'
     NO_DEVICE_MANAGER = NO_ON_DEVICE_TESTING
 
-    @memoized
-    def default_child_processes(self):
-        if apple_additions():
-            return len(apple_additions().device_for_worker_number_map(self, software_variant='watchOS'))
-        return 1
-
     def _driver_class(self):
         if apple_additions():
             return apple_additions().device_driver()
@@ -90,13 +84,15 @@ class WatchDevicePort(WatchPort):
         if self.get_option('version'):
             return Version.from_string(self.get_option('version'))
 
-        if not apple_additions():
+        if not self.DEVICE_MANAGER:
             raise RuntimeError(self.NO_ON_DEVICE_TESTING)
 
-        if not apple_additions().device_for_worker_number_map(self, software_variant='watchOS'):
+        if not self.DEVICE_MANAGER.available_devices(host=self.host):
             raise RuntimeError('No devices are available')
         version = None
-        for device in self.devices():
+        for device in self.DEVICE_MANAGER.available_devices(host=self.host):
+            if not device.platform.is_watchos():
+                continue
             if not version:
                 version = device.platform.os_version
             else:
index dfb5333..19d20a2 100644 (file)
@@ -71,30 +71,9 @@ class WatchSimulatorPort(WatchPort):
                 new_environment[value] = inherited_env[value]
         return new_environment
 
-    @memoized
-    def default_child_processes(self):
-        def filter_booted_watchos_devices(device):
-            if not device.platform_device.is_booted_or_booting():
-                return False
-            return device.platform_device.device_type in DeviceType(software_variant='watchOS', software_version=self.device_version())
-
-        if not self.get_option('dedicated_simulators', False):
-            num_booted_sims = len(SimulatedDeviceManager.device_by_filter(filter_booted_watchos_devices, host=self.host))
-            if num_booted_sims:
-                return num_booted_sims
-        return SimulatedDeviceManager.max_supported_simulators(self.host)
-
     def operating_system(self):
         return 'watchos-simulator'
 
-    def check_sys_deps(self):
-        target_device_type = DeviceType(software_variant='watchOS', software_version=self.device_version())
-        for device in SimulatedDeviceManager.available_devices(self.host):
-            if device.platform_device.device_type in target_device_type:
-                return super(WatchSimulatorPort, self).check_sys_deps()
-        _log.error('No simulated device matching "{}" found in watchOS SDK'.format(str(target_device_type)))
-        return False
-
     def setup_environ_for_server(self, server_name=None):
         _log.debug('Setting up environment for server on {}'.format(self.operating_system()))
         env = super(WatchSimulatorPort, self).setup_environ_for_server(server_name)
index 157cb90..832261c 100644 (file)
@@ -325,6 +325,20 @@ class SimulatedDeviceManager(object):
         SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
 
     @staticmethod
+    def device_count_for_type(device_type, host=SystemHost(), use_booted_simulator=True, **kwargs):
+        if not host.platform.is_mac():
+            return 0
+
+        if SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.is_booted_or_booting(), host=host) and use_booted_simulator:
+            filter = lambda device: device.platform_device.is_booted_or_booting() and device.platform_device.device_type in device_type
+            return len(SimulatedDeviceManager.device_by_filter(filter, host=host))
+
+        for name in SimulatedDeviceManager._device_identifier_to_name.itervalues():
+            if DeviceType.from_string(name) in device_type:
+                return SimulatedDeviceManager.max_supported_simulators(host)
+        return 0
+
+    @staticmethod
     def initialize_devices(requests, host=SystemHost(), name_base='Managed', simulator_ui=True, timeout=SIMULATOR_BOOT_TIMEOUT, **kwargs):
         if SimulatedDeviceManager.INITIALIZED_DEVICES is not None:
             return SimulatedDeviceManager.INITIALIZED_DEVICES