diff options
Diffstat (limited to 'toolkit/components/asyncshutdown/tests/xpcshell')
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] |