Bug 1010518 - Maintaining stack information through Task.jsm. r=paolo

This commit is contained in:
David Rajchenbach-Teller 2014-06-09 13:27:00 +02:00
parent 2c75ba8d1d
commit 2b5b59d204
2 changed files with 280 additions and 13 deletions

View File

@ -241,6 +241,9 @@ function createAsyncFunction(aTask) {
* that is fulfilled when the task terminates.
*/
function TaskImpl(iterator) {
if (Task.Debugging.maintainStack) {
this._stack = (new Error()).stack;
}
this.deferred = Promise.defer();
this._iterator = iterator;
this._isStarGenerator = !("send" in iterator);
@ -346,23 +349,74 @@ TaskImpl.prototype = {
* The uncaught exception to handle.
*/
_handleException: function TaskImpl_handleException(aException) {
if (aException && typeof aException == "object" && "name" in aException &&
ERRORS_TO_REPORT.indexOf(aException.name) != -1) {
if (aException && typeof aException == "object" && "stack" in aException) {
// We suspect that the exception is a programmer error, so we now
// display it using dump(). Note that we do not use Cu.reportError as
// we assume that this is a programming error, so we do not want end
// users to see it. Also, if the programmer handles errors correctly,
// they will either treat the error or log them somewhere.
let stack = aException.stack;
let stack = ("stack" in aException) ? aException.stack : "not available";
dump("*************************\n");
dump("A coding exception was thrown and uncaught in a Task.\n\n");
dump("Full message: " + aException + "\n");
dump("Full stack: " + stack + "\n");
dump("*************************\n");
if (Task.Debugging.maintainStack &&
aException._capturedTaskStack != this._stack &&
typeof stack == "string") {
// Rewrite the stack for more readability.
let bottomStack = this._stack;
let topStack = aException.stack;
// Cut `topStack` at the first line that contains Task.jsm, keep the head.
let reLine = /([^\r\n])+/g;
let match;
let lines = [];
while ((match = reLine.exec(topStack))) {
let line = match[0];
if (line.indexOf("/Task.jsm:") != -1) {
break;
}
lines.push(line);
}
// Cut `bottomStack` at the last line of the first block that contains Task.jsm
reLine = /([^\r\n])+/g;
while ((match = reLine.exec(bottomStack))) {
let line = match[0];
if (line.indexOf("/Task.jsm:") == -1) {
let tail = bottomStack.substring(match.index);
lines.push(tail);
break;
}
}
stack = lines.join("\n");
aException.stack = stack;
// If aException is reinjected in the same task and rethrown,
// we don't want to perform the rewrite again.
aException._capturedTaskStack = bottomStack;
} else if (!stack) {
stack = "Not available";
}
if ("name" in aException &&
ERRORS_TO_REPORT.indexOf(aException.name) != -1) {
// We suspect that the exception is a programmer error, so we now
// display it using dump(). Note that we do not use Cu.reportError as
// we assume that this is a programming error, so we do not want end
// users to see it. Also, if the programmer handles errors correctly,
// they will either treat the error or log them somewhere.
dump("*************************\n");
dump("A coding exception was thrown and uncaught in a Task.\n\n");
dump("Full message: " + aException + "\n");
dump("Full stack: " + aException.stack + "\n");
dump("*************************\n");
}
}
this.deferred.reject(aException);
}
};
Task.Debugging = {
maintainStack: false
};

View File

@ -385,3 +385,216 @@ add_test(function test_async_throw_on_function_in_place_of_promise()
do_throw("Unexpected error: " + ex);
});
});
////////////////// Test rewriting of stack traces
// Backup Task.Debuggin.maintainStack.
// Will be restored by `exit_stack_tests`.
let 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) {
do_print("Checking that the expected frames appear in the right order");
do_print(frames.join(", "));
let stack = ex.stack;
do_print(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];
do_print("Searching for " + frame + " in line " + line);
if (line.indexOf(frame) != -1) {
do_print("Found " + frame);
++framesFound;
} else {
do_print("Didn't find " + frame);
}
}
if (framesFound >= frames.length) {
return;
}
do_throw("Did not find: " + frames.slice(framesFound).join(", ") +
" in " + stack.substr(reLine.lastIndex));
do_print("Ensuring that we have removed Task.jsm, Promise.jsm");
do_check_true(stack.indexOf("Task.jsm") == -1);
do_check_true(stack.indexOf("Promise.jsm") == -1);
do_check_true(stack.indexOf("Promise-backend.js") == -1);
}
// 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();
});
});
// 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 exit_stack_tests() {
Task.Debugging.maintainStack = false;
run_next_test();
});