Bug 1150522: Add WebDriver string statuses to Marionette client
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 02 Apr 2015 20:07:20 +0100
changeset 267774 853bf8d3a270c7e6bf79104e7c3c72f196165747
parent 267773 aef7f6bf34708c20e4a4b4cdc7e0eb06dcd397f8
child 267775 0ab6e1161f945ca37372c1a59ee7fe64aae0ad19
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1150522
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1150522: Add WebDriver string statuses to Marionette client Adds string based statuses as defined by the W3C WebDriver protocol to the Marionette Python client. Importantly, it does not remove the ability to look up errors by their Selenium protocol number for backwards compatibility reasons. r=dburns
testing/marionette/client/marionette/__init__.py
testing/marionette/client/marionette/marionette_test.py
testing/marionette/client/marionette/tests/unit/test_errors.py
testing/marionette/client/marionette/tests/unit/test_marionette.py
testing/marionette/driver/marionette_driver/errors.py
testing/marionette/driver/marionette_driver/marionette.py
testing/marionette/error.js
--- a/testing/marionette/client/marionette/__init__.py
+++ b/testing/marionette/client/marionette/__init__.py
@@ -18,9 +18,9 @@ from runner import (
         MarionetteTestResult,
         MarionetteTextTestRunner,
         MemoryEnduranceTestCaseMixin,
         MozHttpd,
         OptionParser,
         TestManifest,
         TestResult,
         TestResultCollection
-)
+)
\ No newline at end of file
--- a/testing/marionette/client/marionette/marionette_test.py
+++ b/testing/marionette/client/marionette/marionette_test.py
@@ -11,17 +11,17 @@ import socket
 import time
 import types
 import unittest
 import weakref
 import warnings
 
 
 from marionette_driver.errors import (
-        ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException,
+        MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException,
         JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException,
         StaleElementException, ScriptTimeoutException, ElementNotVisibleException,
         NoSuchFrameException, InvalidElementStateException, NoAlertPresentException,
         InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException,
         MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError
         )
 from marionette_driver.marionette import Marionette
 from mozlog.structured.structuredlog import get_default_logger
--- a/testing/marionette/client/marionette/tests/unit/test_errors.py
+++ b/testing/marionette/client/marionette/tests/unit/test_errors.py
@@ -1,48 +1,44 @@
 # 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 sys
 
 from marionette import marionette_test
 from marionette_driver import errors
-from marionette_driver.errors import ErrorCodes
 
 def fake_cause():
     try:
         raise ValueError("bar")
     except ValueError as e:
         return sys.exc_info()
 
 message = "foo"
-status = ErrorCodes.TIMEOUT
 cause = fake_cause()
 stacktrace = "first\nsecond"
 
-class TestMarionetteException(marionette_test.MarionetteTestCase):
+class TestExceptionType(marionette_test.MarionetteTestCase):
     def test_defaults(self):
         exc = errors.MarionetteException()
         self.assertIsNone(exc.msg)
-        self.assertEquals(exc.status, ErrorCodes.MARIONETTE_ERROR)
         self.assertIsNone(exc.cause)
         self.assertIsNone(exc.stacktrace)
 
     def test_construction(self):
         exc = errors.MarionetteException(
-            message=message, status=status, cause=cause, stacktrace=stacktrace)
+            message=message, cause=cause, stacktrace=stacktrace)
         self.assertEquals(exc.msg, message)
-        self.assertEquals(exc.status, status)
         self.assertEquals(exc.cause, cause)
         self.assertEquals(exc.stacktrace, stacktrace)
 
     def test_str(self):
         exc = errors.MarionetteException(
-            message=message, status=status, cause=cause, stacktrace=stacktrace)
+            message=message, cause=cause, stacktrace=stacktrace)
         r = str(exc)
         self.assertIn(message, r)
         self.assertIn(", caused by %r" % cause[0], r)
         self.assertIn("\nstacktrace:\n\tfirst\n\tsecond\n", r)
         self.assertIn("MarionetteException:", r)
 
     def test_cause_string(self):
         exc = errors.MarionetteException(cause="foo")
@@ -50,8 +46,33 @@ class TestMarionetteException(marionette
         r = str(exc)
         self.assertIn(", caused by foo", r)
 
     def test_cause_tuple(self):
         exc = errors.MarionetteException(cause=cause)
         self.assertEqual(exc.cause, cause)
         r = str(exc)
         self.assertIn(", caused by %r" % cause[0], r)
+
+
+class TestLookup(marionette_test.MarionetteTestCase):
+    def test_by_known_number(self):
+        self.assertEqual(errors.NoSuchElementException, errors.lookup(7))
+
+    def test_by_unknown_number(self):
+        self.assertEqual(errors.MarionetteException, errors.lookup(123456))
+
+    def test_by_known_string(self):
+        self.assertEqual(errors.NoSuchElementException,
+            errors.lookup("no such element"))
+
+    def test_by_unknown_string(self):
+        self.assertEqual(errors.MarionetteException, errors.lookup("barbera"))
+
+
+class TestAllExceptions(marionette_test.MarionetteTestCase):
+    def test_properties(self):
+        for exc in errors.excs:
+            self.assertTrue(hasattr(exc, "code"),
+                "expected exception to have attribute `code'")
+            self.assertTrue(hasattr(exc, "status"),
+                "expected exception to have attribute `status'")
+            self.assertIsInstance(exc.code, tuple)
--- a/testing/marionette/client/marionette/tests/unit/test_marionette.py
+++ b/testing/marionette/client/marionette/tests/unit/test_marionette.py
@@ -10,13 +10,22 @@ class TestHandleError(marionette_test.Ma
     def test_malformed_packet(self):
         for t in [{}, {"error": None}]:
             with self.assertRaisesRegexp(errors.MarionetteException, "Malformed packet"):
                 self.marionette._handle_error(t)
 
     def test_known_error_code(self):
         with self.assertRaises(errors.NoSuchElementException):
             self.marionette._handle_error(
-                {"error": {"status": errors.ErrorCodes.NO_SUCH_ELEMENT}})
+                {"error": {"status": errors.NoSuchElementException.code[0]}})
+
+    def test_known_error_status(self):
+        with self.assertRaises(errors.NoSuchElementException):
+            self.marionette._handle_error(
+                {"error": {"status": errors.NoSuchElementException.status}})
 
     def test_unknown_error_code(self):
         with self.assertRaises(errors.MarionetteException):
             self.marionette._handle_error({"error": {"status": 123456}})
+
+    def test_unknown_error_status(self):
+        with self.assertRaises(errors.MarionetteException):
+            self.marionette._handle_error({"error": {"status": "barbera"}})
--- a/testing/marionette/driver/marionette_driver/errors.py
+++ b/testing/marionette/driver/marionette_driver/errors.py
@@ -1,72 +1,38 @@
 # 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 traceback
 
-class ErrorCodes(object):
-    SUCCESS = 0
-    NO_SUCH_ELEMENT = 7
-    NO_SUCH_FRAME = 8
-    UNKNOWN_COMMAND = 9
-    STALE_ELEMENT_REFERENCE = 10
-    ELEMENT_NOT_VISIBLE = 11
-    INVALID_ELEMENT_STATE = 12
-    UNKNOWN_ERROR = 13
-    ELEMENT_NOT_ACCESSIBLE = 56
-    ELEMENT_IS_NOT_SELECTABLE = 15
-    JAVASCRIPT_ERROR = 17
-    XPATH_LOOKUP_ERROR = 19
-    TIMEOUT = 21
-    NO_SUCH_WINDOW = 23
-    INVALID_COOKIE_DOMAIN = 24
-    UNABLE_TO_SET_COOKIE = 25
-    UNEXPECTED_ALERT_OPEN = 26
-    NO_ALERT_OPEN = 27
-    SCRIPT_TIMEOUT = 28
-    INVALID_ELEMENT_COORDINATES = 29
-    INVALID_SELECTOR = 32
-    MOVE_TARGET_OUT_OF_BOUNDS = 34
-    INVALID_XPATH_SELECTOR = 51
-    INVALID_XPATH_SELECTOR_RETURN_TYPER = 52
-    INVALID_RESPONSE = 53
-    FRAME_SEND_NOT_INITIALIZED_ERROR = 54
-    FRAME_SEND_FAILURE_ERROR = 55
-    SESSION_NOT_CREATED = 71
-    UNSUPPORTED_OPERATION = 405
-    MARIONETTE_ERROR = 500
 
 class MarionetteException(Exception):
+
     """Raised when a generic non-recoverable exception has occured."""
 
-    def __init__(self, message=None,
-                 status=ErrorCodes.MARIONETTE_ERROR, cause=None,
-                 stacktrace=None):
+    code = (500,)
+    status = "webdriver error"
+
+    def __init__(self, message=None, cause=None, stacktrace=None):
         """Construct new MarionetteException instance.
 
         :param message: An optional exception message.
 
-        :param status: A WebDriver status code given as an integer.
-            By default the generic Marionette error code 500 will be
-            used.
-
         :param cause: An optional tuple of three values giving
             information about the root exception cause.  Expected
             tuple values are (type, value, traceback).
 
         :param stacktrace: Optional string containing a stacktrace
             (typically from a failed JavaScript execution) that will
             be displayed in the exception's string representation.
 
         """
 
         self.msg = message
-        self.status = status
         self.cause = cause
         self.stacktrace = stacktrace
 
     def __str__(self):
         msg = str(self.msg)
         tb = None
 
         if self.cause:
@@ -76,77 +42,180 @@ class MarionetteException(Exception):
             else:
                 msg += ", caused by %s" % self.cause
         if self.stacktrace:
             st = "".join(["\t%s\n" % x for x in self.stacktrace.splitlines()])
             msg += "\nstacktrace:\n%s" % st
 
         return "".join(traceback.format_exception(self.__class__, msg, tb))
 
+
 class InstallGeckoError(MarionetteException):
     pass
 
+
 class TimeoutException(MarionetteException):
-    pass
+    code = (21,)
+    status = "timeout"
+
 
 class InvalidResponseException(MarionetteException):
-    pass
+    code = (53,)
+    status = "invalid response"
+
 
 class JavascriptException(MarionetteException):
-    pass
+    code = (17,)
+    status = "javascript error"
+
 
 class NoSuchElementException(MarionetteException):
-    pass
+    code = (7,)
+    status = "no such element"
+
 
 class XPathLookupException(MarionetteException):
-    pass
+    code = (19,)
+    status = "invalid xpath selector"
+
 
 class NoSuchWindowException(MarionetteException):
-    pass
+    code = (23,)
+    status = "no such window"
+
 
 class StaleElementException(MarionetteException):
-    pass
+    code = (10,)
+    status = "stale element reference"
+
 
 class ScriptTimeoutException(MarionetteException):
-    pass
+    code = (28,)
+    status = "script timeout"
+
 
 class ElementNotVisibleException(MarionetteException):
-    def __init__(self, message="Element is not currently visible and may not be manipulated",
-                 status=ErrorCodes.ELEMENT_NOT_VISIBLE,
+    code = (11,)
+    status = "element not visible"
+
+    def __init__(
+        self, message="Element is not currently visible and may not be manipulated",
                  stacktrace=None, cause=None):
         super(ElementNotVisibleException, self).__init__(
-            message, status=status, cause=cause, stacktrace=stacktrace)
+            message, cause=cause, stacktrace=stacktrace)
+
 
 class ElementNotAccessibleException(MarionetteException):
-    pass
+    code = (56,)
+    status = "element not accessible"
+
 
 class NoSuchFrameException(MarionetteException):
-    pass
+    code = (8,)
+    status = "no such frame"
+
 
 class InvalidElementStateException(MarionetteException):
-    pass
+    code = (12,)
+    status = "invalid element state"
+
 
 class NoAlertPresentException(MarionetteException):
-    pass
+    code = (27,)
+    status = "no such alert"
+
 
 class InvalidCookieDomainException(MarionetteException):
-    pass
+    code = (24,)
+    status = "invalid cookie domain"
+
 
 class UnableToSetCookieException(MarionetteException):
-    pass
+    code = (25,)
+    status = "unable to set cookie"
+
+
+class InvalidElementCoordinates(MarionetteException):
+    code = (29,)
+    status = "invalid element coordinates"
+
 
 class InvalidSelectorException(MarionetteException):
-    pass
+    code = (32, 51, 52)
+    status = "invalid selector"
+
 
 class MoveTargetOutOfBoundsException(MarionetteException):
-    pass
+    code = (34,)
+    status = "move target out of bounds"
+
 
 class FrameSendNotInitializedError(MarionetteException):
-    pass
+    code = (54,)
+    status = "frame send not initialized"
+
 
 class FrameSendFailureError(MarionetteException):
-    pass
+    code = (55,)
+    status = "frame send failure"
+
 
 class UnsupportedOperationException(MarionetteException):
-    pass
+    code = (405,)
+    status = "unsupported operation"
+
 
 class SessionNotCreatedException(MarionetteException):
-    pass
+    code = (33, 71)
+    status = "session not created"
+
+
+class UnexpectedAlertOpen(MarionetteException):
+    code = (26,)
+    status = "unexpected alert open"
+
+excs = [
+    MarionetteException,
+  TimeoutException,
+  InvalidResponseException,
+  JavascriptException,
+  NoSuchElementException,
+  XPathLookupException,
+  NoSuchWindowException,
+  StaleElementException,
+  ScriptTimeoutException,
+  ElementNotVisibleException,
+  ElementNotAccessibleException,
+  NoSuchFrameException,
+  InvalidElementStateException,
+  NoAlertPresentException,
+  InvalidCookieDomainException,
+  UnableToSetCookieException,
+  InvalidElementCoordinates,
+  InvalidSelectorException,
+  MoveTargetOutOfBoundsException,
+  FrameSendNotInitializedError,
+  FrameSendFailureError,
+  UnsupportedOperationException,
+  SessionNotCreatedException,
+  UnexpectedAlertOpen,
+]
+
+
+def lookup(identifier):
+    """Finds error exception class by associated Selenium JSON wire
+    protocol number code, or W3C WebDriver protocol string."""
+
+    by_code = lambda exc: identifier in exc.code
+    by_status = lambda exc: exc.status == identifier
+
+    rv = None
+    if isinstance(identifier, int):
+        rv = filter(by_code, excs)
+    elif isinstance(identifier, str):
+        rv = filter(by_status, excs)
+
+    if not rv:
+        return MarionetteException
+    return rv[0]
+
+
+__all__ = excs + ["lookup"]
--- a/testing/marionette/driver/marionette_driver/marionette.py
+++ b/testing/marionette/driver/marionette_driver/marionette.py
@@ -688,18 +688,17 @@ class Marionette(object):
             message["parameters"] = kwargs
 
         try:
             response = self.client.send(message)
         except socket.timeout:
             self.session = None
             self.window = None
             self.client.close()
-            raise errors.TimeoutException(
-                "Connection timed out", status=errors.ErrorCodes.TIMEOUT)
+            raise errors.TimeoutException("Connection timed out")
 
         # Process any emulator commands that are sent from a script
         # while it's executing.
         while True:
             if response.get("emulator_cmd"):
                 response = self._handle_emulator_cmd(response)
                 continue;
 
@@ -740,70 +739,21 @@ class Marionette(object):
                                  "result": result})
 
     def _handle_error(self, response):
         if "error" not in response or not isinstance(response["error"], dict):
             raise errors.MarionetteException(
                 "Malformed packet, expected key 'error' to be a dict: %s" % response)
 
         error = response["error"]
-        status = error.get("status", 500)
+        status = error.get("status")
         message = error.get("message")
         stacktrace = error.get("stacktrace")
 
-        # status numbers come from
-        # http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
-        if status == errors.ErrorCodes.NO_SUCH_ELEMENT:
-            raise errors.NoSuchElementException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.NO_SUCH_FRAME:
-            raise errors.NoSuchFrameException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.STALE_ELEMENT_REFERENCE:
-            raise errors.StaleElementException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.ELEMENT_NOT_VISIBLE:
-            raise errors.ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.ELEMENT_NOT_ACCESSIBLE:
-            raise errors.ElementNotAccessibleException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.INVALID_ELEMENT_STATE:
-            raise errors.InvalidElementStateException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.UNKNOWN_ERROR:
-            raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.ELEMENT_IS_NOT_SELECTABLE:
-            raise errors.ElementNotSelectableException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.JAVASCRIPT_ERROR:
-            raise errors.JavascriptException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.XPATH_LOOKUP_ERROR:
-            raise errors.XPathLookupException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.TIMEOUT:
-            raise errors.TimeoutException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.NO_SUCH_WINDOW:
-            raise errors.NoSuchWindowException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.INVALID_COOKIE_DOMAIN:
-            raise errors.InvalidCookieDomainException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.UNABLE_TO_SET_COOKIE:
-            raise errors.UnableToSetCookieException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.NO_ALERT_OPEN:
-            raise errors.NoAlertPresentException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.SCRIPT_TIMEOUT:
-            raise errors.ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.INVALID_SELECTOR \
-             or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR \
-             or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER:
-            raise errors.InvalidSelectorException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS:
-            raise errors.MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.FRAME_SEND_NOT_INITIALIZED_ERROR:
-            raise errors.FrameSendNotInitializedError(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.FRAME_SEND_FAILURE_ERROR:
-            raise errors.FrameSendFailureError(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.UNSUPPORTED_OPERATION:
-            raise errors.UnsupportedOperationException(message=message, status=status, stacktrace=stacktrace)
-        elif status == errors.ErrorCodes.SESSION_NOT_CREATED:
-            raise errors.SessionNotCreatedException(message=message, status=status, stacktrace=stacktrace)
-        else:
-            raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace)
+        raise errors.lookup(status)(message, stacktrace=stacktrace)
 
     def _reset_timeouts(self):
         if self.timeout is not None:
             self.timeouts(self.TIMEOUT_SEARCH, self.timeout)
             self.timeouts(self.TIMEOUT_SCRIPT, self.timeout)
             self.timeouts(self.TIMEOUT_PAGE, self.timeout)
         else:
             self.timeouts(self.TIMEOUT_PAGE, 30000)
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -119,16 +119,17 @@ error.stringify = function(err) {
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
  * error in the specification.
  */
 this.WebDriverError = function(msg) {
   Error.call(this, msg);
   this.name = "WebDriverError";
   this.message = msg;
+  this.status = "webdriver error";
   this.code = 500;  // overridden
 };
 WebDriverError.prototype = Object.create(Error.prototype);
 
 this.ElementNotVisibleError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "ElementNotVisibleError";
   this.status = "element not visible";