Bug 1112378 - Rewrite errors from call watcher to hide information of the error originating from call watcher, rather than content. r=vp

This commit is contained in:
Jordan Santell 2015-02-13 19:10:00 -05:00
parent 2a77de85e3
commit b82b9b797d
5 changed files with 191 additions and 2 deletions

View File

@ -13,6 +13,7 @@ support-files =
doc_automation.html
doc_bug_1125817.html
doc_bug_1130901.html
doc_bug_1112378.html
440hz_sine.ogg
head.js
@ -31,6 +32,7 @@ skip-if = true # bug 1092571
[browser_audionode-actor-get-automation-data-02.js]
[browser_audionode-actor-get-automation-data-03.js]
[browser_callwatcher-01.js]
[browser_callwatcher-02.js]
[browser_webaudio-actor-simple.js]
[browser_webaudio-actor-destroy-node.js]
[browser_webaudio-actor-connect-param.js]

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Bug 1112378
* Tests to ensure that errors called on wrapped functions via call-watcher
* correctly looks like the error comes from the content, not from within the devtools.
*/
const BUG_1112378_URL = EXAMPLE_URL + "doc_bug_1112378.html";
add_task(function*() {
let { target, panel } = yield initWebAudioEditor(BUG_1112378_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, gAudioNodes } = panelWin;
loadFrameScripts();
reload(target);
yield waitForGraphRendered(panelWin, 2, 0);
let error = yield evalInDebuggee("throwError()");
is(error.lineNumber, 21, "error has correct lineNumber");
is(error.columnNumber, 11, "error has correct columnNumber");
is(error.name, "TypeError", "error has correct name");
is(error.message, "Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error has correct message");
is(error.stringified, "TypeError: Argument 1 is not valid for any of the 2-argument overloads of AudioNode.connect.", "error is stringified correctly");
ise(error.instanceof, true, "error is correctly an instanceof TypeError");
is(error.fileName, "http://example.com/browser/browser/devtools/webaudioeditor/test/doc_bug_1112378.html", "error has correct fileName");
error = yield evalInDebuggee("throwDOMException()");
is(error.lineNumber, 37, "exception has correct lineNumber");
is(error.columnNumber, 0, "exception has correct columnNumber");
is(error.code, 9, "exception has correct code");
is(error.result, 2152923145, "exception has correct result");
is(error.name, "NotSupportedError", "exception has correct name");
is(error.message, "Operation is not supported", "exception has correct message");
is(error.stringified, "NotSupportedError: Operation is not supported", "exception is stringified correctly");
ise(error.instanceof, true, "exception is correctly an instance of DOMException");
is(error.filename, "http://example.com/browser/browser/devtools/webaudioeditor/test/doc_bug_1112378.html", "exception has correct filename");
yield teardown(target);
});

View File

@ -0,0 +1,57 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Web Audio Editor test page</title>
</head>
<body>
<script type="text/javascript;version=1.8">
"use strict";
let ctx = new AudioContext();
let osc = ctx.createOscillator();
function throwError () {
try {
osc.connect({});
} catch (e) {
return {
lineNumber: e.lineNumber,
fileName: e.fileName,
columnNumber: e.columnNumber,
message: e.message,
instanceof: e instanceof TypeError,
stringified: e.toString(),
name: e.name
}
}
}
function throwDOMException () {
try {
osc.frequency.setValueAtTime(0, -1);
} catch (e) {
return {
lineNumber: e.lineNumber,
columnNumber: e.columnNumber,
filename: e.filename,
message: e.message,
code: e.code,
result: e.result,
instanceof: e instanceof DOMException,
stringified: e.toString(),
name: e.name
}
}
}
</script>
</body>
</html>

View File

@ -16,10 +16,13 @@ let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio");
let TargetFactory = devtools.TargetFactory;
let mm = null;
const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/test/";
const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
@ -47,6 +50,15 @@ registerCleanupFunction(() => {
Cu.forceGC();
});
/**
* Call manually in tests that use frame script utils after initializing
* the web audio editor. Call after init but before navigating to a different page.
*/
function loadFrameScripts () {
mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
}
function addTab(aUrl, aWindow) {
info("Adding tab: " + aUrl);
@ -440,6 +452,33 @@ function waitForInspectorRender (panelWin, EVENTS) {
]);
}
/**
* Takes a string `script` and evaluates it directly in the content
* in potentially a different process.
*/
function evalInDebuggee (script) {
let deferred = Promise.defer();
if (!mm) {
throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
}
let id = generateUUID().toString();
mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
mm.addMessageListener("devtools:test:eval:response", handler);
function handler ({ data }) {
if (id !== data.id) {
return;
}
mm.removeMessageListener("devtools:test:eval:response", handler);
deferred.resolve(data.value);
}
return deferred.promise;
}
/**
* List of audio node properties to test against expectations of the AudioNode actor
*/

View File

@ -7,7 +7,7 @@ const {Cc, Ci, Cu, Cr} = require("chrome");
const events = require("sdk/event/core");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const protocol = require("devtools/server/protocol");
const {ContentObserver} = require("devtools/content-observer");
const {serializeStack, parseStack} = require("toolkit/loader");
const {on, once, off, emit} = events;
const {method, Arg, Option, RetVal} = protocol;
@ -417,7 +417,12 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
let originalFunc = Cu.unwaiveXrays(target[name]);
Cu.exportFunction(function(...args) {
let result = Cu.waiveXrays(originalFunc.apply(this, args));
let result;
try {
result = Cu.waiveXrays(originalFunc.apply(this, args));
} catch (e) {
throw createContentError(e, unwrappedWindow);
}
if (self._recording) {
let stack = getStack(name);
@ -693,3 +698,45 @@ function getBitToEnumValue(type, object, arg) {
// Cache the combined bitmask value
return table[arg] = flags.join(" | ") || arg;
}
/**
* Creates a new error from an error that originated from content but was called
* from a wrapped overridden method. This is so we can make our own error
* that does not look like it originated from the call watcher.
*
* We use toolkit/loader's parseStack and serializeStack rather than the
* parsing done in the local `getStack` function, because it does not expose
* column number, would have to change the protocol models `call-stack-items` and `call-details`
* which hurts backwards compatibility, and the local `getStack` is an optimized, hot function.
*/
function createContentError (e, win) {
let { message, name, stack } = e;
let parsedStack = parseStack(stack);
let { fileName, lineNumber, columnNumber } = parsedStack[parsedStack.length - 1];
let error;
let isDOMException = e instanceof Ci.nsIDOMDOMException;
let constructor = isDOMException ? win.DOMException : (win[e.name] || win.Error);
if (isDOMException) {
error = new constructor(message, name);
Object.defineProperties(error, {
code: { value: e.code },
columnNumber: { value: 0 }, // columnNumber is always 0 for DOMExceptions?
filename: { value: fileName }, // note the lowercase `filename`
lineNumber: { value: lineNumber },
result: { value: e.result },
stack: { value: serializeStack(parsedStack) }
});
}
else {
// Constructing an error here retains all the stack information,
// and we can add message, fileName and lineNumber via constructor, though
// need to manually add columnNumber.
error = new constructor(message, fileName, lineNumber);
Object.defineProperty(error, "columnNumber", {
value: columnNumber
});
}
return error;
}