Bug 1139254 - Introduce a new jsm to register mock easily. r=gps
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Tue, 24 Mar 2015 19:15:00 -0400
changeset 266476 d2f70dc09ad02ff78a63dadfbcc6365225f3b5f5
parent 266475 b9f20b94bf8cb1dd62b9f847b69c35cd77ad9416
child 266477 4938efa0a6cc880f0a970787c22e59305633737f
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)
reviewersgps
bugs1139254
milestone39.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 1139254 - Introduce a new jsm to register mock easily. r=gps
testing/modules/MockRegistrar.jsm
testing/modules/moz.build
testing/modules/tests/xpcshell/test_mockRegistrar.js
testing/modules/tests/xpcshell/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/testing/modules/MockRegistrar.jsm
@@ -0,0 +1,123 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+  "MockRegistrar",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+let logger = Log.repository.getLogger("MockRegistrar");
+
+this.MockRegistrar = Object.freeze({
+  _registeredComponents: new Map(),
+  _originalCIDs: new Map(),
+  get registrar() {
+    return Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  },
+
+  /**
+   * Register a mock to override target interfaces.
+   * The target interface may be accessed through _genuine property of the mock.
+   * If you register multiple mocks to the same contract ID, you have to call
+   * unregister in reverse order. Otherwise the previous factory will not be
+   * restored.
+   *
+   * @param contractID The contract ID of the interface which is overridden by
+                       the mock.
+   *                   e.g. "@mozilla.org/file/directory_service;1"
+   * @param mock       An object which implements interfaces for the contract ID.
+   * @param args       An array which is passed in the constructor of mock.
+   *
+   * @return           The CID of the mock.
+   */
+  register(contractID, mock, args) {
+    let originalCID = this._originalCIDs.get(contractID);
+    if (!originalCID) {
+      originalCID = this.registrar.contractIDToCID(contractID);
+      this._originalCIDs.set(contractID, originalCID);
+    }
+
+    let originalFactory = Cm.getClassObject(originalCID, Ci.nsIFactory);
+
+    let factory = {
+      createInstance(outer, iid) {
+        if (outer) {
+          throw Cr.NS_ERROR_NO_AGGREGATION;
+        }
+
+        let wrappedMock;
+        if (mock.prototype && mock.prototype.constructor) {
+          wrappedMock = Object.create(mock.prototype);
+          mock.apply(wrappedMock, args);
+        } else {
+          wrappedMock = mock;
+        }
+
+        try {
+          let genuine = originalFactory.createInstance(outer, iid);
+          wrappedMock._genuine = genuine;
+        } catch(ex) {
+          logger.info("Creating original instance failed", ex);
+        }
+
+        return wrappedMock.QueryInterface(iid);
+      },
+      lockFactory(lock) {
+        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+      },
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
+    };
+
+    this.registrar.unregisterFactory(originalCID, originalFactory);
+    this.registrar.registerFactory(originalCID,
+                                   "A Mock for " + contractID,
+                                   contractID,
+                                   factory);
+
+    this._registeredComponents.set(originalCID, {
+      contractID: contractID,
+      factory: factory,
+      originalFactory: originalFactory
+    });
+
+    return originalCID;
+  },
+
+  /**
+   * Unregister the mock.
+   *
+   * @param cid The CID of the mock.
+   */
+  unregister(cid) {
+    let component = this._registeredComponents.get(cid);
+    if (!component) {
+      return;
+    }
+
+    this.registrar.unregisterFactory(cid, component.factory);
+    if (component.originalFactory) {
+      this.registrar.registerFactory(cid,
+                                     "",
+                                     component.contractID,
+                                     component.originalFactory);
+    }
+
+    this._registeredComponents.delete(cid);
+  },
+
+  /**
+   * Unregister all registered mocks.
+   */
+  unregisterAll() {
+    for (let cid of this._registeredComponents.keys()) {
+      this.unregister(cid);
+    }
+  }
+
+});
--- a/testing/modules/moz.build
+++ b/testing/modules/moz.build
@@ -6,11 +6,12 @@
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 TESTING_JS_MODULES += [
     'AppData.jsm',
     'AppInfo.jsm',
     'Assert.jsm',
+    'MockRegistrar.jsm',
     'StructuredLog.jsm',
     'TestUtils.jsm',
 ]
new file mode 100644
--- /dev/null
+++ b/testing/modules/tests/xpcshell/test_mockRegistrar.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+function userInfo(username) {
+  this.username = username;
+}
+
+userInfo.prototype = {
+  fullname: "fullname",
+  emailAddress: "emailAddress",
+  domain: "domain",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIUserInfo]),
+};
+
+function run_test () {
+  run_next_test();
+}
+
+add_test(function test_register() {
+  let localUserInfo = {
+    fullname: "fullname",
+    username: "localusername",
+    emailAddress: "emailAddress",
+    domain: "domain",
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIUserInfo]),
+  };
+
+  let userInfoCID = MockRegistrar.register("@mozilla.org/userinfo;1", localUserInfo);
+  Assert.equal(Cc["@mozilla.org/userinfo;1"].createInstance(Ci.nsIUserInfo).username, "localusername");
+  run_next_test();
+});
+
+add_test(function test_register_with_arguments() {
+  let userInfoCID = MockRegistrar.register("@mozilla.org/userinfo;1", userInfo, ["username"]);
+  Assert.equal(Cc["@mozilla.org/userinfo;1"].createInstance(Ci.nsIUserInfo).username, "username");
+  run_next_test();
+});
+
+add_test(function test_register_twice() {
+  let userInfoCID = MockRegistrar.register("@mozilla.org/userinfo;1", userInfo, ["originalname"]);
+  Assert.equal(Cc["@mozilla.org/userinfo;1"].createInstance(Ci.nsIUserInfo).username, "originalname");
+
+  let newUserInfoCID = MockRegistrar.register("@mozilla.org/userinfo;1", userInfo, ["newname"]);
+  Assert.equal(Cc["@mozilla.org/userinfo;1"].createInstance(Ci.nsIUserInfo).username, "newname");
+  run_next_test();
+});
--- a/testing/modules/tests/xpcshell/xpcshell.ini
+++ b/testing/modules/tests/xpcshell/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head =
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_assert.js]
+[test_mockRegistrar.js]
 [test_structuredlog.js]