Bug 1251701 - Store imported scripts in memory; r=jgriffin
authorAndreas Tolfsen <ato@mozilla.com>
Mon, 29 Feb 2016 18:52:30 +0000
changeset 286669 16dbf8d33829bfc142d77c414c3852b8ecd9c1ba
parent 286668 2ab0db7c01f8e9aed7cc90100873794992f87500
child 286670 5e135136e21ca2ea16b60d2ea6f76fabeaa44b7a
push id18000
push usercbook@mozilla.com
push dateFri, 04 Mar 2016 12:40:23 +0000
treeherderfx-team@365dff9e6e1f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin
bugs1251701, 1251351
milestone47.0a1
Bug 1251701 - Store imported scripts in memory; r=jgriffin This bug is also a dependency for scheduling Mn-e10s tests on Windows 7 (bug 1251351). MozReview-Commit-ID: 2jE4C99d1MX
testing/marionette/driver.js
testing/marionette/evaluate.js
testing/marionette/frame.js
testing/marionette/harness/marionette/marionette_test.py
testing/marionette/harness/marionette/tests/unit/test_import_script.py
testing/marionette/harness/marionette/tests/unit/test_import_script_reuse_window.py
testing/marionette/harness/marionette/tests/unit/unit-tests.ini
testing/marionette/jar.mn
testing/marionette/listener.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -6,35 +6,35 @@
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
     .getService(Ci.mozIJSSubScriptLoader);
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 this.DevToolsUtils = devtools.require("devtools/shared/DevToolsUtils");
 
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
-Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/frame.js");
-Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
 loader.loadSubScript("chrome://marionette/content/common.js");
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
@@ -116,20 +116,17 @@ this.GeckoDriver = function(appName, dev
   // called by simpletest methods
   this.heartbeatCallback = function() {};
   this.marionetteLog = new MarionetteLogObj();
   // topmost chrome frame
   this.mainFrame = null;
   // chrome iframe that currently has focus
   this.curFrame = null;
   this.mainContentFrameId = null;
-  this.importedScripts = FileUtils.getFile("TmpD", ["marionetteChromeScripts"]);
-  this.importedScriptHashes = {};
-  this.importedScriptHashes[Context.CONTENT] = [];
-  this.importedScriptHashes[Context.CHROME] = [];
+  this.importedScripts = new evaluate.ScriptStorageService([Context.CHROME, Context.CONTENT]);
   this.currentFrameElement = null;
   this.testName = null;
   this.mozBrowserClose = null;
   this.sandboxes = {};
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
@@ -831,25 +828,17 @@ GeckoDriver.prototype.executeScriptInSan
     directInject,
     async,
     timeout,
     filename) {
   if (directInject && async && (timeout === null || timeout === 0)) {
     throw new TimeoutError("Please set a timeout");
   }
 
-  if (this.importedScripts.exists()) {
-    let stream = Cc["@mozilla.org/network/file-input-stream;1"]
-        .createInstance(Ci.nsIFileInputStream);
-    stream.init(this.importedScripts, -1, 0, 0);
-    let data = NetUtil.readInputStreamToString(stream, stream.available());
-    stream.close();
-    script = data + script;
-  }
-
+  script = this.importedScripts.for(Context.CHROME).concat(script);
   let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file", 0);
 
   if (directInject && !async &&
       (typeof res == "undefined" || typeof res.passed == "undefined")) {
     throw new WebDriverError("finish() not called");
   }
 
   if (!async) {
@@ -2494,68 +2483,41 @@ GeckoDriver.prototype.deleteSession = fu
   this.sessionTearDown();
 };
 
 /** Returns the current status of the Application Cache. */
 GeckoDriver.prototype.getAppCacheStatus = function*(cmd, resp) {
   resp.body.value = yield this.listener.getAppCacheStatus();
 };
 
+/**
+ * Import script to the JS evaluation runtime.
+ *
+ * Imported scripts are exposed in the contexts of all subsequent
+ * calls to {@code executeScript}, {@code executeAsyncScript}, and
+ * {@code executeJSScript} by prepending them to the evaluated script.
+ *
+ * Scripts can be cleared with the {@code clearImportedScripts} command.
+ *
+ * @param {string} script
+ *     Script to include.  If the script is byte-by-byte equal to an
+ *     existing imported script, it is not imported.
+ */
 GeckoDriver.prototype.importScript = function*(cmd, resp) {
   let script = cmd.parameters.script;
-
-  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
-      .createInstance(Ci.nsIScriptableUnicodeConverter);
-  converter.charset = "UTF-8";
-  let result = {};
-  let data = converter.convertToByteArray(cmd.parameters.script, result);
-  let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
-  ch.init(ch.MD5);
-  ch.update(data, data.length);
-  let hash = ch.finish(true);
-  // return if we've already imported this script
-  if (this.importedScriptHashes[this.context].indexOf(hash) > -1) {
-    return;
-  }
-  this.importedScriptHashes[this.context].push(hash);
-
-  switch (this.context) {
-    case Context.CHROME:
-      let file;
-      if (this.importedScripts.exists()) {
-        file = FileUtils.openFileOutputStream(this.importedScripts,
-            FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY);
-      } else {
-        // the permission bits here don't actually get set (bug 804563)
-        this.importedScripts.createUnique(
-            Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
-        file = FileUtils.openFileOutputStream(this.importedScripts,
-            FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
-        this.importedScripts.permissions = parseInt("0666", 8);
-      }
-      file.write(script, script.length);
-      file.close();
-      break;
-
-    case Context.CONTENT:
-      yield this.listener.importScript({script: script});
-      break;
-  }
+  this.importedScripts.for(this.context).add(script);
 };
 
-GeckoDriver.prototype.clearImportedScripts = function(cmd, resp) {
-  switch (this.context) {
-    case Context.CHROME:
-      this.deleteFile("marionetteChromeScripts");
-      break;
-
-    case Context.CONTENT:
-      this.deleteFile("marionetteContentScripts");
-      break;
-  }
+/**
+ * Clear all scripts that are imported into the JS evaluation runtime.
+ *
+ * Scripts can be imported using the {@code importScript} command.
+ */
+GeckoDriver.prototype.clearImportedScripts = function*(cmd, resp) {
+  this.importedScripts.for(this.context).clear();
 };
 
 /**
  * Takes a screenshot of a web element, current frame, or viewport.
  *
  * The screen capture is returned as a lossless PNG image encoded as
  * a base 64 string.
  *
new file mode 100644
--- /dev/null
+++ b/testing/marionette/evaluate.js
@@ -0,0 +1,179 @@
+/* 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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const logger = Log.repository.getLogger("Marionette");
+
+this.EXPORTED_SYMBOLS = ["evaluate"];
+
+this.evaluate = {};
+
+/**
+ * Stores scripts imported from the local end through the
+ * {@code GeckoDriver#importScript} command.
+ *
+ * Imported scripts are prepended to the script that is evaluated
+ * on each call to {@code GeckoDriver#executeScript},
+ * {@code GeckoDriver#executeAsyncScript}, and
+ * {@code GeckoDriver#executeJSScript}.
+ *
+ * Usage:
+ *
+ *     let importedScripts = new evaluate.ScriptStorage();
+ *     importedScripts.add(firstScript);
+ *     importedScripts.add(secondScript);
+ *
+ *     let scriptToEval = importedScripts.concat(script);
+ *     // firstScript and secondScript are prepended to script
+ *
+ */
+evaluate.ScriptStorage = class extends Set {
+
+  /**
+   * Produce a string of all stored scripts.
+   *
+   * The stored scripts are concatenated into a string, with optional
+   * additional scripts then appended.
+   *
+   * @param {...string} addional
+   *     Optional scripts to include.
+   *
+   * @return {string}
+   *     Concatenated string consisting of stored scripts and additional
+   *     scripts, in that order.
+   */
+  concat(...additional) {
+    let rv = "";
+    for (let s of this) {
+      rv = s + rv;
+    }
+    for (let s of additional) {
+      rv = rv + s;
+    }
+    return rv;
+  }
+
+  toJson() {
+    return Array.from(this);
+  }
+
+};
+
+/**
+ * Service that enables the script storage service to be queried from
+ * content space.
+ *
+ * The storage can back multiple |ScriptStorage|, each typically belonging
+ * to a |Context|.  Since imported scripts' scope are global and not
+ * scoped to the current browsing context, all imported scripts are stored
+ * in chrome space and fetched by content space as needed.
+ *
+ * Usage in chrome space:
+ *
+ *     let service = new evaluate.ScriptStorageService(
+ *         [Context.CHROME, Context.CONTENT]);
+ *     let storage = service.for(Context.CHROME);
+ *     let scriptToEval = storage.concat(script);
+ *
+ */
+evaluate.ScriptStorageService = class extends Map {
+
+  /**
+   * Create the service.
+   *
+   * An optional array of names for script storages to initially create
+   * can be provided.
+   *
+   * @param {Array.<string>=} initialStorages
+   *     List of names of the script storages to create initially.
+   */
+  constructor(initialStorages = []) {
+    super(initialStorages.map(name => [name, new evaluate.ScriptStorage()]));
+  }
+
+  /**
+   * Retrieve the scripts associated with the given context.
+   *
+   * @param {Context} context
+   *     Context to retrieve the scripts from.
+   *
+   * @return {ScriptStorage}
+   *     Scrips associated with given |context|.
+   */
+  for(context) {
+    return this.get(context);
+  }
+
+  processMessage(msg) {
+    switch (msg.name) {
+      case "Marionette:getImportedScripts":
+        let storage = this.for.apply(this, msg.json);
+        return storage.toJson();
+
+      default:
+        throw new TypeError("Unknown message: " + msg.name);
+    }
+  }
+
+  // TODO(ato): The idea of services in chrome space
+  // can be generalised at some later time (see cookies.js:38).
+  receiveMessage(msg) {
+    try {
+      return this.processMessage(msg);
+    } catch (e) {
+      logger.error(e);
+    }
+  }
+
+};
+
+evaluate.ScriptStorageService.prototype.QueryInterface =
+    XPCOMUtils.generateQI([
+      Ci.nsIMessageListener,
+      Ci.nsISupportsWeakReference,
+    ]);
+
+/**
+ * Bridges the script storage in chrome space, to make it possible to
+ * retrieve a {@code ScriptStorage} associated with a given
+ * {@code Context} from content space.
+ *
+ * Usage in content space:
+ *
+ *     let client = new evaluate.ScriptStorageServiceClient(chromeProxy);
+ *     let storage = client.for(Context.CONTENT);
+ *     let scriptToEval = storage.concat(script);
+ *
+ */
+evaluate.ScriptStorageServiceClient = class {
+
+  /**
+   * @param {proxy.SyncChromeSender} chromeProxy
+   *     Proxy for communicating with chrome space.
+   */
+  constructor(chromeProxy) {
+    this.chrome = chromeProxy;
+  }
+
+  /**
+   * Retrieve scripts associated with the given context.
+   *
+   * @param {Context} context
+   *     Context to retrieve scripts from.
+   *
+   * @return {ScriptStorage}
+   *     Scripts associated with given |context|.
+   */
+  for(context) {
+    let scripts = this.chrome.getImportedScripts(context)[0];
+    return new evaluate.ScriptStorage(scripts);
+  }
+
+};
--- a/testing/marionette/frame.js
+++ b/testing/marionette/frame.js
@@ -217,16 +217,17 @@ frame.Manager = class {
     mm.addWeakMessageListener("Marionette:emitTouchEvent", this.driver);
     mm.addWeakMessageListener("Marionette:log", this.driver);
     mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.driver.emulator);
     mm.addWeakMessageListener("Marionette:runEmulatorShell", this.driver.emulator);
     mm.addWeakMessageListener("Marionette:shareData", this.driver);
     mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.driver);
     mm.addWeakMessageListener("Marionette:switchedToFrame", this.driver);
     mm.addWeakMessageListener("Marionette:getVisibleCookies", this.driver);
+    mm.addWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts);
     mm.addWeakMessageListener("Marionette:register", this.driver);
     mm.addWeakMessageListener("Marionette:listenersAttached", this.driver);
     mm.addWeakMessageListener("Marionette:getFiles", this.driver);
     mm.addWeakMessageListener("MarionetteFrame:handleModal", this);
     mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
     mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
   }
 
@@ -247,16 +248,17 @@ frame.Manager = class {
     mm.removeWeakMessageListener("Marionette:done", this.driver);
     mm.removeWeakMessageListener("Marionette:error", this.driver);
     mm.removeWeakMessageListener("Marionette:log", this.driver);
     mm.removeWeakMessageListener("Marionette:shareData", this.driver);
     mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.driver.emulator);
     mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.driver.emulator);
     mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver);
     mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver);
+    mm.removeWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts);
     mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver);
     mm.removeWeakMessageListener("Marionette:register", this.driver);
     mm.removeWeakMessageListener("Marionette:getFiles", this.driver);
     mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);
     mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
   }
 };
 
--- a/testing/marionette/harness/marionette/marionette_test.py
+++ b/testing/marionette/harness/marionette/marionette_test.py
@@ -74,16 +74,23 @@ def expectedFailure(func):
     def wrapper(*args, **kwargs):
         try:
             func(*args, **kwargs)
         except Exception:
             raise _ExpectedFailure(sys.exc_info())
         raise _UnexpectedSuccess
     return wrapper
 
+def skip_if_chrome(target):
+    def wrapper(self, *args, **kwargs):
+        if self.marionette._send_message("getContext", key="value") == "chrome":
+            raise SkipTest("skipping test in chrome context")
+        return target(self, *args, **kwargs)
+    return wrapper
+
 def skip_if_desktop(target):
     def wrapper(self, *args, **kwargs):
         if self.marionette.session_capabilities.get('b2g') is None:
             raise SkipTest('skipping due to desktop')
         return target(self, *args, **kwargs)
     return wrapper
 
 def skip_if_b2g(target):
--- a/testing/marionette/harness/marionette/tests/unit/test_import_script.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_import_script.py
@@ -1,142 +1,125 @@
 # 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 os
-from marionette import MarionetteTestCase
+
+from marionette.marionette_test import MarionetteTestCase, skip_if_chrome
 from marionette_driver.errors import JavascriptException
 
-class TestImportScript(MarionetteTestCase):
+class TestImportScriptContent(MarionetteTestCase):
+    contexts = set(["chrome", "content"])
+
+    script_file = os.path.abspath(
+        os.path.join(__file__, os.path.pardir, "importscript.js"))
+    another_script_file = os.path.abspath(
+        os.path.join(__file__, os.path.pardir, "importanotherscript.js"))
+
     def setUp(self):
         MarionetteTestCase.setUp(self)
+        for context in self.contexts:
+            with self.marionette.using_context(context):
+                self.marionette.clear_imported_scripts()
+        self.reset_context()
 
-    def clear_other_context(self):
-        self.marionette.set_context("chrome")
-        self.marionette.clear_imported_scripts()
+    def reset_context(self):
         self.marionette.set_context("content")
 
-    def check_file_exists(self):
-        return self.marionette.execute_script("""
-          let FileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
-          let importedScripts = FileUtils.getFile('TmpD', ['marionetteContentScripts']);
-          return importedScripts.exists();
-        """, sandbox='system')
+    @property
+    def current_context(self):
+        return self.marionette._send_message("getContext", key="value")
+
+    @property
+    def other_context(self):
+        return self.contexts.copy().difference([self.current_context]).pop()
+
+    def is_defined(self, symbol):
+        return self.marionette.execute_script(
+            "return typeof %s != 'undefined'" % symbol)
 
-    def get_file_size(self):
-        return self.marionette.execute_script("""
-          let FileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
-          let importedScripts = FileUtils.getFile('TmpD', ['marionetteContentScripts']);
-          return importedScripts.fileSize;
-        """, sandbox='system')
+    def assert_defined(self, symbol, msg=None):
+        if msg is None:
+            msg = "Expected symbol %s to be defined" % symbol
+        self.assertTrue(self.is_defined(symbol), msg)
+
+    def assert_undefined(self, symbol, msg=None):
+        if msg is None:
+            msg = "Expected symbol %s to be undefined" % symbol
+        self.assertFalse(self.is_defined(symbol), msg)
+
+    def assert_scripts_cleared(self):
+        self.marionette.import_script(self.script_file)
+        self.assert_defined("testFunc")
+        self.marionette.clear_imported_scripts()
+        self.assert_undefined("testFunc")
 
     def test_import_script(self):
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.marionette.import_script(js)
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
+        self.marionette.import_script(self.script_file)
+        self.assertEqual(
+            "i'm a test function!", self.marionette.execute_script("return testFunc();"))
+        self.assertEqual("i'm a test function!", self.marionette.execute_async_script(
+            "marionetteScriptFinished(testFunc());"))
 
     def test_import_script_twice(self):
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.marionette.import_script(js)
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
-        self.assertTrue(self.check_file_exists())
-        file_size = self.get_file_size()
-        self.assertNotEqual(file_size, None)
-        self.marionette.import_script(js)
-        file_size = self.get_file_size()
-        self.assertEqual(file_size, self.get_file_size())
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
+        self.marionette.import_script(self.script_file)
+        self.assert_defined("testFunc")
 
-    def test_import_two_scripts_twice(self):
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.marionette.import_script(js)
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
-        self.assertTrue(self.check_file_exists())
-        file_size = self.get_file_size()
-        self.assertNotEqual(file_size, None)
-        self.marionette.import_script(js)
-        # same script should not append to file
-        self.assertEqual(file_size, self.get_file_size())
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importanotherscript.js"))
-        self.marionette.import_script(js)
-        new_size = self.get_file_size()
-        # new script should append to file
-        self.assertNotEqual(file_size, new_size)
-        file_size = new_size
-        self.assertEqual("i'm yet another test function!",
-                    self.marionette.execute_script("return testAnotherFunc();"))
-        self.assertEqual("i'm yet another test function!",
-                    self.marionette.execute_async_script("marionetteScriptFinished(testAnotherFunc());"))
-        self.marionette.import_script(js)
-        # same script should not append to file
-        self.assertEqual(file_size, self.get_file_size())
+        # TODO(ato): Note that the WebDriver command primitives
+        # does not allow us to check what scripts have been imported.
+        # I suspect we must to do this through an xpcshell test.
+
+        self.marionette.import_script(self.script_file)
+        self.assert_defined("testFunc")
 
     def test_import_script_and_clear(self):
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.marionette.import_script(js)
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
+        self.marionette.import_script(self.script_file)
+        self.assert_defined("testFunc")
         self.marionette.clear_imported_scripts()
-        self.assertFalse(self.check_file_exists())
-        self.assertRaises(JavascriptException, self.marionette.execute_script, "return testFunc();")
-        self.assertRaises(JavascriptException, self.marionette.execute_async_script, "marionetteScriptFinished(testFunc());")
+        self.assert_scripts_cleared()
+        self.assert_undefined("testFunc")
+        with self.assertRaises(JavascriptException):
+            self.marionette.execute_script("return testFunc()")
+        with self.assertRaises(JavascriptException):
+            self.marionette.execute_async_script(
+                "marionetteScriptFinished(testFunc())")
 
-    def test_import_script_and_clear_in_chrome(self):
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.marionette.import_script(js)
-        self.assertTrue(self.check_file_exists())
-        file_size = self.get_file_size()
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
-        self.clear_other_context()
+    def test_clear_scripts_in_other_context(self):
+        self.marionette.import_script(self.script_file)
+        self.assert_defined("testFunc")
+
         # clearing other context's script file should not affect ours
-        self.assertTrue(self.check_file_exists())
-        self.assertEqual(file_size, self.get_file_size())
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-        self.assertEqual("i'm a test function!", self.marionette.execute_async_script("marionetteScriptFinished(testFunc());"))
+        with self.marionette.using_context(self.other_context):
+            self.marionette.clear_imported_scripts()
+            self.assert_undefined("testFunc")
 
-    def test_importing_another_script_and_check_they_append(self):
-        firstjs = os.path.abspath(
-                os.path.join(__file__, os.path.pardir, "importscript.js"))
-        secondjs = os.path.abspath(
-                os.path.join(__file__, os.path.pardir, "importanotherscript.js"))
-
-        self.marionette.import_script(firstjs)
-        self.marionette.import_script(secondjs)
+        self.assert_defined("testFunc")
 
-        self.assertEqual("i'm a test function!",
-                self.marionette.execute_script("return testFunc();"))
+    def test_multiple_imports(self):
+        self.marionette.import_script(self.script_file)
+        self.marionette.import_script(self.another_script_file)
+        self.assert_defined("testFunc")
+        self.assert_defined("testAnotherFunc")
 
-        self.assertEqual("i'm yet another test function!",
-                    self.marionette.execute_script("return testAnotherFunc();"))
+    @skip_if_chrome
+    def test_imports_apply_globally(self):
+        self.marionette.navigate(
+            self.marionette.absolute_url("test_windows.html"))
+        original_window = self.marionette.current_window_handle
+        self.marionette.find_element("link text", "Open new window").click()
 
-class TestImportScriptChrome(TestImportScript):
-    def setUp(self):
-        MarionetteTestCase.setUp(self)
-        self.marionette.set_script_timeout(30000)
-        self.marionette.set_context("chrome")
+        windows = set(self.marionette.window_handles)
+        print "windows=%s" % windows
+        new_window = windows.difference([original_window]).pop()
+        self.marionette.switch_to_window(new_window)
+
+        self.marionette.import_script(self.script_file)
+        self.marionette.close()
 
-    def clear_other_context(self):
-        self.marionette.set_context("content")
-        self.marionette.clear_imported_scripts()
+        print "switching to original window: %s" % original_window
+        self.marionette.switch_to_window(original_window)
+        self.assert_defined("testFunc")
+
+
+class TestImportScriptChrome(TestImportScriptContent):
+    def reset_context(self):
         self.marionette.set_context("chrome")
-
-    def check_file_exists(self):
-        return self.marionette.execute_async_script("""
-          let FileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
-          let importedScripts = FileUtils.getFile('TmpD', ['marionetteChromeScripts']);
-          marionetteScriptFinished(importedScripts.exists());
-        """)
-
-    def get_file_size(self):
-        return self.marionette.execute_async_script("""
-          let FileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
-          let importedScripts = FileUtils.getFile('TmpD', ['marionetteChromeScripts']);
-          marionetteScriptFinished(importedScripts.fileSize);
-        """)
-
deleted file mode 100644
--- a/testing/marionette/harness/marionette/tests/unit/test_import_script_reuse_window.py
+++ /dev/null
@@ -1,28 +0,0 @@
-
-# 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 os
-from marionette import MarionetteTestCase
-
-class TestImportScriptContent(MarionetteTestCase):
-
-    def test_importing_script_then_reusing_it(self):
-        test_html = self.marionette.absolute_url("test_windows.html")
-        self.marionette.navigate(test_html)
-        js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importscript.js"))
-        self.current_window = self.marionette.current_window_handle
-        link = self.marionette.find_element("link text", "Open new window")
-        link.click()
-
-        windows = self.marionette.window_handles
-        windows.remove(self.current_window)
-        self.marionette.switch_to_window(windows[0])
-
-        self.marionette.import_script(js)
-        self.marionette.close()
-
-        self.marionette.switch_to_window(self.current_window)
-        self.assertEqual("i'm a test function!", self.marionette.execute_script("return testFunc();"))
-
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -18,18 +18,16 @@ skip = false
 
 [test_accessibility.py]
 b2g = false
 
 [test_expectedfail.py]
 expected = fail
 [test_import_script.py]
 b2g = false
-[test_import_script_reuse_window.py]
-b2g = false
 [test_click.py]
 [test_click_chrome.py]
 b2g = false
 [test_selected.py]
 [test_selected_chrome.py]
 b2g = false
 [test_getattr.py]
 [test_getattr_chrome.py]
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -19,16 +19,17 @@ marionette.jar:
   content/message.js (message.js)
   content/dispatcher.js (dispatcher.js)
   content/emulator.js (emulator.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
   content/atom.js (atom.js)
+  content/evaluate.js (evaluate.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif
 
 % content specialpowers %content/
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -14,22 +14,22 @@ loader.loadSubScript("chrome://marionett
 loader.loadSubScript("chrome://marionette/content/common.js");
 
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/capture.js");
 Cu.import("chrome://marionette/content/cookies.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
+Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/proxy.js");
-Cu.import("chrome://marionette/content/interaction.js");
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var marionetteLogObj = new MarionetteLogObj();
 
 var isB2G = false;
 
 var marionetteTestName;
@@ -41,17 +41,16 @@ var isRemoteBrowser = () => curContainer
 var previousContainer = null;
 var elementManager = new ElementManager([]);
 
 // Holds session capabilities.
 var capabilities = {};
 var interactions = new Interactions(() => capabilities);
 
 var actions = new action.Chain(checkForInterrupted);
-var importedScripts = null;
 
 // Contains the last file input element that was the target of
 // sendKeysToElement.
 var fileInputElement;
 
 // A dict of sandboxes used this session
 var sandboxes = {};
 // The name of the current sandbox
@@ -78,16 +77,17 @@ var navTimer = Cc["@mozilla.org/timer;1"
 var onDOMContentLoaded;
 // Send move events about this often
 var EVENT_INTERVAL = 30; // milliseconds
 // last touch for each fingerId
 var multiLast = {};
 
 var chrome = proxy.toChrome(sendSyncMessage.bind(this));
 var cookies = new Cookies(() => curContainer.frame.document, chrome);
+var importedScripts = new evaluate.ScriptStorageServiceClient(chrome);
 
 Cu.import("resource://gre/modules/Log.jsm");
 var logger = Log.repository.getLogger("Marionette");
 logger.debug("loaded listener.js");
 var modalHandler = function() {
   // This gets called on the system app only since it receives the mozbrowserprompt event
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
   let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
@@ -112,18 +112,16 @@ function registerSelf() {
     capabilities = register[0][2];
     isB2G = capabilities.B2G;
     listenerId = id;
     if (typeof id != "undefined") {
       // check if we're the main process
       if (register[0][1] == true) {
         addMessageListener("MarionetteMainListener:emitTouchEvent", emitTouchEventForIFrame);
       }
-      importedScripts = FileUtils.getDir('TmpD', [], false);
-      importedScripts.append('marionetteContentScripts');
       startListeners();
       let rv = {};
       if (remotenessChange) {
         rv.listenerId = id;
       }
       sendAsyncMessage("Marionette:listenersAttached", rv);
     }
   }
@@ -159,16 +157,20 @@ function emitTouchEventForIFrame(message
 }
 
 // Eventually we will not have a closure for every single command, but
 // use a generic dispatch for all listener commands.
 //
 // Perhaps one could even conceive having a separate instance of
 // CommandProcessor for the listener, because the code is mostly the same.
 function dispatch(fn) {
+  if (typeof fn != "function") {
+    throw new TypeError("Provided dispatch handler is not a function");
+  }
+
   return function(msg) {
     let id = msg.json.command_id;
 
     let req = Task.spawn(function*() {
       if (typeof msg.json == "undefined" || msg.json instanceof Array) {
         return yield fn.apply(null, msg.json);
       } else {
         return yield fn(msg.json);
@@ -265,17 +267,16 @@ function startListeners() {
   addMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement);
   addMessageListenerId("Marionette:clearElement", clearElementFn);
   addMessageListenerId("Marionette:switchToFrame", switchToFrame);
   addMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
   addMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   addMessageListenerId("Marionette:deleteSession", deleteSession);
   addMessageListenerId("Marionette:sleepSession", sleepSession);
   addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
-  addMessageListenerId("Marionette:importScript", importScript);
   addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   addMessageListenerId("Marionette:setTestName", setTestName);
   addMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
   addMessageListenerId("Marionette:addCookie", addCookieFn);
   addMessageListenerId("Marionette:getCookies", getCookiesFn);
   addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
   addMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
 }
@@ -370,17 +371,16 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement);
   removeMessageListenerId("Marionette:clearElement", clearElementFn);
   removeMessageListenerId("Marionette:switchToFrame", switchToFrame);
   removeMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
   removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   removeMessageListenerId("Marionette:deleteSession", deleteSession);
   removeMessageListenerId("Marionette:sleepSession", sleepSession);
   removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
-  removeMessageListenerId("Marionette:importScript", importScript);
   removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   removeMessageListenerId("Marionette:setTestName", setTestName);
   removeMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
   removeMessageListenerId("Marionette:addCookie", addCookieFn);
   removeMessageListenerId("Marionette:getCookies", getCookiesFn);
   removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
   removeMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
   if (isB2G) {
@@ -617,25 +617,19 @@ function executeScript(msg, directInject
   } else {
     sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
   }
 
   let sandbox = sandboxes[sandboxName];
 
   try {
     if (directInject) {
-      if (importedScripts.exists()) {
-        let stream = Components.classes["@mozilla.org/network/file-input-stream;1"].
-                      createInstance(Components.interfaces.nsIFileInputStream);
-        stream.init(importedScripts, -1, 0, 0);
-        let data = NetUtil.readInputStreamToString(stream, stream.available());
-        stream.close();
-        script = data + script;
-      }
+      script = importedScripts.for("content").concat(script);
       let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file" ,0);
+
       sendSyncMessage("Marionette:shareData",
                       {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
       marionetteLogObj.clearLogs();
 
       if (res == undefined || res.passed == undefined) {
         sendError(new JavaScriptError("Marionette.finish() not called"), asyncTestCommandId);
       }
       else {
@@ -648,24 +642,18 @@ function executeScript(msg, directInject
           msg.json.args, curContainer), sandbox, { wrapReflectors: true });
       } catch (e) {
         sendError(e, asyncTestCommandId);
         return;
       }
 
       script = "var __marionetteFunc = function(){" + script + "};" +
                    "__marionetteFunc.apply(null, __marionetteParams);";
-      if (importedScripts.exists()) {
-        let stream = Components.classes["@mozilla.org/network/file-input-stream;1"].
-                      createInstance(Components.interfaces.nsIFileInputStream);
-        stream.init(importedScripts, -1, 0, 0);
-        let data = NetUtil.readInputStreamToString(stream, stream.available());
-        stream.close();
-        script = data + script;
-      }
+      script = importedScripts.for("content").concat(script);
+
       let res = Cu.evalInSandbox(script, sandbox, "1.8", filename ? filename : "dummy file", 0);
       sendSyncMessage("Marionette:shareData",
                       {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
       marionetteLogObj.clearLogs();
       sendResponse(elementManager.wrapValue(res), asyncTestCommandId);
     }
   } catch (e) {
     let err = new JavaScriptError(
@@ -804,24 +792,17 @@ function executeWithCallback(msg, useFin
 
     scriptSrc = "__marionetteParams.push(marionetteScriptFinished);" +
                 "var __marionetteFunc = function() { " + script + "};" +
                 "__marionetteFunc.apply(null, __marionetteParams); ";
   }
 
   try {
     asyncTestRunning = true;
-    if (importedScripts.exists()) {
-      let stream = Cc["@mozilla.org/network/file-input-stream;1"].
-                      createInstance(Ci.nsIFileInputStream);
-      stream.init(importedScripts, -1, 0, 0);
-      let data = NetUtil.readInputStreamToString(stream, stream.available());
-      stream.close();
-      scriptSrc = data + scriptSrc;
-    }
+    scriptSrc = importedScripts.for("content").concat(scriptSrc);
     Cu.evalInSandbox(scriptSrc, sandbox, "1.8", filename ? filename : "dummy file", 0);
   } catch (e) {
     let err = new JavaScriptError(
         e,
         "execute_async_script",
         msg.json.filename,
         msg.json.line,
         scriptSrc);
@@ -1758,36 +1739,16 @@ function emulatorCmdResult(msg) {
   try {
     cb(result);
   } catch (e) {
     let err = new JavaScriptError(e);
     sendError(err, id);
   }
 }
 
-function importScript(msg) {
-  let command_id = msg.json.command_id;
-  let file;
-  if (importedScripts.exists()) {
-    file = FileUtils.openFileOutputStream(importedScripts,
-        FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY);
-  }
-  else {
-    //Note: The permission bits here don't actually get set (bug 804563)
-    importedScripts.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE,
-                                 parseInt("0666", 8));
-    file = FileUtils.openFileOutputStream(importedScripts,
-                                          FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
-    importedScripts.permissions = parseInt("0666", 8); //actually set permissions
-  }
-  file.write(msg.json.script, msg.json.script.length);
-  file.close();
-  sendOk(command_id);
-}
-
 /**
  * Perform a screen capture in content context.
  *
  * @param {UUID=} id
  *     Optional web element reference of an element to take a screenshot
  *     of.
  * @param {boolean=} full
  *     True to take a screenshot of the entire document element.  Is not