Bug 751783 - Allow sandbox reuse between execute_script calls, r=jgriffin, r=mdas, DONTBUILD because NPOTB,
authorPhilipp von Weitershausen <philipp@weitershausen.de>
Wed, 09 May 2012 12:05:39 -0700
changeset 93601 b54871d38ed1ed0360cbcf2828aaf901f24fdab1
parent 93600 b5765cc9da6cea73b65f9589a981ae3a79761877
child 93602 d6b1898a8eb76b8b3e4d6445db4f2ca578a958d4
push id22650
push userjgriffin@mozilla.com
push dateWed, 09 May 2012 20:23:33 +0000
treeherdermozilla-central@b54871d38ed1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin, mdas, DONTBUILD
bugs751783
milestone15.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 751783 - Allow sandbox reuse between execute_script calls, r=jgriffin, r=mdas, DONTBUILD because NPOTB,
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
testing/marionette/client/marionette/tests/unit/test_execute_script.py
testing/marionette/marionette-actors.js
testing/marionette/marionette-listener.js
testing/marionette/marionette-simpletest.js
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -308,39 +308,48 @@ class Marionette(object):
                     unwrapped = HTMLElement(self, value[key])
                 else:
                     unwrapped[key] = self.unwrapValue(value[key])
         else:
             unwrapped = value
 
         return unwrapped
 
-    def execute_js_script(self, script, script_args=None, timeout=True):
+    def execute_js_script(self, script, script_args=None, timeout=True, new_sandbox=True):
         if script_args is None:
             script_args = []
         args = self.wrapArguments(script_args)
         response = self._send_message('executeJSScript',
                                       'value',
                                       value=script,
                                       args=args,
-                                      timeout=timeout)
+                                      timeout=timeout,
+                                      newSandbox=new_sandbox)
         return self.unwrapValue(response)
 
-    def execute_script(self, script, script_args=None):
+    def execute_script(self, script, script_args=None, new_sandbox=True):
         if script_args is None:
             script_args = []
         args = self.wrapArguments(script_args)
-        response = self._send_message('executeScript', 'value', value=script, args=args)
+        response = self._send_message('executeScript',
+                                     'value',
+                                      value=script,
+                                      args=args,
+                                      newSandbox=new_sandbox)
         return self.unwrapValue(response)
 
-    def execute_async_script(self, script, script_args=None):
+    def execute_async_script(self, script, script_args=None, new_sandbox=True):
         if script_args is None:
             script_args = []
         args = self.wrapArguments(script_args)
-        response = self._send_message('executeAsyncScript', 'value', value=script, args=args)
+        response = self._send_message('executeAsyncScript',
+                                      'value',
+                                      value=script,
+                                      args=args,
+                                      newSandbox=new_sandbox)
         return self.unwrapValue(response)
 
     def find_element(self, method, target, id=None):
         kwargs = { 'value': target, 'using': method }
         if id:
             kwargs['element'] = id
         response = self._send_message('findElement', 'value', **kwargs)
         element = HTMLElement(self, response)
--- a/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
@@ -66,21 +66,21 @@ class TestExecuteAsyncContent(Marionette
         self.assertRaises(JavascriptException, self.marionette.execute_async_script, unload)
 
     def test_check_window(self):
         self.assertTrue(self.marionette.execute_async_script("marionetteScriptFinished(window !=null && window != undefined);"))
 
     def test_same_context(self):
         var1 = 'testing'
         self.assertEqual(self.marionette.execute_script("""
-            window.wrappedJSObject._testvar = '%s';
-            return window.wrappedJSObject._testvar;
+            this.testvar = '%s';
+            return this.testvar;
             """ % var1), var1)
         self.assertEqual(self.marionette.execute_async_script(
-            "marionetteScriptFinished(window.wrappedJSObject._testvar);"), var1)
+            "marionetteScriptFinished(this.testvar);", new_sandbox=False), var1)
 
     def test_execute_no_return(self):
         self.assertEqual(self.marionette.execute_async_script("marionetteScriptFinished()"), None)
 
     def test_execute_js_exception(self):
         self.assertRaises(JavascriptException,
             self.marionette.execute_async_script, "foo(bar);")
 
@@ -97,16 +97,29 @@ class TestExecuteAsyncContent(Marionette
             """))
 
     def test_execute_permission(self):
         self.assertRaises(JavascriptException, self.marionette.execute_async_script, """
 var c = Components.classes;
 marionetteScriptFinished(1);
 """)
 
+    def test_sandbox_reuse(self):
+        # Sandboxes between `execute_script()` invocations are shared.
+        self.marionette.execute_async_script("this.foobar = [23, 42];"
+                                             "marionetteScriptFinished();")
+        self.assertEqual(self.marionette.execute_async_script(
+            "marionetteScriptFinished(this.foobar);", new_sandbox=False), [23, 42])
+
+        self.marionette.execute_async_script("global.barfoo = [42, 23];"
+                                             "marionetteScriptFinished();")
+        self.assertEqual(self.marionette.execute_async_script(
+            "marionetteScriptFinished(global.barfoo);", new_sandbox=False), [42, 23])
+
+
 class TestExecuteAsyncChrome(TestExecuteAsyncContent):
     def setUp(self):
         super(TestExecuteAsyncChrome, self).setUp()
         self.marionette.set_context("chrome")
 
     def test_execute_async_unload(self):
         pass
 
@@ -114,8 +127,11 @@ class TestExecuteAsyncChrome(TestExecute
         pass
 
     def test_execute_permission(self):
         self.assertEqual(1, self.marionette.execute_async_script("""
 var c = Components.classes;
 marionetteScriptFinished(1);
 """))
 
+    def test_sandbox_reuse(self):
+        pass
+
--- a/testing/marionette/client/marionette/tests/unit/test_execute_script.py
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_script.py
@@ -59,17 +59,26 @@ class TestExecuteContent(MarionetteTestC
         self.assertEqual(self.marionette.execute_script("return [1, 2];"), [1, 2])
         self.assertEqual(self.marionette.execute_script("return {'foo': 'bar', 'fizz': 'fazz'};"),
                          {'foo': 'bar', 'fizz': 'fazz'})
         self.assertEqual(self.marionette.execute_script("return [1, {'foo': 'bar'}, 2];"),
                          [1, {'foo': 'bar'}, 2])
         self.assertEqual(self.marionette.execute_script("return {'foo': [1, 'a', 2]};"),
                          {'foo': [1, 'a', 2]})
 
+    def test_sandbox_reuse(self):
+        # Sandboxes between `execute_script()` invocations are shared.
+        self.marionette.execute_script("this.foobar = [23, 42];")
+        self.assertEqual(self.marionette.execute_script("return this.foobar;", new_sandbox=False), [23, 42])
+
+        self.marionette.execute_script("global.barfoo = [42, 23];")
+        self.assertEqual(self.marionette.execute_script("return global.barfoo;", new_sandbox=False), [42, 23])
 
 class TestExecuteChrome(TestExecuteContent):
     def setUp(self):
         super(TestExecuteChrome, self).setUp()
         self.marionette.set_context("chrome")
 
     def test_execute_permission(self):
         self.assertEqual(1, self.marionette.execute_script("var c = Components.classes;return 1;"))
 
+    def test_sandbox_reuse(self):
+        pass
--- a/testing/marionette/marionette-actors.js
+++ b/testing/marionette/marionette-actors.js
@@ -467,18 +467,26 @@ MarionetteDriverActor.prototype = {
    * @param object aRequest
    *        'value' member is the script to run
    *        'args' member holds the arguments to the script
    * @param boolean directInject
    *        if true, it will be run directly and not as a 
    *        function body
    */
   execute: function MDA_execute(aRequest, directInject) {
+    logger.info("newSandbox: " + aRequest.newSandbox);
+    if (aRequest.newSandbox == undefined) {
+      //if client does not send a value in newSandbox, 
+      //then they expect the same behaviour as webdriver
+      aRequest.newSandbox = true;
+    }
     if (this.context == "content") {
-      this.sendAsync("executeScript", {value: aRequest.value, args: aRequest.args});
+      this.sendAsync("executeScript", {value: aRequest.value,
+                                       args: aRequest.args,
+                                       newSandbox:aRequest.newSandbox});
       return;
     }
 
     let curWindow = this.getCurrentWindow();
     let marionette = new Marionette(false, curWindow, "chrome", this.marionetteLog);
     let _chromeSandbox = this.createExecuteSandbox(curWindow, marionette, aRequest.args);
     if (!_chromeSandbox)
       return;
@@ -528,16 +536,21 @@ MarionetteDriverActor.prototype = {
    *
    * @param object aRequest
    *        'value' member holds the script to execute
    *        'args' member holds the arguments to the script
    *        'timeout' member will be used as the script timeout if it is given
    */
   executeJSScript: function MDA_executeJSScript(aRequest) {
     //all pure JS scripts will need to call Marionette.finish() to complete the test.
+    if (aRequest.newSandbox == undefined) {
+      //if client does not send a value in newSandbox, 
+      //then they expect the same behaviour as webdriver
+      aRequest.newSandbox = true;
+    }
     if (this.context == "chrome") {
       if (aRequest.timeout) {
         this.executeWithCallback(aRequest, aRequest.timeout);
       }
       else {
         this.execute(aRequest, true);
       }
     }
@@ -557,22 +570,28 @@ MarionetteDriverActor.prototype = {
    * @param object aRequest
    *        'value' member holds the script to execute
    *        'args' member holds the arguments for the script
    * @param boolean directInject
    *        if true, it will be run directly and not as a 
    *        function body
    */
   executeWithCallback: function MDA_executeWithCallback(aRequest, directInject) {
+    if (aRequest.newSandbox == undefined) {
+      //if client does not send a value in newSandbox, 
+      //then they expect the same behaviour as webdriver
+      aRequest.newSandbox = true;
+    }
     this.command_id = this.uuidGen.generateUUID().toString();
 
     if (this.context == "content") {
       this.sendAsync("executeAsyncScript", {value: aRequest.value,
                                             args: aRequest.args,
-                                            id: this.command_id});
+                                            id: this.command_id,
+                                            newSandbox: aRequest.newSandbox});
       return;
     }
 
     let curWindow = this.getCurrentWindow();
     let original_onerror = curWindow.onerror;
     let that = this;
     let marionette = new Marionette(true, curWindow, "chrome", this.marionetteLog);
     marionette.command_id = this.command_id;
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -24,16 +24,25 @@ let isB2G = false;
 
 let marionetteTimeout = null;
 let winUtil = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
 let listenerId = null; //unique ID of this listener
 let activeFrame = null;
 let curWindow = content;
 let elementManager = new ElementManager([]);
 
+// The sandbox we execute test scripts in. Gets lazily created in
+// createExecuteContentSandbox().
+let sandbox;
+
+// Flag to indicate whether an async script is currently running or not.
+let asyncTestRunning = false;
+let asyncTestCommandId;
+let asyncTestTimeoutId;
+
 /**
  * Called when listener is first started up. 
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
   let register = sendSyncMessage("Marionette:register", {value: winUtil.outerWindowID, href: content.location.href});
   
@@ -173,16 +182,17 @@ function sendError(message, status, trac
   let error_msg = { message: message, status: status, stacktrace: trace };
   sendToServer("Marionette:error", error_msg, command_id);
 }
 
 /**
  * Clear test values after completion of test
  */
 function resetValues() {
+  sandbox = null;
   marionetteTimeout = null;
   curWin = content;
 }
 
 /**
  * send error when we detect an unload event during async scripts
  */
 function errUnload() {
@@ -192,70 +202,104 @@ function errUnload() {
 
 /*
  * Marionette Methods
  */
 
 /**
  * Returns a content sandbox that can be used by the execute_foo functions.
  */
-function createExecuteContentSandbox(aWindow, marionette, args) {
-  try {
-    args = elementManager.convertWrappedArguments(args, aWindow);
-  }
-  catch(e) {
-    sendError(e.message, e.num, e.stack);
-    return;
-  }
-
+function createExecuteContentSandbox(aWindow) {
   let sandbox = new Cu.Sandbox(aWindow);
+  sandbox.global = sandbox;
   sandbox.window = aWindow;
   sandbox.document = sandbox.window.document;
   sandbox.navigator = sandbox.window.navigator;
-  sandbox.__namedArgs = elementManager.applyNamedArgs(args);
-  sandbox.__marionetteParams = args;
   sandbox.__proto__ = sandbox.window;
   sandbox.testUtils = utils;
 
+  let marionette = new Marionette(false, aWindow, "content", marionetteLogObj);
+  sandbox.marionette = marionette;
   marionette.exports.forEach(function(fn) {
     sandbox[fn] = marionette[fn].bind(marionette);
   });
 
+  sandbox.asyncComplete = function sandbox_asyncComplete(value, status) {
+    curWindow.removeEventListener("unload", errUnload, false);
+
+    /* clear all timeouts potentially generated by the script*/
+    for (let i = 0; i <= asyncTestTimeoutId; i++) {
+      curWindow.clearTimeout(i);
+    }
+
+    sendSyncMessage("Marionette:testLog",
+                    {value: elementManager.wrapValue(marionetteLogObj.getLogs())});
+    marionetteLogObj.clearLogs();
+    if (status == 0){
+      sendResponse({value: elementManager.wrapValue(value), status: status}, asyncTestCommandId);
+    }
+    else {
+      sendError(value, status, null, asyncTestCommandId);
+    }
+
+    asyncTestRunning = false;
+    asyncTestTimeoutId = undefined;
+    asyncTestCommandId = undefined;
+  };
+  sandbox.finish = function sandbox_finish() {
+    if (asyncTestRunning) {
+      sandbox.asyncComplete(marionette.generate_results(), 0);
+    } else {
+      return marionette.generate_results();
+    }
+  };
+  sandbox.marionetteScriptFinished = function sandbox_marionetteScriptFinished(value) {
+    return sandbox.asyncComplete(value, 0);
+  };
+
   return sandbox;
 }
 
 /**
  * Execute the given script either as a function body (executeScript)
  * or directly (for 'mochitest' like JS Marionette tests)
  */
 function executeScript(msg, directInject) {
   let script = msg.json.value;
-  let marionette = new Marionette(false, curWindow, "content", marionetteLogObj);
 
-  let sandbox = createExecuteContentSandbox(curWindow, marionette, msg.json.args);
-  if (!sandbox)
-    return;
-
-  sandbox.finish = function sandbox_finish() {
-    return marionette.generate_results();
-  };
+  if (msg.json.newSandbox || !sandbox) {
+    sandbox = createExecuteContentSandbox(curWindow);
+    if (!sandbox) {
+      sendError("Could not create sandbox!");
+      return;
+    }
+  }
 
   try {
     if (directInject) {
       let res = Cu.evalInSandbox(script, sandbox, "1.8");
       sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())});
       marionetteLogObj.clearLogs();
       if (res == undefined || res.passed == undefined) {
         sendError("Marionette.finish() not called", 17, null);
       }
       else {
         sendResponse({value: elementManager.wrapValue(res)});
       }
     }
     else {
+      try {
+        sandbox.__marionetteParams = elementManager.convertWrappedArguments(
+          msg.json.args, curWindow);
+      }
+      catch(e) {
+        sendError(e.message, e.num, e.stack);
+        return;
+      }
+
       let scriptSrc = "let __marionetteFunc = function(){" + script + "};" +
                       "__marionetteFunc.apply(null, __marionetteParams);";
       let res = Cu.evalInSandbox(scriptSrc, sandbox, "1.8");
       sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())});
       marionetteLogObj.clearLogs();
       sendResponse({value: elementManager.wrapValue(res)});
     }
   }
@@ -297,77 +341,65 @@ function executeJSScript(msg) {
  * 
  * For executeJSScript, it will return a message only when the finish() method is called.
  * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1] 
  * method is called, or if it times out.
  */
 function executeWithCallback(msg, timeout) {
   curWindow.addEventListener("unload", errUnload, false);
   let script = msg.json.value;
-  let command_id = msg.json.id;
+  asyncTestCommandId = msg.json.id;
 
   // Error code 28 is scriptTimeout, but spec says execute_async should return 21 (Timeout),
   // see http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/execute_async.
   // However Selenium code returns 28, see
   // http://code.google.com/p/selenium/source/browse/trunk/javascript/firefox-driver/js/evaluate.js.
   // We'll stay compatible with the Selenium code.
-  let timeoutId = curWindow.setTimeout(function() {
-    contentAsyncReturnFunc('timed out', 28);
+  asyncTestTimeoutId = curWindow.setTimeout(function() {
+    sandbox.asyncComplete('timed out', 28);
   }, marionetteTimeout);
   curWindow.addEventListener('error', function win__onerror(evt) {
     curWindow.removeEventListener('error', win__onerror, true);
-    contentAsyncReturnFunc(evt, 17);
+    sandbox.asyncComplete(evt, 17);
     return true;
   }, true);
 
-  function contentAsyncReturnFunc(value, status) {
-    curWindow.removeEventListener("unload", errUnload, false);
-
-    /* clear all timeouts potentially generated by the script*/
-    for(let i=0; i<=timeoutId; i++) {
-      curWindow.clearTimeout(i);
+  if (msg.json.newSandbox || !sandbox) {
+    sandbox = createExecuteContentSandbox(curWindow);
+    if (!sandbox) {
+      sendError("Could not create sandbox!");
+      return;
     }
-
-    sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())});
-    marionetteLogObj.clearLogs();
-    if (status == 0){
-      sendResponse({value: elementManager.wrapValue(value), status: status}, command_id);
-    }
-    else {
-      sendError(value, status, null, command_id);
-    }
-  };
+  }
 
   let scriptSrc;
   if (timeout) {
     if (marionetteTimeout == null || marionetteTimeout == 0) {
       sendError("Please set a timeout", 21, null);
     }
     scriptSrc = script;
   }
   else {
-    scriptSrc = "let marionetteScriptFinished = function(value) { return asyncComplete(value,0);};" +
-                "__marionetteParams.push(marionetteScriptFinished);" +
+    try {
+      sandbox.__marionetteParams = elementManager.convertWrappedArguments(
+        msg.json.args, curWindow);
+    }
+    catch(e) {
+      sendError(e.message, e.num, e.stack);
+      return;
+    }
+
+    scriptSrc = "__marionetteParams.push(marionetteScriptFinished);" +
                 "let __marionetteFunc = function() { " + script + "};" +
                 "__marionetteFunc.apply(null, __marionetteParams); ";
   }
 
-  let marionette = new Marionette(true, curWindow, "content", marionetteLogObj);
-
-  let sandbox = createExecuteContentSandbox(curWindow, marionette, msg.json.args);
-  if (!sandbox)
-    return;
-
-  sandbox.asyncComplete = contentAsyncReturnFunc;
-  sandbox.finish = function sandbox_finish() {
-    contentAsyncReturnFunc(marionette.generate_results(), 0);
-  };
-
   try {
-   Cu.evalInSandbox(scriptSrc, sandbox, "1.8");
+    asyncTestRunning = true;
+    Cu.evalInSandbox(scriptSrc, sandbox, "1.8");
   } catch (e) {
     // 17 = JavascriptException
     sendError(e.name + ': ' + e.message, 17, e.stack);
   }
 }
 
 /**
  * Function to set the timeout period for element searching 
@@ -616,19 +648,21 @@ function switchToFrame(msg) {
       }
       break;
     case "number":
       if (curWindow.frames[msg.json.value] != undefined) {
         foundFrame = msg.json.value;
       }
       break;
   }
-  if (foundFrame != null) {
-    curWindow = curWindow.frames[foundFrame];
-    curWindow.focus();
-    sendOk();
-  } else {
+  if (foundFrame == null) {
     sendError("Unable to locate frame: " + msg.json.value, 8, null);
+    return;
   }
+  curWindow = curWindow.frames[foundFrame];
+  curWindow.focus();
+  sendOk();
+
+  sandbox = null;
 }
 
 //call register self when we get loaded
 registerSelf();
--- a/testing/marionette/marionette-simpletest.js
+++ b/testing/marionette/marionette-simpletest.js
@@ -6,20 +6,21 @@
  */
 
 function Marionette(is_async, window, context, logObj) {
   this.is_async = is_async;
   this.window = window;
   this.tests = [];
   this.logObj = logObj;
   this.context = context;
-  this.exports = ['ok', 'is', 'isnot', 'log', 'getLogs', 'generate_results', 'waitFor'];
 }
 
 Marionette.prototype = {
+  exports: ['ok', 'is', 'isnot', 'log', 'getLogs', 'generate_results', 'waitFor'],
+
   ok: function Marionette__ok(condition, name, diag) {
     let test = {'result': !!condition, 'name': name, 'diag': diag};
     this.logResult(test, "TEST-PASS", "TEST-UNEXPECTED-FAIL");
     this.tests.push(test);
   },
 
   is: function Marionette__is(a, b, name) {
     let pass = (a == b);
@@ -57,16 +58,18 @@ Marionette.prototype = {
         passed++;
       }
       else {
         failed++;
         failures.push({'name': this.tests[i].name,
                        'diag': this.tests[i].diag});
       }
     }
+    // Reset state in case this object is reused for more tests.
+    this.tests = [];
     return {"passed": passed, "failed": failed, "failures": failures};
   },
 
   logToFile: function Marionette__logToFile(file) {
     //TODO
   },
 
   logResult: function Marionette__logResult(test, passString, failString) {