Bug 809561 - Integrate xpcshell test harness with chrome remote debugging. r=past/chmanchester

This commit is contained in:
Mark Hammond 2014-11-29 10:40:58 +11:00
parent 757831e55e
commit 759ac2fdd1
7 changed files with 226 additions and 15 deletions

View File

@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { RootActor } = devtools.require("devtools/server/actors/root");
const { BrowserTabList } = devtools.require("devtools/server/actors/webbrowser");
/**
* xpcshell-test (XPCST) specific actors.
*
*/
/**
* Construct a root actor appropriate for use in a server running xpcshell
* tests. <snip boilerplate> :)
*/
function createRootActor(connection)
{
let parameters = {
tabList: new XPCSTTabList(connection),
globalActorFactories: DebuggerServer.globalActorFactories,
onShutdown() {
// If the user never switches to the "debugger" tab we might get a
// shutdown before we've attached.
Services.obs.notifyObservers(null, "xpcshell-test-devtools-shutdown", null);
}
};
return new RootActor(connection, parameters);
}
/**
* A "stub" TabList implementation that provides no tabs.
*/
function XPCSTTabList(connection)
{
BrowserTabList.call(this, connection);
}
XPCSTTabList.prototype = Object.create(BrowserTabList.prototype);
XPCSTTabList.prototype.constructor = XPCSTTabList;
XPCSTTabList.prototype.getList = function() {
return Promise.resolve([]);
};

View File

@ -336,7 +336,101 @@ function _register_modules_protocol_handler() {
protocolHandler.setSubstitution("testing-common", modulesURI);
}
function _initDebugging(port) {
let prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
// Always allow remote debugging.
prefs.setBoolPref("devtools.debugger.remote-enabled", true);
// for debugging-the-debugging, let an env var cause log spew.
let env = Components.classes["@mozilla.org/process/environment;1"]
.getService(Components.interfaces.nsIEnvironment);
if (env.get("DEVTOOLS_DEBUGGER_LOG")) {
prefs.setBoolPref("devtools.debugger.log", true);
}
if (env.get("DEVTOOLS_DEBUGGER_LOG_VERBOSE")) {
prefs.setBoolPref("devtools.debugger.log.verbose", true);
}
let {DebuggerServer} = Components.utils.import('resource://gre/modules/devtools/dbg-server.jsm', {});
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
// An observer notification that tells us when we can "resume" script
// execution.
let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
let initialized = false;
const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
let observe = function(subject, topic, data) {
switch (topic) {
case "devtools-thread-resumed":
// Exceptions in here aren't reported and block the debugger from
// resuming, so...
try {
// Add a breakpoint for the first line in our test files.
let threadActor = subject.wrappedJSObject;
let location = { line: 1 };
for (let file of _TEST_FILE) {
let sourceActor = threadActor.sources.source({originalUrl: file});
sourceActor.createAndStoreBreakpoint(location);
}
} catch (ex) {
do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
}
break;
case "xpcshell-test-devtools-shutdown":
// the debugger has shutdown before we got a resume event - nothing
// special to do here.
break;
}
initialized = true;
for (let topicToRemove of TOPICS) {
obsSvc.removeObserver(observe, topicToRemove);
}
};
for (let topic of TOPICS) {
obsSvc.addObserver(observe, topic, false);
}
do_print("");
do_print("*******************************************************************");
do_print("Waiting for the debugger to connect on port " + port)
do_print("")
do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
do_print("from the Developer menu and specify the port as " + port);
do_print("*******************************************************************");
do_print("")
DebuggerServer.openListener(port);
// spin an event loop until the debugger connects.
let thr = Components.classes["@mozilla.org/thread-manager;1"]
.getService().currentThread;
while (!initialized) {
do_print("Still waiting for debugger to connect...");
thr.processNextEvent(true);
}
// NOTE: if you want to debug the harness itself, you can now add a 'debugger'
// statement anywhere and it will stop - but we've already added a breakpoint
// for the first line of the test scripts, so we just continue...
do_print("Debugger connected, starting test execution");
}
function _execute_test() {
// _JSDEBUGGER_PORT is dynamically defined by <runxpcshelltests.py>.
if (_JSDEBUGGER_PORT) {
try {
_initDebugging(_JSDEBUGGER_PORT);
} catch (ex) {
do_print("Failed to initialize debugging: " + ex + "\n" + ex.stack);
}
}
_register_protocol_handlers();
// Override idle service by default.
@ -1072,6 +1166,8 @@ function do_load_child_test_harness()
+ "const _HEAD_FILES=" + uneval(_HEAD_FILES) + "; "
+ "const _TAIL_FILES=" + uneval(_TAIL_FILES) + "; "
+ "const _TEST_NAME=" + uneval(_TEST_NAME) + "; "
// We'll need more magic to get the debugger working in the child
+ "const _JSDEBUGGER_PORT=0; "
+ "const _XPCSHELL_PROCESS='child';";
if (this._TESTING_MODULES_DIR) {

View File

@ -65,6 +65,7 @@ class XPCShellRunner(MozbuildObject):
def run_test(self, test_paths, interactive=False,
keep_going=False, sequential=False, shuffle=False,
debugger=None, debuggerArgs=None, debuggerInteractive=None,
jsDebugger=False, jsDebuggerPort=None,
rerun_failures=False, test_objects=None, verbose=False,
log=None,
# ignore parameters from other platforms' options
@ -83,6 +84,7 @@ class XPCShellRunner(MozbuildObject):
keep_going=keep_going, shuffle=shuffle, sequential=sequential,
debugger=debugger, debuggerArgs=debuggerArgs,
debuggerInteractive=debuggerInteractive,
jsDebugger=jsDebugger, jsDebuggerPort=jsDebuggerPort,
rerun_failures=rerun_failures,
verbose=verbose, log=log)
return
@ -113,6 +115,8 @@ class XPCShellRunner(MozbuildObject):
'debugger': debugger,
'debuggerArgs': debuggerArgs,
'debuggerInteractive': debuggerInteractive,
'jsDebugger': jsDebugger,
'jsDebuggerPort': jsDebuggerPort,
'rerun_failures': rerun_failures,
'manifest': manifest,
'verbose': verbose,
@ -125,6 +129,7 @@ class XPCShellRunner(MozbuildObject):
test_path=None, shuffle=False, interactive=False,
keep_going=False, sequential=False,
debugger=None, debuggerArgs=None, debuggerInteractive=None,
jsDebugger=False, jsDebuggerPort=None,
rerun_failures=False, verbose=False, log=None):
# Obtain a reference to the xpcshell test runner.
@ -161,6 +166,8 @@ class XPCShellRunner(MozbuildObject):
'debugger': debugger,
'debuggerArgs': debuggerArgs,
'debuggerInteractive': debuggerInteractive,
'jsDebugger': jsDebugger,
'jsDebuggerPort': jsDebuggerPort,
}
if test_path is not None:
@ -417,6 +424,13 @@ class MachCommands(MachCommandBase):
dest = "debuggerInteractive",
help = "prevents the test harness from redirecting "
"stdout and stderr for interactive debuggers")
@CommandArgument("--jsdebugger", dest="jsDebugger", action="store_true",
help="Waits for a devtools JS debugger to connect before "
"starting the test.")
@CommandArgument("--jsdebugger-port", dest="jsDebuggerPort",
type=int, default=6000,
help="The port to listen on for a debugger connection if "
"--jsdebugger is specified (default=6000).")
@CommandArgument('--interactive', '-i', action='store_true',
help='Open an xpcshell prompt before running tests.')
@CommandArgument('--keep-going', '-k', action='store_true',

View File

@ -9,3 +9,7 @@ TEST_DIRS += ['example']
PYTHON_UNIT_TESTS += [
'selftest.py',
]
TESTING_JS_MODULES += [
'dbg-actors.js',
]

View File

@ -19,7 +19,7 @@ import sys
import time
import traceback
from collections import deque
from collections import deque, namedtuple
from distutils import dir_util
from multiprocessing import cpu_count
from optparse import OptionParser
@ -111,6 +111,7 @@ class XPCShellTestThread(Thread):
self.xrePath = kwargs.get('xrePath')
self.testingModulesDir = kwargs.get('testingModulesDir')
self.debuggerInfo = kwargs.get('debuggerInfo')
self.jsDebuggerInfo = kwargs.get('jsDebuggerInfo')
self.pluginsPath = kwargs.get('pluginsPath')
self.httpdManifest = kwargs.get('httpdManifest')
self.httpdJSPath = kwargs.get('httpdJSPath')
@ -366,10 +367,15 @@ class XPCShellTestThread(Thread):
for f in headfiles])
cmdT = ", ".join(['"' + f.replace('\\', '/') + '"'
for f in tailfiles])
dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port
return xpcscmd + \
['-e', 'const _SERVER_ADDR = "localhost"',
'-e', 'const _HEAD_FILES = [%s];' % cmdH,
'-e', 'const _TAIL_FILES = [%s];' % cmdT]
'-e', 'const _TAIL_FILES = [%s];' % cmdT,
'-e', 'const _JSDEBUGGER_PORT = %d;' % dbgport,
]
def getHeadAndTailFiles(self, test_object):
"""Obtain the list of head and tail files.
@ -632,7 +638,7 @@ class XPCShellTestThread(Thread):
testTimeoutInterval *= int(self.test_object['requesttimeoutfactor'])
testTimer = None
if not self.interactive and not self.debuggerInfo:
if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
testTimer.start()
@ -1004,7 +1010,8 @@ class XPCShellTests(object):
profileName=None, mozInfo=None, sequential=False, shuffle=False,
testsRootDir=None, testingModulesDir=None, pluginsPath=None,
testClass=XPCShellTestThread, failureManifest=None,
log=None, stream=None, **otherOptions):
log=None, stream=None, jsDebugger=False, jsDebuggerPort=0,
**otherOptions):
"""Run xpcshell tests.
|xpcshell|, is the xpcshell executable to use to run the tests.
@ -1075,6 +1082,12 @@ class XPCShellTests(object):
if debugger:
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debuggerArgs, debuggerInteractive)
self.jsDebuggerInfo = None
if jsDebugger:
# A namedtuple let's us keep .port instead of ['port']
JSDebuggerInfo = namedtuple('JSDebuggerInfo', ['port'])
self.jsDebuggerInfo = JSDebuggerInfo(port=jsDebuggerPort)
self.xpcshell = xpcshell
self.xrePath = xrePath
self.appPath = appPath
@ -1161,6 +1174,7 @@ class XPCShellTests(object):
'xrePath': self.xrePath,
'testingModulesDir': self.testingModulesDir,
'debuggerInfo': self.debuggerInfo,
'jsDebuggerInfo': self.jsDebuggerInfo,
'pluginsPath': self.pluginsPath,
'httpdManifest': self.httpdManifest,
'httpdJSPath': self.httpdJSPath,
@ -1190,6 +1204,13 @@ class XPCShellTests(object):
if self.debuggerInfo.interactive:
signal.signal(signal.SIGINT, lambda signum, frame: None)
if self.jsDebuggerInfo:
# The js debugger magic needs more work to do the right thing
# if debugging multiple files.
if len(self.alltests) != 1:
self.log.error("Error: --jsdebugger can only be used with a single test!")
return False
# create a queue of all tests that will run
tests_queue = deque()
# also a list for the tests that need to be run sequentially
@ -1434,6 +1455,13 @@ class XPCShellOptions(OptionParser):
action = "store_true", dest = "debuggerInteractive",
help = "prevents the test harness from redirecting "
"stdout and stderr for interactive debuggers")
self.add_option("--jsdebugger", dest="jsDebugger", action="store_true",
help="Waits for a devtools JS debugger to connect before "
"starting the test.")
self.add_option("--jsdebugger-port", type="int", dest="jsDebuggerPort",
default=6000,
help="The port to listen on for a debugger connection if "
"--jsdebugger is specified.")
def main():
parser = XPCShellOptions()

View File

@ -602,6 +602,9 @@ function ThreadActor(aParent, aGlobal)
this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
this.onNewScript = this.onNewScript.bind(this);
// Set a wrappedJSObject property so |this| can be sent via the observer svc
// for the xpcshell harness.
this.wrappedJSObject = this;
}
ThreadActor.prototype = {
@ -1178,6 +1181,11 @@ ThreadActor.prototype = {
let packet = this._resumed();
this._popThreadPause();
// Tell anyone who cares of the resume (as of now, that's the xpcshell
// harness)
if (Services.obs) {
Services.obs.notifyObservers(this, "devtools-thread-resumed", null);
}
return packet;
}, error => {
return error instanceof Error
@ -1322,7 +1330,7 @@ ThreadActor.prototype = {
for (let line = 0, n = offsets.length; line < n; line++) {
if (offsets[line]) {
let location = { line: line };
let resp = sourceActor._createAndStoreBreakpoint(location);
let resp = sourceActor.createAndStoreBreakpoint(location);
dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
if (resp.error) {
reportError(new Error("Unable to set breakpoint on event listener"));
@ -2516,11 +2524,10 @@ SourceActor.prototype = {
let sourceFetched = fetch(this.url, { loadFromCache: !this.source });
// Record the contentType we just learned during fetching
sourceFetched.then(({ contentType }) => {
this._contentType = contentType;
return sourceFetched.then(result => {
this._contentType = result.contentType;
return result;
});
return sourceFetched;
}
});
},
@ -2848,7 +2855,7 @@ SourceActor.prototype = {
_createBreakpoint: function(loc, originalLoc, condition) {
return resolve(null).then(() => {
return this._createAndStoreBreakpoint({
return this.createAndStoreBreakpoint({
line: loc.line,
column: loc.column,
condition: condition
@ -2915,12 +2922,14 @@ SourceActor.prototype = {
* Create a breakpoint at the specified location and store it in the
* cache. Takes ownership of `aRequest`. This is the
* generated location if this source is sourcemapped.
* Used by the XPCShell test harness to set breakpoints in a script before
* it has loaded.
*
* @param Object aRequest
* An object of the form { line[, column, condition] }. The
* location is in the generated source, if sourcemapped.
*/
_createAndStoreBreakpoint: function (aRequest) {
createAndStoreBreakpoint: function (aRequest) {
let bp = update({}, aRequest, { source: this.form() });
this.breakpointStore.addBreakpoint(bp);
return this._setBreakpoint(aRequest);

View File

@ -13,10 +13,19 @@ const { Cc, Ci } = require("chrome");
Object.defineProperty(this, "addonManager", {
get: (function () {
let cached;
return () => cached
? cached
: (cached = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.amIAddonManager))
return () => {
if (cached === undefined) {
// catch errors as the addonManager might not exist in this environment
// (eg, xpcshell)
try {
cached = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.amIAddonManager);
} catch (ex) {
cached = null;
}
}
return cached;
}
}())
});