/* 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(); });