Commit d8489ee7 authored by Andreas Tolfsen's avatar Andreas Tolfsen
Browse files

Bug 1411281 - Unmarshal all responses in WPT WebDriver client r=jgraham

The WPT WebDriver client currently only unmarshals responses for some
commands (notably execute_script, execute_async_script, and find.css).
For the client API we want to unmarshal all response bodies automatically.

This patch moves all JSON serialisation/deserialisation to a new
webdriver.protocol package so that it is not scattered around
the client API.  It introduces specialisations of JSONEncoder and
JSONDecoder that allows web element references to be recognised
and converted to complex webdriver.Element objects.

This change means it is no longer necessary for callers to invoke
webdriver.Session._element to convert the response to a web element
as this will be done automatically on any request- and response
body to webdriver.Sesson.send_command.

An important thing to note is that HTTPWireProtocol.send does not  follow
this behaviour by default.  That is because session.transport.send
is used throughout WebDriver tests in WPT as a way to get the raw
JSON body without having to set up session state manually.

MozReview-Commit-ID: 5UyDAe43Hgf

--HG--
extra : rebase_source : b86495dd0dcd102e6dea87011caafad475b88723
parent ca6428c4
Loading
Loading
Loading
Loading
+22 −36
Original line number Diff line number Diff line
import json
import urlparse

import error
import protocol
import transport

from mozlog import get_default_logger
@@ -145,7 +145,7 @@ class ActionSequence(object):
        if duration is not None:
            action["duration"] = duration
        if origin is not None:
            action["origin"] = origin if isinstance(origin, basestring) else origin.json()
            action["origin"] = origin
        self._actions.append(action)
        return self

@@ -297,18 +297,9 @@ class Find(object):

    def _find_element(self, strategy, selector, all):
        route = "elements" if all else "element"

        body = {"using": strategy,
                "value": selector}

        data = self.session.send_session_command("POST", route, body)

        if all:
            rv = [self.session._element(item) for item in data]
        else:
            rv = self.session._element(data)

        return rv
        return self.session.send_session_command("POST", route, body)


class Cookies(object):
@@ -432,10 +423,15 @@ class Session(object):
            the `value` field returned after parsing the response
            body as JSON.

        :raises ValueError: If the response body does not contain a
            `value` key.
        :raises error.WebDriverException: If the remote end returns
            an error.
        """
        response = self.transport.send(method, url, body)
        response = self.transport.send(
            method, url, body,
            encoder=protocol.Encoder, decoder=protocol.Decoder,
            session=self)

        if response.status != 200:
            raise error.from_response(response)
@@ -443,9 +439,8 @@ class Session(object):
        if "value" in response.body:
            value = response.body["value"]
        else:
            raise error.UnknownErrorException(
                "Expected 'value' key in response body:\n"
                "%s" % json.dumps(response.body))
            raise ValueError("Expected 'value' key in response body:\n"
                "%s" % response)

        return value

@@ -520,9 +515,6 @@ class Session(object):
            body = None
        else:
            url = "frame"
            if isinstance(frame, Element):
                body = {"id": frame.json()}
            else:
            body = {"id": frame}

        return self.send_session_command("POST", url, body)
@@ -539,16 +531,7 @@ class Session(object):
    @property
    @command
    def active_element(self):
        data = self.send_session_command("GET", "element/active")
        if data is not None:
            return self._element(data)

    def _element(self, data):
        elem_id = data[Element.identifier]
        assert elem_id
        if elem_id in self._element_cache:
            return self._element_cache[elem_id]
        return Element(elem_id, self)
        return self.send_session_command("GET", "element/active")

    @command
    def cookies(self, name=None):
@@ -637,20 +620,23 @@ class Element(object):
        return isinstance(other, Element) and self.id == other.id \
                and self.session == other.session

    @classmethod
    def from_json(cls, json, session):
        assert Element.identifier in json
        uuid = json[Element.identifier]
        if uuid in session._element_cache:
            return session._element_cache[uuid]
        return cls(uuid, session)

    def send_element_command(self, method, uri, body=None):
        url = "element/%s/%s" % (self.id, uri)
        return self.session.send_session_command(method, url, body)

    def json(self):
        return {Element.identifier: self.id}

    @command
    def find_element(self, strategy, selector):
        body = {"using": strategy,
                "value": selector}

        elem = self.send_element_command("POST", "element", body)
        return self.session._element(elem)
        return self.send_element_command("POST", "element", body)

    @command
    def click(self):
+35 −0
Original line number Diff line number Diff line
import json

import webdriver


"""WebDriver wire protocol codecs."""


class Encoder(json.JSONEncoder):
    def __init__(self, *args, **kwargs):
        kwargs.pop("session")
        super(Encoder, self).__init__(*args, **kwargs)

    def default(self, obj):
        if isinstance(obj, (list, tuple)):
            return [self.default(x) for x in obj]
        elif isinstance(obj, webdriver.Element):
            return {webdriver.Element.identifier: obj.id}
        return super(ProtocolEncoder, self).default(obj)


class Decoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        self.session = kwargs.pop("session")
        super(Decoder, self).__init__(
            object_hook=self.object_hook, *args, **kwargs)

    def object_hook(self, payload):
        if isinstance(payload, (list, tuple)):
            return [self.object_hook(x) for x in payload]
        elif isinstance(payload, dict) and webdriver.Element.identifier in payload:
            return webdriver.Element.from_json(payload, self.session)
        elif isinstance(payload, dict):
            return {k: self.object_hook(v) for k, v in payload.iteritems()}
        return payload
+80 −42
Original line number Diff line number Diff line
@@ -5,6 +5,9 @@ import urlparse
import error


"""Implements HTTP transport for the WebDriver wire protocol."""


class Response(object):
    """
    Describes an HTTP response received from a remote end whose
@@ -16,10 +19,12 @@ class Response(object):
        self.body = body

    def __repr__(self):
        cls_name = self.__class__.__name__
        if self.error:
            return "<%s status=%s error=%s>" % (cls_name, self.status, repr(self.error))
        return "<% status=%s body=%s>" % (cls_name, self.status, self.body)
        return "<% status=%s body=%s>" % (cls_name, self.status, json.dumps(self.body))

    def __str__(self):
        return json.dumps(self.body, indent=2)

    @property
    def error(self):
@@ -28,38 +33,40 @@ class Response(object):
        return None

    @classmethod
    def from_http_response(cls, http_response):
        status = http_response.status
        body = http_response.read()

        # SpecID: dfn-send-a-response
        #
        # > 3. Set the response's header with name and value with the following
        # >    values:
        # >
        # >    "Content-Type"
        # >       "application/json; charset=utf-8"
        # >    "cache-control"
        # >       "no-cache"

        if body:
    def from_http(cls, http_response, decoder=json.JSONDecoder, **kwargs):
        try:
                body = json.loads(body)
            except:
                raise error.UnknownErrorException("Failed to decode body as json:\n%s" % body)
            body = json.load(http_response, cls=decoder, **kwargs)
        except ValueError:
            raise ValueError("Failed to decode response body as JSON:\n"
                "%s" % json.dumps(body, indent=2))

        return cls(status, body)


class ToJsonEncoder(json.JSONEncoder):
    def default(self, obj):
        return getattr(obj.__class__, "json", json.JSONEncoder().default)(obj)
        return cls(http_response.status, body)


class HTTPWireProtocol(object):
    """
    Transports messages (commands and responses) over the WebDriver
    wire protocol.

    Complex objects, such as ``webdriver.Element``, are by default
    not marshaled to enable use of `session.transport.send` in WPT tests::

        session = webdriver.Session("127.0.0.1", 4444)
        response = transport.send("GET", "element/active", None)
        print response.body["value"]
        # => {u'element-6066-11e4-a52e-4f735466cecf': u'<uuid>'}

    Automatic marshaling is provided by ``webdriver.protocol.Encoder``
    and ``webdriver.protocol.Decoder``, which can be passed in to
    ``HTTPWireProtocol.send`` along with a reference to the current
    ``webdriver.Session``::

        session = webdriver.Session("127.0.0.1", 4444)
        response = transport.send("GET", "element/active", None,
            encoder=protocol.Encoder, decoder=protocol.Decoder,
            session=session)
        print response.body["value"]
        # => webdriver.Element
    """

    def __init__(self, host, port, url_prefix="/", timeout=None):
@@ -78,43 +85,74 @@ class HTTPWireProtocol(object):
    def url(self, suffix):
        return urlparse.urljoin(self.url_prefix, suffix)

    def send(self, method, uri, body=None, headers=None):
    def send(self,
             method,
             uri,
             body=None,
             headers=None,
             encoder=json.JSONEncoder,
             decoder=json.JSONDecoder,
             **codec_kwargs):
        """
        Send a command to the remote.

        The request `body` must be JSON serialisable unless a
        custom `encoder` has been provided.  This means complex
        objects such as ``webdriver.Element`` are not automatically
        made into JSON.  This behaviour is, however, provided by
        ``webdriver.protocol.Encoder``, should you want it.

        Similarly, the response body is returned au natural
        as plain JSON unless a `decoder` that converts web
        element references to ``webdriver.Element`` is provided.
        Use ``webdriver.protocol.Decoder`` to achieve this behaviour.

        :param method: `GET`, `POST`, or `DELETE`.
        :param uri: Relative endpoint of the requests URL path.
        :param body: Body of the request.  Defaults to an empty
            dictionary if ``method`` is `POST`.
        :param headers: Additional headers to include in the request.

        :return: Instance of ``wdclient.Response`` describing the
            HTTP response received from the remote end.
        :param headers: Additional dictionary of headers to include
            in the request.
        :param encoder: JSON encoder class, which defaults to
            ``json.JSONEncoder`` unless specified.
        :param decoder: JSON decoder class, which defaults to
            ``json.JSONDecoder`` unless specified.
        :param codec_kwargs: Surplus arguments passed on to `encoder`
            and `decoder` on construction.

        :return: Instance of ``webdriver.transport.Response``
            describing the HTTP response received from the remote end.

        :raises ValueError: If `body` or the response body are not
            JSON serialisable.
        """
        if body is None and method == "POST":
            body = {}

        if isinstance(body, dict):
            body = json.dumps(body, cls=ToJsonEncoder)

        if isinstance(body, unicode):
            body = body.encode("utf-8")
        try:
            payload = json.dumps(body, cls=encoder, **codec_kwargs)
        except ValueError:
            raise ValueError("Failed to encode request body as JSON:\n"
                "%s" % json.dumps(body, indent=2))
        if isinstance(payload, unicode):
            payload = body.encode("utf-8")

        if headers is None:
            headers = {}

        url = self.url(uri)

        kwargs = {}
        conn_kwargs = {}
        if self._timeout is not None:
            kwargs["timeout"] = self._timeout
            conn_kwargs["timeout"] = self._timeout

        conn = httplib.HTTPConnection(
            self.host, self.port, strict=True, **kwargs)
        conn.request(method, url, body, headers)
            self.host, self.port, strict=True, **conn_kwargs)
        conn.request(method, url, payload, headers)

        try:
            response = conn.getresponse()
            return Response.from_http_response(response)
            return Response.from_http(
                response, decoder=decoder, **codec_kwargs)
        finally:
            conn.close()