From 759ac2fdd10789827036af7de00d980474b7466f Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Sat, 29 Nov 2014 10:40:58 +1100 Subject: [PATCH] Bug 809561 - Integrate xpcshell test harness with chrome remote debugging. r=past/chmanchester --- testing/xpcshell/dbg-actors.js | 51 ++++++++++ testing/xpcshell/head.js | 96 +++++++++++++++++++ testing/xpcshell/mach_commands.py | 14 +++ testing/xpcshell/moz.build | 4 + testing/xpcshell/runxpcshelltests.py | 36 ++++++- toolkit/devtools/server/actors/script.js | 23 +++-- .../actors/utils/map-uri-to-addon-id.js | 17 +++- 7 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 testing/xpcshell/dbg-actors.js diff --git a/testing/xpcshell/dbg-actors.js b/testing/xpcshell/dbg-actors.js new file mode 100644 index 00000000000..e369e1fbae2 --- /dev/null +++ b/testing/xpcshell/dbg-actors.js @@ -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. :) + */ +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([]); +}; diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index f4c394e5251..f90909bb712 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -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 . + 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) { diff --git a/testing/xpcshell/mach_commands.py b/testing/xpcshell/mach_commands.py index 8ce5944a66e..e5f9989b918 100644 --- a/testing/xpcshell/mach_commands.py +++ b/testing/xpcshell/mach_commands.py @@ -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', diff --git a/testing/xpcshell/moz.build b/testing/xpcshell/moz.build index aeaedbc4dd4..42a6e75e3f3 100644 --- a/testing/xpcshell/moz.build +++ b/testing/xpcshell/moz.build @@ -9,3 +9,7 @@ TEST_DIRS += ['example'] PYTHON_UNIT_TESTS += [ 'selftest.py', ] + +TESTING_JS_MODULES += [ + 'dbg-actors.js', +] diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index be5df7a311b..610938f6c02 100755 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -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() diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index 85f9bd733dc..35e075de297 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -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); diff --git a/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js b/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js index 04cf195ea63..bf0fa4e0ca0 100644 --- a/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js +++ b/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js @@ -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; + } }()) });