mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 809561 - Integrate xpcshell test harness with chrome remote debugging. r=past/chmanchester
This commit is contained in:
parent
757831e55e
commit
759ac2fdd1
51
testing/xpcshell/dbg-actors.js
Normal file
51
testing/xpcshell/dbg-actors.js
Normal 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([]);
|
||||
};
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -9,3 +9,7 @@ TEST_DIRS += ['example']
|
||||
PYTHON_UNIT_TESTS += [
|
||||
'selftest.py',
|
||||
]
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'dbg-actors.js',
|
||||
]
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}())
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user