Bug 1316622 - New timeouts inteface in Marionette Python client; r=automatedtester, a=test-only
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 10 Nov 2016 21:00:23 +0000
changeset 352626 cbd82915080b85d8811c95f792445911423aefbd
parent 352625 09b3b8d9ac655235522c964dc70fab4076c9c783
child 352627 d5ea435d52124b562c002d8099bb1bff97aeb833
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester, test-only
bugs1316622
milestone52.0a2
Bug 1316622 - New timeouts inteface in Marionette Python client; r=automatedtester, a=test-only Introduce a new interface for managing timeouts in the Marionette Python client. MozReview-Commit-ID: JHojs7rWBz5
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/client/marionette_driver/timeout.py
testing/marionette/client/marionette_driver/wait.py
testing/marionette/harness/marionette/tests/unit/test_timeouts.py
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -13,28 +13,28 @@ import time
 import traceback
 import warnings
 
 from contextlib import contextmanager
 
 from decorators import do_process_check
 from keys import Keys
 
+import errors
 import geckoinstance
-import errors
 import transport
+from timeout import Timeouts
+
 
 WEBELEMENT_KEY = "ELEMENT"
 W3C_WEBELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
 
 
 class HTMLElement(object):
-    """
-    Represents a DOM Element.
-    """
+    """Represents a DOM Element."""
 
     def __init__(self, marionette, id):
         self.marionette = marionette
         assert(id is not None)
         self.id = id
 
     def __str__(self):
         return self.id
@@ -533,58 +533,72 @@ class Alert(object):
         tab modal dialog."""
         body = {"value": Marionette.convert_keys(*string)}
         self.marionette._send_message("sendKeysToDialog", body)
 
 
 class Marionette(object):
     """Represents a Marionette connection to a browser or device."""
 
-    CONTEXT_CHROME = 'chrome'  # non-browser content: windows, dialogs, etc.
-    CONTEXT_CONTENT = 'content'  # browser content: iframes, divs, etc.
+    CONTEXT_CHROME = "chrome"  # non-browser content: windows, dialogs, etc.
+    CONTEXT_CONTENT = "content"  # browser content: iframes, divs, etc.
     DEFAULT_SOCKET_TIMEOUT = 60
     DEFAULT_STARTUP_TIMEOUT = 120
     DEFAULT_SHUTDOWN_TIMEOUT = 65  # Firefox will kill hanging threads after 60s
 
-    def __init__(self, host='localhost', port=2828, app=None, bin=None,
+    def __init__(self, host="localhost", port=2828, app=None, bin=None,
                  baseurl=None, timeout=None, socket_timeout=DEFAULT_SOCKET_TIMEOUT,
                  startup_timeout=None, **instance_args):
-        """
-        :param host: address for Marionette connection
-        :param port: integer port for Marionette connection
-        :param baseurl: where to look for files served from Marionette's www directory
-        :param startup_timeout: seconds to wait for a connection with binary
-        :param timeout: time to wait for page load, scripts, search
-        :param socket_timeout: timeout for Marionette socket operations
-        :param bin: path to app binary; if any truthy value is given this will
-            attempt to start a gecko instance with the specified `app`
-        :param app: type of instance_class to use for managing app instance.
-            See marionette_driver.geckoinstance
-        :param instance_args: args to pass to instance_class
+        """Construct a holder for the Marionette connection.
+
+        Remember to call ``start_session`` in order to initiate the
+        connection and start a Marionette session.
+
+        :param host: Host where the Marionette server listens.
+            Defaults to localhost.
+        :param port: Port where the Marionette server listens.
+            Defaults to port 2828.
+        :param baseurl: Where to look for files served from Marionette's
+            www directory.
+        :param timeout: Dictionary of default page load, script, and
+            implicit wait timeouts.  Timeouts in the session are reset
+            to these values whenever ``reset_timeouts`` is called.
+        :param socket_timeout: Timeout for Marionette socket operations.
+        :param startup_timeout: Seconds to wait for a connection with
+            binary.
+        :param bin: Path to browser binary.  If any truthy value is given
+            this will attempt to start a Gecko instance with the specified
+            `app`.
+        :param app: Type of ``instance_class`` to use for managing app
+            instance. See ``marionette_driver.geckoinstance``.
+        :param instance_args: Arguments to pass to ``instance_class``.
+
         """
         self.host = host
         self.port = self.local_port = int(port)
         self.bin = bin
+        self.default_timeouts = timeout
         self.instance = None
         self.session = None
         self.session_id = None
         self.window = None
         self.chrome_window = None
         self.baseurl = baseurl
         self._test_name = None
-        self.timeout = timeout
         self.socket_timeout = socket_timeout
         self.crashed = 0
 
         startup_timeout = startup_timeout or self.DEFAULT_STARTUP_TIMEOUT
         if self.bin:
             self.instance = self._create_instance(app, instance_args)
             self.instance.start()
             self.raise_for_port(timeout=startup_timeout)
 
+        self.timeout = Timeouts(self)
+
     def _create_instance(self, app, instance_args):
         if not Marionette.is_port_available(self.port, host=self.host):
             ex_msg = "{0}:{1} is unavailable.".format(self.host, self.port)
             raise errors.MarionetteException(message=ex_msg)
         if app:
             # select instance class for the given app
             try:
                 instance_class = geckoinstance.apps[app]
@@ -758,31 +772,30 @@ class Marionette(object):
         else:
             error = obj["error"]
             message = obj["message"]
             stacktrace = obj["stacktrace"]
 
         raise errors.lookup(error)(message, stacktrace=stacktrace)
 
     def reset_timeouts(self):
-        """Resets timeouts to their defaults to the `self.timeout`
-        attribute. If unset, only the page load timeout is reset to
-        30 seconds.
+        """Resets timeouts to their defaults to the
+        `self.default_timeouts` attribute. If unset, only the page load
+        timeout is reset to 30 seconds.
 
         """
-
-        timeout_types = {"search": self.set_search_timeout,
-                         "script": self.set_script_timeout,
-                         "page load": self.set_page_load_timeout}
+        setters = {"search": "implicit",
+                   "script": "script",
+                   "page load": "page_load"}
 
-        if self.timeout is not None:
-            for typ, ms in self.timeout:
-                timeout_types[typ](ms)
+        if self.default_timeouts is not None:
+            for typ, ms in self.default_timeouts:
+                setattr(self.timeout, setters[typ], ms)
         else:
-            self.set_page_load_timeout(30000)
+            self.timeout.page_load = 30
 
     def check_for_crash(self):
         """Check if the process crashed.
 
         :returns: True, if a crash happened since the method has been called the last time.
         """
         crash_count = 0
 
@@ -1820,17 +1833,17 @@ class Marionette(object):
             in which case no globals are preserved.
         :param debug_script: Capture javascript exceptions when in
             `CONTEXT_CHROME` context.
 
         Usage example:
 
         ::
 
-            marionette.set_script_timeout(10000) # set timeout period of 10 seconds
+            marionette.timeout.script = 10
             result = self.marionette.execute_async_script('''
               // this script waits 5 seconds, and then returns the number 1
               setTimeout(function() {
                 marionetteScriptFinished(1);
               }, 5000);
             ''')
             assert result == 1
         """
@@ -1850,17 +1863,17 @@ class Marionette(object):
 
     def find_element(self, method, target, id=None):
         """Returns an HTMLElement instances that matches the specified
         method and target in the current context.
 
         An HTMLElement instance may be used to call other methods on the
         element, such as click().  If no element is immediately found, the
         attempt to locate an element will be repeated for up to the amount of
-        time set by set_search_timeout(). If multiple elements match the given
+        time set by ``timeout.implicit``. If multiple elements match the given
         criteria, only the first is returned. If no element matches, a
         NoSuchElementException will be raised.
 
         :param method: The method to use to locate the element; one of:
             "id", "name", "class name", "tag name", "css selector",
             "link text", "partial link text", "xpath", "anon" and "anon
             attribute". Note that the "name", "link text" and "partial
             link test" methods are not supported in the chrome DOM.
@@ -1877,17 +1890,17 @@ class Marionette(object):
 
     def find_elements(self, method, target, id=None):
         """Returns a list of all HTMLElement instances that match the
         specified method and target in the current context.
 
         An HTMLElement instance may be used to call other methods on the
         element, such as click().  If no element is immediately found,
         the attempt to locate an element will be repeated for up to the
-        amount of time set by set_search_timeout().
+        amount of time set by ``timeout.implicit``.
 
         :param method: The method to use to locate the elements; one
             of: "id", "name", "class name", "tag name", "css selector",
             "link text", "partial link text", "xpath", "anon" and "anon
             attribute". Note that the "name", "link text" and "partial link
             test" methods are not supported in the chrome DOM.
         :param target: The target of the search.  For example, if method =
             "tag", target might equal "div".  If method = "id", target would be
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/timeout.py
@@ -0,0 +1,87 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+import errors
+
+
+class Timeouts(object):
+    """Manage timeout settings in the Marionette session.
+
+    Usage::
+
+        marionette = Marionette(...)
+        marionette.start_session()
+        marionette.timeout.page_load = 10
+        marionette.timeout.page_load
+        # => 10
+
+    """
+
+    def __init__(self, marionette):
+        self._marionette = marionette
+
+    def _set(self, name, sec):
+        ms = sec * 1000
+        try:
+            self._marionette._send_message("setTimeouts", {name: ms})
+        except errors.UnknownCommandException:
+            # remove when 55 is stable
+            self._marionette._send_message("timeouts", {"type": name, "ms": ms})
+
+    def _get(self, name):
+        ms = self._marionette._send_message("getTimeouts", key=name)
+        return ms / 1000
+
+    @property
+    def script(self):
+        """Get the session's script timeout.  This specifies the time
+        to wait for injected scripts to finished before interrupting
+        them. It is by default 30 seconds.
+
+        """
+        return self._get("script")
+
+    @script.setter
+    def script(self, sec):
+        """Set the session's script timeout.  This specifies the time
+        to wait for injected scripts to finish before interrupting them.
+
+        """
+        self._set("script", sec)
+
+    @property
+    def page_load(self):
+        """Get the session's page load timeout.  This specifies the time
+        to wait for the page loading to complete.  It is by default 5
+        minutes (or 300 seconds).
+
+        """
+        return self._get("page load")
+
+    @page_load.setter
+    def page_load(self, sec):
+        """Set the session's page load timeout.  This specifies the time
+        to wait for the page loading to complete.
+
+        """
+        self._set("page load", sec)
+
+    @property
+    def implicit(self):
+        """Get the session's implicit wait timeout.  This specifies the
+        time to wait for the implicit element location strategy when
+        retrieving elements.  It is by default disabled (0 seconds).
+
+        """
+        return self._get("implicit")
+
+    @implicit.setter
+    def implicit(self, sec):
+        """Set the session's implicit wait timeout.  This specifies the
+        time to wait for the implicit element location strategy when
+        retrieving elements.
+
+        """
+        self._set("implicit", sec)
--- a/testing/marionette/client/marionette_driver/wait.py
+++ b/testing/marionette/client/marionette_driver/wait.py
@@ -66,18 +66,22 @@ class Wait(object):
 
         :param clock: Allows overriding the use of the runtime's
             default time library.  See `wait.SystemClock` for
             implementation details.
 
         """
 
         self.marionette = marionette
-        self.timeout = timeout or (self.marionette.timeout and
-                                   self.marionette.timeout / 1000.0) or DEFAULT_TIMEOUT
+        self.timeout = timeout
+        if self.timeout is None:
+            if self.marionette.default_timeouts is not None:
+                self.timeout = self.marionette.default_timeouts.get("search")
+            else:
+                self.timeout = DEFAULT_TIMEOUT
         self.clock = clock or SystemClock()
         self.end = self.clock.now + self.timeout
         self.interval = interval
 
         exceptions = []
         if ignored_exceptions is not None:
             if isinstance(ignored_exceptions, collections.Iterable):
                 exceptions.extend(iter(ignored_exceptions))
--- a/testing/marionette/harness/marionette/tests/unit/test_timeouts.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_timeouts.py
@@ -63,23 +63,16 @@ class TestTimeouts(MarionetteTestCase):
         test_html = self.marionette.absolute_url("test.html")
         self.marionette.navigate(test_html)
         self.marionette.set_script_timeout(10000)
         self.assertTrue(self.marionette.execute_async_script("""
              var callback = arguments[arguments.length - 1];
              setTimeout(function() { callback(true); }, 500);
              """))
 
-    def test_invalid_timeout_types(self):
-        for val in [3.14, True, [], {}, "foo"]:
-            print "testing %s" % type(val)
-            self.assertRaises(InvalidArgumentException, self.marionette.set_search_timeout, val)
-            self.assertRaises(InvalidArgumentException, self.marionette.set_script_timeout, val)
-            self.assertRaises(InvalidArgumentException, self.marionette.set_page_load_timeout, val)
-
     def test_compat_input_types(self):
         # When using the spec-incompatible input format which we have
         # for backwards compatibility, it should be possible to send ms
         # as a string type and have the server parseInt it to an integer.
         body = {"type": "script", "ms": "30000"}
         self.marionette._send_message("setTimeouts", body)
 
     def test_deprecated_set_timeouts_command(self):