diff options
Diffstat (limited to 'toolkit/modules/tests/xpcshell/test_task.js')
-rw-r--r-- | toolkit/modules/tests/xpcshell/test_task.js | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/toolkit/modules/tests/xpcshell/test_task.js b/toolkit/modules/tests/xpcshell/test_task.js new file mode 100644 index 0000000000..682af45c61 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_task.js @@ -0,0 +1,579 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the Task.jsm module. + */ + +//////////////////////////////////////////////////////////////////////////////// +/// Globals + +ChromeUtils.defineModuleGetter(this, "Task", + "resource://testing-common/Task.jsm"); + +/** + * Returns a promise that will be resolved with the given value, when an event + * posted on the event loop of the main thread is processed. + */ +function promiseResolvedLater(aValue) { + return new Promise(resolve => { + Services.tm.dispatchToMainThread(() => resolve(aValue)); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Tests + +function run_test() +{ + run_next_test(); +} + +add_test(function test_spawn_primitive() +{ + function fibonacci(n) { + return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); + }; + + // Polymorphism between task and non-task functions (see "test_recursion"). + Task.spawn(fibonacci(6)).then(function (result) { + Assert.equal(8, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function() +{ + Task.spawn(function () { + return "This is not a generator."; + }).then(function (result) { + Assert.equal("This is not a generator.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_this() +{ + Task.spawn(function () { + return this; + }).then(function (result) { + // Since the task function wasn't defined in strict mode, its "this" object + // should be the same as the "this" object in this function, i.e. the global + // object. + Assert.equal(result, this); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_this_strict() +{ + "use strict"; + Task.spawn(function () { + return this; + }).then(function (result) { + // Since the task function was defined in strict mode, its "this" object + // should be undefined. + Assert.equal(typeof(result), "undefined"); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_returning_promise() +{ + Task.spawn(function () { + return promiseResolvedLater("Resolution value."); + }).then(function (result) { + Assert.equal("Resolution value.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_exceptions() +{ + Task.spawn(function () { + throw new Error("Exception uncaught by task."); + }).then(function (result) { + do_throw("Unexpected success!"); + }, function (ex) { + Assert.equal("Exception uncaught by task.", ex.message); + run_next_test(); + }); +}); + +add_test(function test_spawn_function_taskresult() +{ + Task.spawn(function () { + throw new Task.Result("Task result"); + }).then(function (result) { + Assert.equal("Task result", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_yielded_undefined() +{ + Task.spawn(function* () { + yield; + return "We continued correctly."; + }).then(function (result) { + Assert.equal("We continued correctly.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_yielded_primitive() +{ + Task.spawn(function* () { + return "Primitive " + (yield "value."); + }).then(function (result) { + Assert.equal("Primitive value.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_star_normal() +{ + Task.spawn(function* () { + let result = yield Promise.resolve("Value"); + for (let i = 0; i < 3; i++) { + result += yield promiseResolvedLater("!"); + } + return "Task result: " + result; + }).then(function (result) { + Assert.equal("Task result: Value!!!", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_star_exceptions() +{ + Task.spawn(function* () { + try { + yield Promise.reject("Rejection result by promise."); + do_throw("Exception expected because the promise was rejected."); + } catch (ex) { + // We catch this exception now, we will throw a different one later. + Assert.equal("Rejection result by promise.", ex); + } + throw new Error("Exception uncaught by task."); + }).then(function (result) { + do_throw("Unexpected success!"); + }, function (ex) { + Assert.equal("Exception uncaught by task.", ex.message); + run_next_test(); + }); +}); + +add_test(function test_star_recursion() +{ + function* task_fibonacci(n) { + return n < 2 ? n : (yield task_fibonacci(n - 1)) + + (yield task_fibonacci(n - 2)); + }; + + Task.spawn(task_fibonacci(6)).then(function (result) { + Assert.equal(8, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_nested_star() +{ + Task.spawn(function* () { + return yield (function* () { + return yield 5; + })(); + }).then(function (result) { + Assert.equal(5, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_from_generator() +{ + Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function* (param) { + Assert.equal(this, object); + return param; + }) + }; + + // Ensure the async function returns a promise that resolves as expected. + Assert.equal((yield object.asyncFunction(1)), 1); + + // Ensure a second call to the async function also returns such a promise. + Assert.equal((yield object.asyncFunction(3)), 3); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_from_function() +{ + Task.spawn(function* () { + return Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function (param) { + Assert.equal(this, object); + return param; + }) + }; + + // Ensure the async function returns a promise that resolves as expected. + Assert.equal((yield object.asyncFunction(5)), 5); + + // Ensure a second call to the async function also returns such a promise. + Assert.equal((yield object.asyncFunction(7)), 7); + }); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_that_throws_rejects_promise() +{ + Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function* () { + throw "Rejected!"; + }) + }; + + yield object.asyncFunction(); + }).then(function () { + do_throw("unexpected success calling async function that throws error"); + }, function (ex) { + Assert.equal(ex, "Rejected!"); + run_next_test(); + }); +}); + +add_test(function test_async_return_function() +{ + Task.spawn(function* () { + // Ensure an async function that returns a function resolves to the function + // itself instead of calling the function and resolving to its return value. + return Task.spawn(function* () { + let returnValue = function () { + return "These aren't the droids you're looking for."; + }; + + let asyncFunction = Task.async(function () { + return returnValue; + }); + + Assert.equal((yield asyncFunction()), returnValue); + }); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_throw_argument_not_function() +{ + Task.spawn(function* () { + // Ensure Task.async throws if its aTask argument is not a function. + Assert.throws(() => Task.async("not a function"), + /aTask argument must be a function/); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_throw_on_function_in_place_of_promise() +{ + Task.spawn(function* () { + // Ensure Task.spawn throws if passed an async function. + Assert.throws(() => Task.spawn(Task.async(function* () {})), + /Cannot use an async function in place of a promise/); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + + +////////////////// Test rewriting of stack traces + +// Backup Task.Debuggin.maintainStack. +// Will be restored by `exit_stack_tests`. +var maintainStack; +add_test(function enter_stack_tests() { + maintainStack = Task.Debugging.maintainStack; + Task.Debugging.maintainStack = true; + run_next_test(); +}); + + +/** + * Ensure that a list of frames appear in a stack, in the right order + */ +function do_check_rewritten_stack(frames, ex) { + info("Checking that the expected frames appear in the right order"); + info(frames.join(", ")); + let stack = ex.stack; + info(stack); + + let framesFound = 0; + let lineNumber = 0; + let reLine = /([^\r\n])+/g; + let match; + while (framesFound < frames.length && (match = reLine.exec(stack))) { + let line = match[0]; + let frame = frames[framesFound]; + info("Searching for " + frame + " in line " + line); + if (line.includes(frame)) { + info("Found " + frame); + ++framesFound; + } else { + info("Didn't find " + frame); + } + } + + if (framesFound >= frames.length) { + return; + } + do_throw("Did not find: " + frames.slice(framesFound).join(", ") + + " in " + stack.substr(reLine.lastIndex)); + + info("Ensuring that we have removed Task.jsm, Promise.jsm"); + Assert.ok(!stack.includes("Task.jsm")); + Assert.ok(!stack.includes("Promise.jsm")); + Assert.ok(!stack.includes("Promise-backend.js")); +} + + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.spawn. +add_test(function test_spawn_throw_stack() { + Task.spawn(function* task_spawn_throw_stack() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_spawn_throw_stack", + "test_spawn_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we yield +// a rejection in a Task.spawn. +add_test(function test_spawn_yield_reject_stack() { + Task.spawn(function* task_spawn_yield_reject_stack() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_spawn_yield_reject_stack", + "test_spawn_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_function_throw_stack() { + let task_async_function_throw_stack = Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + })().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_function_throw_stack", + "test_async_function_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_function_yield_reject_stack() { + let task_async_function_yield_reject_stack = Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + })().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_function_yield_reject_stack", + "test_async_function_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_method_throw_stack() { + let object = { + task_async_method_throw_stack: Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + }) + }; + object.task_async_method_throw_stack().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_method_throw_stack", + "test_async_method_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_method_yield_reject_stack() { + let object = { + task_async_method_yield_reject_stack: Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + }) + }; + object.task_async_method_yield_reject_stack().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_method_yield_reject_stack", + "test_async_method_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that two tasks whose execution takes place interleaved do not capture each other's stack. +add_test(function test_throw_stack_do_not_capture_the_wrong_task() { + for (let iter_a of [3, 4, 5]) { // Vary the interleaving + for (let iter_b of [3, 4, 5]) { + Task.spawn(function* task_a() { + for (let i = 0; i < iter_a; ++i) { + yield Promise.resolve(); + } + throw new Error("BOOM"); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_a", + "test_throw_stack_do_not_capture_the_wrong_task"], + ex); + Assert.ok(!ex.stack.includes("task_b")); + run_next_test(); + }); + Task.spawn(function* task_b() { + for (let i = 0; i < iter_b; ++i) { + yield Promise.resolve(); + } + }); + } + } +}); + +// Put things together +add_test(function test_throw_complex_stack() +{ + // Setup the following stack: + // inner_method() + // task_3() + // task_2() + // task_1() + // function_3() + // function_2() + // function_1() + // test_throw_complex_stack() + (function function_1() { + return (function function_2() { + return (function function_3() { + return Task.spawn(function* task_1() { + yield Promise.resolve(); + try { + yield Task.spawn(function* task_2() { + yield Promise.resolve(); + yield Task.spawn(function* task_3() { + yield Promise.resolve(); + let inner_object = { + inner_method: Task.async(function*() { + throw new Error("BOOM"); + }) + }; + yield Promise.resolve(); + yield inner_object.inner_method(); + }); + }); + } catch (ex) { + yield Promise.resolve(); + throw ex; + } + }); + })(); + })(); + })().then( + () => do_throw("Shouldn't have succeeded"), + (ex) => { + let expect = ["inner_method", + "task_3", + "task_2", + "task_1", + "function_3", + "function_2", + "function_1", + "test_throw_complex_stack"]; + do_check_rewritten_stack(expect, ex); + + run_next_test(); + }); +}); + +add_test(function test_without_maintainStack() { + info("Calling generateReadableStack without a Task"); + Task.Debugging.generateReadableStack(new Error("Not a real error")); + + Task.Debugging.maintainStack = false; + + info("Calling generateReadableStack with neither a Task nor maintainStack"); + Task.Debugging.generateReadableStack(new Error("Not a real error")); + + info("Calling generateReadableStack without maintainStack"); + Task.spawn(function*() { + Task.Debugging.generateReadableStack(new Error("Not a real error")); + run_next_test(); + }); +}); + +add_test(function exit_stack_tests() { + Task.Debugging.maintainStack = maintainStack; + run_next_test(); +}); |