summaryrefslogtreecommitdiffstats
path: root/toolkit/components/asyncshutdown/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/asyncshutdown/tests')
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/head.js174
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js194
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js96
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js88
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini8
6 files changed, 567 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js b/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/head.js b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
new file mode 100644
index 000000000..9de489808
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+var asyncShutdownService = Cc["@mozilla.org/async-shutdown-service;1"].
+ getService(Ci.nsIAsyncShutdownService);
+
+
+Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+
+/**
+ * Utility function used to provide the same API for various sources
+ * of async shutdown barriers.
+ *
+ * @param {string} kind One of
+ * - "phase" to test an AsyncShutdown phase;
+ * - "barrier" to test an instance of AsyncShutdown.Barrier;
+ * - "xpcom-barrier" to test an instance of nsIAsyncShutdownBarrier;
+ * - "xpcom-barrier-unwrapped" to test the field `jsclient` of a nsIAsyncShutdownClient.
+ *
+ * @return An object with the following methods:
+ * - addBlocker() - the same method as AsyncShutdown phases and barrier clients
+ * - wait() - trigger the resolution of the lock
+ */
+function makeLock(kind) {
+ if (kind == "phase") {
+ let topic = "test-Phase-" + ++makeLock.counter;
+ let phase = AsyncShutdown._getPhase(topic);
+ return {
+ addBlocker: function(...args) {
+ return phase.addBlocker(...args);
+ },
+ removeBlocker: function(blocker) {
+ return phase.removeBlocker(blocker);
+ },
+ wait: function() {
+ Services.obs.notifyObservers(null, topic, null);
+ return Promise.resolve();
+ }
+ };
+ } else if (kind == "barrier") {
+ let name = "test-Barrier-" + ++makeLock.counter;
+ let barrier = new AsyncShutdown.Barrier(name);
+ return {
+ addBlocker: barrier.client.addBlocker,
+ removeBlocker: barrier.client.removeBlocker,
+ wait: function() {
+ return barrier.wait();
+ }
+ };
+ } else if (kind == "xpcom-barrier") {
+ let name = "test-xpcom-Barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ return {
+ addBlocker: function(blockerName, condition, state) {
+ if (condition == null) {
+ // Slight trick as `null` or `undefined` cannot be used as keys
+ // for `xpcomMap`. Note that this has no incidence on the result
+ // of the test as the XPCOM interface imposes that the condition
+ // is a method, so it cannot be `null`/`undefined`.
+ condition = "<this case can't happen with the xpcom interface>";
+ }
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ blocker = {
+ name: blockerName,
+ state: state,
+ blockShutdown: function(aBarrierClient) {
+ return Task.spawn(function*() {
+ try {
+ if (typeof condition == "function") {
+ yield Promise.resolve(condition());
+ } else {
+ yield Promise.resolve(condition);
+ }
+ } finally {
+ aBarrierClient.removeBlocker(blocker);
+ }
+ });
+ },
+ };
+ makeLock.xpcomMap.set(condition, blocker);
+ }
+ let {fileName, lineNumber, stack} = (new Error());
+ return barrier.client.addBlocker(blocker, fileName, lineNumber, stack);
+ },
+ removeBlocker: function(condition) {
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ return;
+ }
+ barrier.client.removeBlocker(blocker);
+ },
+ wait: function() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ }
+ };
+ } else if ("unwrapped-xpcom-barrier") {
+ let name = "unwrapped-xpcom-barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ let client = barrier.client.jsclient;
+ return {
+ addBlocker: client.addBlocker,
+ removeBlocker: client.removeBlocker,
+ wait: function() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ }
+ };
+ }
+ throw new TypeError("Unknown kind " + kind);
+}
+makeLock.counter = 0;
+makeLock.xpcomMap = new Map(); // Note: Not a WeakMap as we wish to handle non-gc-able keys (e.g. strings)
+
+/**
+ * An asynchronous task that takes several ticks to complete.
+ *
+ * @param {*=} resolution The value with which the resulting promise will be
+ * resolved once the task is complete. This may be a rejected promise,
+ * in which case the resulting promise will itself be rejected.
+ * @param {object=} outResult An object modified by side-effect during the task.
+ * Initially, its field |isFinished| is set to |false|. Once the task is
+ * complete, its field |isFinished| is set to |true|.
+ *
+ * @return {promise} A promise fulfilled once the task is complete
+ */
+function longRunningAsyncTask(resolution = undefined, outResult = {}) {
+ outResult.isFinished = false;
+ if (!("countFinished" in outResult)) {
+ outResult.countFinished = 0;
+ }
+ let deferred = Promise.defer();
+ do_timeout(100, function() {
+ ++outResult.countFinished;
+ outResult.isFinished = true;
+ deferred.resolve(resolution);
+ });
+ return deferred.promise;
+}
+
+function get_exn(f) {
+ try {
+ f();
+ return null;
+ } catch (ex) {
+ return ex;
+ }
+}
+
+function do_check_exn(exn, constructor) {
+ do_check_neq(exn, null);
+ if (exn.name == constructor) {
+ do_check_eq(exn.constructor.name, constructor);
+ return;
+ }
+ do_print("Wrong error constructor");
+ do_print(exn.constructor.name);
+ do_print(exn.stack);
+ do_check_true(false);
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
new file mode 100644
index 000000000..f1aebc3ad
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_no_condition() {
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Testing a barrier with no condition (" + kind + ")");
+ let lock = makeLock(kind);
+ yield lock.wait();
+ do_print("Barrier with no condition didn't lock");
+ }
+});
+
+add_task(function* test_phase_various_failures() {
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Kind: " + kind);
+ // Testing with wrong arguments
+ let lock = makeLock(kind);
+
+ Assert.throws(() => lock.addBlocker(), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
+ Assert.throws(() => lock.addBlocker(null, true), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
+
+ if (kind != "xpcom-barrier") {
+ // xpcom-barrier actually expects a string in that position
+ Assert.throws(() => lock.addBlocker("Test 2", () => true, "not a function"), /TypeError/);
+ }
+
+ // Attempting to add a blocker after we are done waiting
+ yield lock.wait();
+ Assert.throws(() => lock.addBlocker("Test 3", () => true), /is finished/);
+ }
+});
+
+add_task(function* test_reentrant() {
+ do_print("Ensure that we can call addBlocker from within a blocker");
+
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Kind: " + kind);
+ let lock = makeLock(kind);
+
+ let deferredOuter = PromiseUtils.defer();
+ let deferredInner = PromiseUtils.defer();
+ let deferredBlockInner = PromiseUtils.defer();
+
+ lock.addBlocker("Outer blocker", () => {
+ do_print("Entering outer blocker");
+ deferredOuter.resolve();
+ lock.addBlocker("Inner blocker", () => {
+ do_print("Entering inner blocker");
+ deferredInner.resolve();
+ return deferredBlockInner.promise;
+ });
+ });
+
+ // Note that phase-style locks spin the event loop and do not return from
+ // `lock.wait()` until after all blockers have been resolved. Therefore,
+ // to be able to test them, we need to dispatch the following steps to the
+ // event loop before calling `lock.wait()`, which we do by forcing
+ // a Promise.resolve().
+ //
+ let promiseSteps = Task.spawn(function* () {
+ yield Promise.resolve();
+
+ do_print("Waiting until we have entered the outer blocker");
+ yield deferredOuter.promise;
+
+ do_print("Waiting until we have entered the inner blocker");
+ yield deferredInner.promise;
+
+ do_print("Allowing the lock to resolve")
+ deferredBlockInner.resolve();
+ });
+
+ do_print("Starting wait");
+ yield lock.wait();
+
+ do_print("Waiting until all steps have been walked");
+ yield promiseSteps;
+ }
+});
+
+
+add_task(function* test_phase_removeBlocker() {
+ do_print("Testing that we can call removeBlocker before, during and after the call to wait()");
+
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+
+ do_print("Switching to kind " + kind);
+ do_print("Attempt to add then remove a blocker before wait()");
+ let lock = makeLock(kind);
+ let blocker = () => {
+ do_print("This promise will never be resolved");
+ return Promise.defer().promise;
+ };
+
+ lock.addBlocker("Wait forever", blocker);
+ let do_remove_blocker = function(aLock, aBlocker, aShouldRemove) {
+ do_print("Attempting to remove blocker " + aBlocker + ", expecting result " + aShouldRemove);
+ if (kind == "xpcom-barrier") {
+ // The xpcom variant always returns `undefined`, so we can't
+ // check its result.
+ aLock.removeBlocker(aBlocker);
+ return;
+ }
+ do_check_eq(aLock.removeBlocker(aBlocker), aShouldRemove);
+ };
+ do_remove_blocker(lock, blocker, true);
+ do_remove_blocker(lock, blocker, false);
+ do_print("Attempt to remove non-registered blockers before wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ do_print("Waiting (should lift immediately)");
+ yield lock.wait();
+
+ do_print("Attempt to add a blocker then remove it during wait()");
+ lock = makeLock(kind);
+ let blockers = [
+ () => {
+ do_print("This blocker will self-destruct");
+ do_remove_blocker(lock, blockers[0], true);
+ return Promise.defer().promise;
+ },
+ () => {
+ do_print("This blocker will self-destruct twice");
+ do_remove_blocker(lock, blockers[1], true);
+ do_remove_blocker(lock, blockers[1], false);
+ return Promise.defer().promise;
+ },
+ () => {
+ do_print("Attempt to remove non-registered blockers during wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ }
+ ];
+ for (let i in blockers) {
+ lock.addBlocker("Wait forever again: " + i, blockers[i]);
+ }
+ do_print("Waiting (should lift very quickly)");
+ yield lock.wait();
+ do_remove_blocker(lock, blockers[0], false);
+
+
+ do_print("Attempt to remove a blocker after wait");
+ lock = makeLock(kind);
+ blocker = Promise.resolve.bind(Promise);
+ yield lock.wait();
+ do_remove_blocker(lock, blocker, false);
+
+ do_print("Attempt to remove non-registered blocker after wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ }
+
+});
+
+add_task(function* test_state() {
+ do_print("Testing information contained in `state`");
+
+ let BLOCKER_NAME = "test_state blocker " + Math.random();
+
+ // Set up the barrier. Note that we cannot test `barrier.state`
+ // immediately, as it initially contains "Not started"
+ let barrier = new AsyncShutdown.Barrier("test_filename");
+ let deferred = Promise.defer();
+ let {filename, lineNumber} = Components.stack;
+ barrier.client.addBlocker(BLOCKER_NAME,
+ function() {
+ return deferred.promise;
+ });
+
+ let promiseDone = barrier.wait();
+
+ // Now that we have called `wait()`, the state contains interesting things
+ let state = barrier.state[0];
+ do_print("State: " + JSON.stringify(barrier.state, null, "\t"));
+ Assert.equal(state.filename, filename);
+ Assert.equal(state.lineNumber, lineNumber + 1);
+ Assert.equal(state.name, BLOCKER_NAME);
+ Assert.ok(state.stack.some(x => x.includes("test_state")), "The stack contains the caller function's name");
+ Assert.ok(state.stack.some(x => x.includes(filename)), "The stack contains the calling file's name");
+
+ deferred.resolve();
+ yield promiseDone;
+});
+
+add_task(function*() {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
new file mode 100644
index 000000000..33da1f53f
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+//
+// This file contains tests that need to leave uncaught asynchronous
+// errors. If your test catches all its asynchronous errors, please
+// put it in another file.
+//
+
+Promise.Debugging.clearUncaughtErrorObservers();
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_phase_simple_async() {
+ do_print("Testing various combinations of a phase with a single condition");
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolution of [arg, Promise.reject(arg)]) {
+ for (let success of [false, true]) {
+ for (let state of [[null],
+ [],
+ [() => "some state"],
+ [function() {
+ throw new Error("State BOOM"); }],
+ [function() {
+ return {
+ toJSON: function() {
+ throw new Error("State.toJSON BOOM");
+ }
+ };
+ }]]) {
+ // Asynchronous phase
+ do_print("Asynchronous test with " + arg + ", " + resolution + ", " + kind);
+ let lock = makeLock(kind);
+ let outParam = { isFinished: false };
+ lock.addBlocker(
+ "Async test",
+ function() {
+ if (success) {
+ return longRunningAsyncTask(resolution, outParam);
+ }
+ throw resolution;
+ },
+ ...state
+ );
+ do_check_false(outParam.isFinished);
+ yield lock.wait();
+ do_check_eq(outParam.isFinished, success);
+ }
+ }
+
+ // Synchronous phase - just test that we don't throw/freeze
+ do_print("Synchronous test with " + arg + ", " + resolution + ", " + kind);
+ let lock = makeLock(kind);
+ lock.addBlocker(
+ "Sync test",
+ resolution
+ );
+ yield lock.wait();
+ }
+ }
+ }
+});
+
+add_task(function* test_phase_many() {
+ do_print("Testing various combinations of a phase with many conditions");
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ let lock = makeLock(kind);
+ let outParams = [];
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolve of [true, false]) {
+ do_print("Testing with " + kind + ", " + arg + ", " + resolve);
+ let resolution = resolve ? arg : Promise.reject(arg);
+ let outParam = { isFinished: false };
+ lock.addBlocker(
+ "Test " + Math.random(),
+ () => longRunningAsyncTask(resolution, outParam)
+ );
+ }
+ }
+ do_check_true(outParams.every((x) => !x.isFinished));
+ yield lock.wait();
+ do_check_true(outParams.every((x) => x.isFinished));
+ }
+});
+
+
+
+
+add_task(function*() {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
+
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
new file mode 100644
index 000000000..c6c923187
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test conversion between nsIPropertyBag and JS values.
+ */
+
+var PropertyBagConverter = asyncShutdownService.wrappedJSObject._propertyBagConverter;
+
+function run_test() {
+ test_conversions();
+}
+
+function normalize(obj) {
+ if (obj == null || typeof obj != "object") {
+ return obj;
+ }
+ if (Array.isArray(obj)) {
+ return obj.map(normalize);
+ }
+ let result = {};
+ for (let k of Object.keys(obj).sort()) {
+ result[k] = normalize(obj[k]);
+ }
+ return result;
+}
+
+function test_conversions() {
+ const SAMPLES = [
+ // Simple values
+ 1,
+ true,
+ "string",
+ null,
+
+ // Objects
+ {
+ a: 1,
+ b: true,
+ c: "string",
+ d:.5,
+ e: [2, false, "another string", .3],
+ f: [],
+ g: {
+ a2: 1,
+ b2: true,
+ c2: "string",
+ d2:.5,
+ e2: [2, false, "another string", .3],
+ f2: [],
+ g2: [{
+ a3: 1,
+ b3: true,
+ c3: "string",
+ d3:.5,
+ e3: [2, false, "another string", .3],
+ f3: [],
+ g3: {}
+ }]
+ }
+ }];
+
+ for (let sample of SAMPLES) {
+ let stringified = JSON.stringify(normalize(sample), null, "\t");
+ do_print("Testing conversions of " + stringified);
+ let rewrites = [sample];
+ for (let i = 1; i < 3; ++i) {
+ let source = rewrites[i - 1];
+ let bag = PropertyBagConverter.fromValue(source);
+ do_print(" => " + bag);
+ if (source == null) {
+ Assert.ok(bag == null, "The bag is null");
+ } else if (typeof source == "object") {
+ Assert.ok(bag instanceof Ci.nsIPropertyBag, "The bag is a property bag");
+ } else {
+ Assert.ok(typeof bag != "object", "The bag is not an object");
+ }
+ let dest = PropertyBagConverter.toValue(bag);
+ let restringified = JSON.stringify(normalize(dest), null, "\t");
+ do_print("Comparing");
+ do_print(stringified);
+ do_print(restringified);
+ Assert.deepEqual(sample, dest, "Testing after " + i + " conversions");
+ rewrites.push(dest);
+ }
+ }
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..f573955bc
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head=head.js
+tail=
+skip-if = toolkit == 'android'
+
+[test_AsyncShutdown.js]
+[test_AsyncShutdown_leave_uncaught.js]
+[test_converters.js]