Bug 1388424 - Read capabilities off top-level object. r?whimboo draft
authorAndreas Tolfsen <ato@sny.no>
Tue, 08 Aug 2017 17:37:41 +0100
changeset 642754 b4ac45286176751e9925586883c0178bda7855b3
parent 642518 a921bfb8a2cf3db4d9edebe9b35799a3f9d035da
child 725085 e98659134ba468ecb2c5aff41dba2e371a057b57
push id72850
push userbmo:ato@sny.no
push dateTue, 08 Aug 2017 17:49:42 +0000
reviewerswhimboo
bugs1388424, 1387380
milestone57.0a1
Bug 1388424 - Read capabilities off top-level object. r?whimboo geckodriver sends capabilities as a JSON Object in the body of the command, like this: [0,1,"newSession",{"acceptInsecureCerts":true}] With https://bugzil.la/1387380 we wanted the Marionette Python client to match this behaviour, however the patch overlooked the fact that the server reads cmd.parameters.capabilities, meaning it looks for a "capabilities" field on this object instead of treating the object as the dictionary of capabilities. As a follow-up to that bug, this patch removes the ability to override the session ID by specifying a "sessionId" field. This functionality was only used for in-app restart tests. When Firefox restarts, the Marionette session is arguably not the same, and sessions should not live on between restarts. This patch will fix capabilities passed from geckodriver and align the Marionette Python client. For backwards compatibility reasons, it needs to be possible to use the Python client with older Firefoxen that reads cmd.parameters.capabilities instead of cmd.parameters. This is why we duplicate the capabilities object, like geckodriver does. MozReview-Commit-ID: DCpaxl9hOLe
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_session.py
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -1121,17 +1121,16 @@ class Marionette(object):
         :param callback: If provided and `in_app` is True, the callback will be
                          used to trigger the restart.
         """
         if not self.instance:
             raise errors.MarionetteException("restart() can only be called "
                                              "on Gecko instances launched by Marionette")
 
         context = self._send_message("getContext", key="value")
-        session_id = self.session_id
 
         if in_app:
             if clean:
                 raise ValueError("An in_app restart cannot be triggered with the clean flag set")
 
             if callable(callback):
                 self._send_message("acceptConnections", {"value": False})
                 callback()
@@ -1150,17 +1149,17 @@ class Marionette(object):
                     self.cleanup()
                     raise exc, "Requested restart of the application was aborted", tb
 
         else:
             self.delete_session()
             self.instance.restart(clean=clean)
             self.raise_for_port(timeout=self.DEFAULT_STARTUP_TIMEOUT)
 
-        self.start_session(session_id=session_id)
+        self.start_session()
 
         # Restore the context as used before the restart
         self.set_context(context)
 
         if in_app and self.process_id:
             # In some cases Firefox restarts itself by spawning into a new process group.
             # As long as mozprocess cannot track that behavior (bug 1284864) we assist by
             # informing about the new process id.
@@ -1170,30 +1169,28 @@ class Marionette(object):
         '''
         Returns an absolute url for files served from Marionette's www directory.
 
         :param relative_url: The url of a static file, relative to Marionette's www directory.
         '''
         return "{0}{1}".format(self.baseurl, relative_url)
 
     @do_process_check
-    def start_session(self, capabilities=None, session_id=None, timeout=60):
+    def start_session(self, capabilities=None, timeout=60):
         """Create a new WebDriver session.
 
         This method must be called before performing any other action.
 
         :param capabilities: An optional dictionary of
             Marionette-recognised capabilities.  It does not
             accept a WebDriver conforming capabilities dictionary
             (including alwaysMatch, firstMatch, desiredCapabilities,
             or requriedCapabilities), and only recognises extension
             capabilities that are specific to Marionette.
         :param timeout: Timeout in seconds for the server to be ready.
-        :param session_id: Unique identifier for the session. If no
-            session ID is passed in then one will be generated.
 
         :returns: A dictionary of the capabilities offered.
 
         """
         self.crashed = 0
 
         if self.instance:
             returncode = self.instance.runner.returncode
@@ -1207,17 +1204,34 @@ class Marionette(object):
             self.socket_timeout)
 
         # Call wait_for_port() before attempting to connect in
         # the event gecko hasn't started yet.
         timeout = timeout or self.startup_timeout
         self.wait_for_port(timeout=timeout)
         self.protocol, _ = self.client.connect()
 
-        body = {"capabilities": capabilities, "sessionId": session_id}
+        body = capabilities
+        if body is None:
+            body = {}
+
+        # Duplicate capabilities object so the body we end up
+        # sending looks like this:
+        #
+        #     {acceptInsecureCerts: true, {capabilities: {acceptInsecureCerts: true}}}
+        #
+        # We do this because geckodriver sends the capabilities at the
+        # top-level, and after bug 1388424 removed support for overriding
+        # the session ID, we also do this with this client.  However,
+        # because this client is used with older Firefoxen (through upgrade
+        # tests et al.) we need to preserve backwards compatibility until
+        # Firefox 60.
+        if "capabilities" not in body and capabilities is not None:
+            body["capabilities"] = dict(capabilities)
+
         resp = self._send_message("newSession", body)
 
         self.session_id = resp["sessionId"]
         self.session = resp["value"] if self.protocol == 1 else resp["capabilities"]
         # fallback to processId can be removed in Firefox 55
         self.process_id = self.session.get("moz:processID", self.session.get("processId"))
         self.profile = self.session.get("moz:profile")
 
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -767,22 +767,21 @@ GeckoDriver.prototype.listeningPromise =
  * @throws {SessionNotCreatedError}
  *     If, for whatever reason, a session could not be created.
  */
 GeckoDriver.prototype.newSession = function* (cmd, resp) {
   if (this.sessionID) {
     throw new SessionNotCreatedError("Maximum number of active sessions");
   }
 
-  this.sessionID = cmd.parameters.sessionId || element.generateUUID();
+  this.sessionID = element.generateUUID();
   this.newSessionCommandId = cmd.id;
 
   try {
-    this.capabilities = session.Capabilities.fromJSON(
-        cmd.parameters.capabilities);
+    this.capabilities = session.Capabilities.fromJSON(cmd.parameters);
   } catch (e) {
     throw new SessionNotCreatedError(e);
   }
 
   if (!this.secureTLS) {
     logger.warn("TLS certificate errors will be ignored for this session");
     let acceptAllCerts = new cert.InsecureSweepingOverride();
     cert.installOverride(acceptAllCerts);
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_session.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_session.py
@@ -31,23 +31,16 @@ class TestSession(MarionetteTestCase):
 
     def test_get_session_id(self):
         # Sends newSession
         self.marionette.start_session()
 
         self.assertTrue(self.marionette.session_id is not None)
         self.assertTrue(isinstance(self.marionette.session_id, unicode))
 
-    def test_set_the_session_id(self):
-        # Sends newSession
-        self.marionette.start_session(session_id="ILoveCheese")
-
-        self.assertEqual(self.marionette.session_id, "ILoveCheese")
-        self.assertTrue(isinstance(self.marionette.session_id, unicode))
-
     def test_session_already_started(self):
         self.marionette.start_session()
         self.assertTrue(isinstance(self.marionette.session_id, unicode))
         with self.assertRaises(errors.SessionNotCreatedException):
             self.marionette._send_message("newSession", {})
 
     def test_no_session(self):
         with self.assertRaises(errors.InvalidSessionIdException):