Bug 809561 - Integrate xpcshell test harness with chrome remote debugging. r=past/chmanchester
authorMark Hammond <mhammond@skippinet.com.au>
Sat, 29 Nov 2014 10:40:58 +1100
changeset 243924 adc66033b9188de0c5e35697332cc048a77a11cf
parent 243923 6dededc47e3d31c6707f56303dedc87039356d9e
child 243925 1e024ddabbb3f7e4685c82347891923216875480
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, chmanchester
bugs809561
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 809561 - Integrate xpcshell test harness with chrome remote debugging. r=past/chmanchester
testing/xpcshell/dbg-actors.js
testing/xpcshell/head.js
testing/xpcshell/mach_commands.py
testing/xpcshell/moz.build
testing/xpcshell/runxpcshelltests.py
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js
new file mode 100644
--- /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. <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([]);
+};
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -331,17 +331,111 @@ function _register_modules_protocol_hand
                     _TESTING_MODULES_DIR);
   }
 
   let modulesURI = ios.newFileURI(modulesFile);
 
   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.
   // Call do_get_idle() to restore the factory and get the service.
   _fakeIdleService.activate();
 
   _Promise.Debugging.clearUncaughtErrorObservers();
   _Promise.Debugging.addUncaughtErrorObserver(function observer({message, date, fileName, stack, lineNumber}) {
@@ -1067,16 +1161,18 @@ function do_load_child_test_harness()
   _XPCSHELL_PROCESS = "parent";
 
   let command =
         "const _HEAD_JS_PATH=" + uneval(_HEAD_JS_PATH) + "; "
       + "const _HTTPD_JS_PATH=" + uneval(_HTTPD_JS_PATH) + "; "
       + "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) {
     command += " const _TESTING_MODULES_DIR=" + uneval(_TESTING_MODULES_DIR) + ";";
   }
 
   command += " load(_HEAD_JS_PATH);";
   sendCommand(command);
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -60,16 +60,17 @@ class XPCShellRunner(MozbuildObject):
         manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
             '_tests', 'xpcshell', 'xpcshell.ini')])
 
         return self._run_xpcshell_harness(manifest=manifest, **kwargs)
 
     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
                  **kwargs):
         """Runs an individual xpcshell test."""
         from mozbuild.testing import TestResolver
         from manifestparser import TestManifest
 
@@ -78,16 +79,17 @@ class XPCShellRunner(MozbuildObject):
         if build_path not in sys.path:
             sys.path.append(build_path)
 
         if test_paths == ['all']:
             self.run_suite(interactive=interactive,
                            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
         elif test_paths:
             test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
 
         if test_objects:
             tests = test_objects
@@ -108,28 +110,31 @@ class XPCShellRunner(MozbuildObject):
         args = {
             'interactive': interactive,
             'keep_going': keep_going,
             'shuffle': shuffle,
             'sequential': sequential,
             'debugger': debugger,
             'debuggerArgs': debuggerArgs,
             'debuggerInteractive': debuggerInteractive,
+            'jsDebugger': jsDebugger,
+            'jsDebuggerPort': jsDebuggerPort,
             'rerun_failures': rerun_failures,
             'manifest': manifest,
             'verbose': verbose,
             'log': log,
         }
 
         return self._run_xpcshell_harness(**args)
 
     def _run_xpcshell_harness(self, manifest,
                               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.
         import runxpcshelltests
 
         xpcshell = runxpcshelltests.XPCShellTests(log=log)
         self.log_manager.enable_unstructured()
 
@@ -156,16 +161,18 @@ class XPCShellRunner(MozbuildObject):
             'profileName': 'firefox',
             'verbose': verbose or single_test,
             'xunitFilename': os.path.join(self.statedir, 'xpchsell.xunit.xml'),
             'xunitName': 'xpcshell',
             'pluginsPath': os.path.join(self.distdir, 'plugins'),
             'debugger': debugger,
             'debuggerArgs': debuggerArgs,
             'debuggerInteractive': debuggerInteractive,
+            'jsDebugger': jsDebugger,
+            'jsDebuggerPort': jsDebuggerPort,
         }
 
         if test_path is not None:
             args['testPath'] = test_path
 
         # A failure manifest is written by default. If --rerun-failures is
         # specified and a prior failure manifest is found, the prior manifest
         # will be run. A new failure manifest is always written over any
@@ -412,16 +419,23 @@ class MachCommands(MachCommandBase):
     @CommandArgument("--debugger-args", default=None, metavar='ARGS', type=str,
                      dest = "debuggerArgs",
                      help = "pass the given args to the debugger _before_ "
                             "the application on the command line")
     @CommandArgument("--debugger-interactive", action = "store_true",
                      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',
         help='Continue running tests after a SIGINT is received.')
     @CommandArgument('--sequential', action='store_true',
         help='Run the tests sequentially.')
     @CommandArgument('--shuffle', '-s', action='store_true',
         help='Randomize the execution order of tests.')
--- a/testing/xpcshell/moz.build
+++ b/testing/xpcshell/moz.build
@@ -4,8 +4,12 @@
 # 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/.
 
 TEST_DIRS += ['example']
 
 PYTHON_UNIT_TESTS += [
     'selftest.py',
 ]
+
+TESTING_JS_MODULES += [
+    'dbg-actors.js',
+]
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -14,17 +14,17 @@ import os.path
 import random
 import re
 import shutil
 import signal
 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
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp, gettempdir
 from threading import (
     Timer,
     Thread,
@@ -106,16 +106,17 @@ class XPCShellTestThread(Thread):
         self.test_object = test_object
         self.cleanup_dir_list = cleanup_dir_list
         self.retry = retry
 
         self.appPath = kwargs.get('appPath')
         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')
         self.headJSPath = kwargs.get('headJSPath')
         self.testharnessdir = kwargs.get('testharnessdir')
         self.profileName = kwargs.get('profileName')
         self.singleFile = kwargs.get('singleFile')
         self.env = copy.deepcopy(kwargs.get('env'))
@@ -361,20 +362,25 @@ class XPCShellTestThread(Thread):
           along with the address of the webserver which some tests require.
 
           On a remote system, this is overloaded to resolve quoting issues over a secondary command line.
         """
         cmdH = ", ".join(['"' + f.replace('\\', '/') + '"'
                        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.
 
         Returns a 2-tuple. The first element is a list of head files. The second
         is a list of tail files.
         """
         def sanitize_list(s, kind):
@@ -627,17 +633,17 @@ class XPCShellTestThread(Thread):
             self.env['DMD_PRELOAD_VALUE'] = libdmd
 
         testTimeoutInterval = self.harness_timeout
         # Allow a test to request a multiple of the timeout if it is expected to take long
         if 'requesttimeoutfactor' in self.test_object:
             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()
 
         proc = None
         process_output = None
 
         try:
             self.log.test_start(name)
@@ -999,17 +1005,18 @@ class XPCShellTests(object):
     def runTests(self, xpcshell, xrePath=None, appPath=None, symbolsPath=None,
                  manifest=None, testdirs=None, testPath=None, mobileArgs=None,
                  interactive=False, verbose=False, keepGoing=False, logfiles=True,
                  thisChunk=1, totalChunks=1, debugger=None,
                  debuggerArgs=None, debuggerInteractive=False,
                  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.
         |xrePath|, if provided, is the path to the XRE to use.
         |appPath|, if provided, is the path to an application directory.
         |symbolsPath|, if provided is the path to a directory containing
           breakpad symbols for processing crashes in tests.
         |manifest|, if provided, is a file containing a list of
@@ -1070,16 +1077,22 @@ class XPCShellTests(object):
             if not testingModulesDir.endswith(os.path.sep):
                 testingModulesDir += os.path.sep
 
         self.debuggerInfo = None
 
         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
         self.symbolsPath = symbolsPath
         self.manifest = manifest
         self.testdirs = testdirs
         self.testPath = testPath
         self.interactive = interactive
@@ -1156,16 +1169,17 @@ class XPCShellTests(object):
         self.cleanup_dir_list = []
         self.try_again_list = []
 
         kwargs = {
             'appPath': self.appPath,
             'xrePath': self.xrePath,
             'testingModulesDir': self.testingModulesDir,
             'debuggerInfo': self.debuggerInfo,
+            'jsDebuggerInfo': self.jsDebuggerInfo,
             'pluginsPath': self.pluginsPath,
             'httpdManifest': self.httpdManifest,
             'httpdJSPath': self.httpdJSPath,
             'headJSPath': self.headJSPath,
             'testharnessdir': self.testharnessdir,
             'profileName': self.profileName,
             'singleFile': self.singleFile,
             'env': self.env, # making a copy of this in the testthreads
@@ -1185,16 +1199,23 @@ class XPCShellTests(object):
         if self.debuggerInfo:
             # Force a sequential run
             self.sequential = True
 
             # If we have an interactive debugger, disable SIGINT entirely.
             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
         sequential_tests = []
         for test_object in self.alltests:
             # Test identifiers are provided for the convenience of logging. These
             # start as path names but are rewritten in case tests from the same path
             # are re-run.
@@ -1429,16 +1450,23 @@ class XPCShellOptions(OptionParser):
         self.add_option("--debugger-args",
                         action = "store", dest = "debuggerArgs",
                         help = "pass the given args to the debugger _before_ "
                            "the application on the command line")
         self.add_option("--debugger-interactive",
                         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()
     structured.commandline.add_logging_group(parser)
     options, args = parser.parse_args()
 
 
     log = structured.commandline.setup_logging("XPCShell",
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -597,16 +597,19 @@ function ThreadActor(aParent, aGlobal)
   this.global = aGlobal;
 
   this._allEventsListener = this._allEventsListener.bind(this);
   this.onNewGlobal = this.onNewGlobal.bind(this);
   this.onNewSource = this.onNewSource.bind(this);
   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 = {
   // Used by the ObjectActor to keep track of the depth of grip() calls.
   _gripDepth: null,
 
   actorPrefix: "context",
 
@@ -1173,16 +1176,21 @@ ThreadActor.prototype = {
         this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
         this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
         this.maybePauseOnExceptions();
         this._maybeListenToEvents(aRequest);
       }
 
       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
         ? { error: "unknownError",
             message: DevToolsUtils.safeErrorString(error) }
         // It is a known error, and the promise was rejected with an error
         // packet.
         : error;
@@ -1317,17 +1325,17 @@ ThreadActor.prototype = {
    */
   _breakOnEnter: function(script) {
     let offsets = script.getAllOffsets();
     let sourceActor = this.sources.source({ source: script.source });
 
     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"));
           return;
         }
         let bp = this.breakpointStore.getBreakpoint({
           source: sourceActor.form(),
           line: location.line
@@ -2511,21 +2519,20 @@ SourceActor.prototype = {
       }
       else {
         // XXX bug 865252: Don't load from the cache if this is a source mapped
         // source because we can't guarantee that the cache has the most up to date
         // content for this source like we can if it isn't source mapped.
         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;
       }
     });
   },
 
   /**
    * Get all executable lines from the current source
    * @return Array - Executable lines of the current script
    **/
@@ -2843,17 +2850,17 @@ SourceActor.prototype = {
       else {
         return this._createBreakpoint(genLoc, originalLoc, aRequest.condition);
       }
     });
   },
 
   _createBreakpoint: function(loc, originalLoc, condition) {
     return resolve(null).then(() => {
-      return this._createAndStoreBreakpoint({
+      return this.createAndStoreBreakpoint({
         line: loc.line,
         column: loc.column,
         condition: condition
       });
     }).then(response => {
       var actual = response.actualLocation;
       if (actual) {
         if (this.source) {
@@ -2910,22 +2917,24 @@ SourceActor.prototype = {
       DevToolsUtils.reportException("onSetBreakpoint", error);
     });
   },
 
   /**
    * 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);
   },
 
   /** Get or create the BreakpointActor for the breakpoint at the given location.
    *
    * NB: This will override a pre-existing BreakpointActor's condition with
--- a/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js
+++ b/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js
@@ -8,20 +8,29 @@
 
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const Services = require("Services");
 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;
+    }
   }())
 });
 
 const B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
 
 /**
  * This is a wrapper around amIAddonManager.mapURIToAddonID which always returns
  * false on B2G to avoid loading the add-on manager there and reports any