Bug 1514248 - Reimplement xpcshell test debugging using a public thread-actor API. r=jlast
authorLogan Smyth <loganfsmyth@gmail.com>
Fri, 14 Dec 2018 17:33:45 +0000
changeset 507654 72065a75fc01b20e47f4d6f1ad66b85200b6c04f
parent 507653 8e57cb8a87e329f177153bdd9f292e0d59380ba3
child 507655 d02d14a3dd6e172c4cc8efb5c749752d3893fc90
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1514248
milestone66.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 1514248 - Reimplement xpcshell test debugging using a public thread-actor API. r=jlast Differential Revision: https://phabricator.services.mozilla.com/D14561
devtools/server/actors/source.js
devtools/server/actors/thread.js
testing/xpcshell/head.js
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -558,18 +558,18 @@ const SourceActor = ActorClassWithSpec(s
    *        A condition which must be true for breakpoint to be hit.
    * @param Boolean noSliding
    *        If true, disables breakpoint sliding.
    *
    * @returns Promise
    *          A promise that resolves to a JSON object representing the
    *          response.
    */
-  setBreakpoint: function(line, column, condition, noSliding) {
-    if (this.threadActor.state !== "paused") {
+  setBreakpoint: function(line, column, condition, noSliding, inNestedLoop) {
+    if (!inNestedLoop && this.threadActor.state !== "paused") {
       const errorObject = {
         error: "wrongState",
         message: "Cannot set breakpoint while debuggee is running.",
       };
       throw errorObject;
     }
 
     const location = new OriginalLocation(this, line, column);
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -75,35 +75,42 @@ const ThreadActor = ActorClassWithSpec(t
     this.sourceActorStore = new SourceActorStore();
 
     this._debuggerSourcesSeen = null;
 
     // A map of actorID -> actor for breakpoints created and managed by the
     // server.
     this._hiddenBreakpoints = new Map();
 
+    // A Set of URLs string to watch for when new sources are found by
+    // the debugger instance.
+    this._onLoadBreakpointURLs = new Set();
+
     this.global = global;
 
     this._allEventsListener = this._allEventsListener.bind(this);
     this.onNewSourceEvent = this.onNewSourceEvent.bind(this);
     this.onUpdatedSourceEvent = this.onUpdatedSourceEvent.bind(this);
 
     this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
     this.createCompletionGrip = this.createCompletionGrip.bind(this);
     this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
     this.onNewScript = this.onNewScript.bind(this);
     this.objectGrip = this.objectGrip.bind(this);
     this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
     this._onWindowReady = this._onWindowReady.bind(this);
     this._onOpeningRequest = this._onOpeningRequest.bind(this);
     EventEmitter.on(this._parent, "window-ready", this._onWindowReady);
 
-    // Set a wrappedJSObject property so |this| can be sent via the observer svc
-    // for the xpcshell harness.
-    this.wrappedJSObject = this;
+    if (Services.obs) {
+      // Set a wrappedJSObject property so |this| can be sent via the observer svc
+      // for the xpcshell harness.
+      this.wrappedJSObject = this;
+      Services.obs.notifyObservers(this, "devtools-thread-instantiated");
+    }
   },
 
   // Used by the ObjectActor to keep track of the depth of grip() calls.
   _gripDepth: null,
 
   get dbg() {
     if (!this._dbg) {
       this._dbg = this._parent.makeDebugger();
@@ -301,16 +308,27 @@ const ThreadActor = ActorClassWithSpec(t
       // now.
       return null;
     } catch (e) {
       reportError(e);
       return { error: "notAttached", message: e.toString() };
     }
   },
 
+  /**
+   * Tell the thread to automatically add a breakpoint on the first line of
+   * a given file, when it is first loaded.
+   *
+   * This is currently only used by the xpcshell test harness, and unless
+   * we decide to expand the scope of this feature, we should keep it that way.
+   */
+  setBreakpointOnLoad(urls) {
+    this._onLoadBreakpointURLs = new Set(urls);
+  },
+
   _findXHRBreakpointIndex(p, m) {
     return this._xhrBreakpoints.findIndex(
       ({ path, method }) => path === p && method === m);
   },
 
   removeXHRBreakpoint: function(path, method) {
     const index = this._findXHRBreakpointIndex(path, method);
 
@@ -2012,16 +2030,22 @@ const ThreadActor = ActorClassWithSpec(t
         } else {
           actor.originalLocation.originalSourceActor._setBreakpointAtGeneratedLocation(
             actor, GeneratedLocation.fromOriginalLocation(actor.originalLocation)
           );
         }
       }
     }
 
+    if (this._onLoadBreakpointURLs.has(source.url)) {
+      this.unsafeSynchronize(
+        sourceActor.setBreakpoint(1, undefined, undefined, undefined, true)
+      );
+    }
+
     this._debuggerSourcesSeen.add(source);
     return true;
   },
 
   /**
    * Get prototypes and properties of multiple objects.
    */
   onPrototypesAndProperties: function(request) {
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -392,48 +392,32 @@ function _setupDebuggerServer(breakpoint
                     "  firefox-appdir = browser\n" +
                     "to the xpcshell.ini manifest.\n" +
                     "It is possible for this to alter test behevior by " +
                     "triggering additional browser code to run, so check " +
                     "test behavior after making this change.\n" +
                     "See also https://bugzil.la/1215378.");
   }
   let { DebuggerServer } = require("devtools/server/main");
-  let { OriginalLocation } = require("devtools/server/actors/common");
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
   let { createRootActor } = require("resource://testing-common/dbg-actors.js");
   DebuggerServer.setRootActor(createRootActor);
   DebuggerServer.allowChromeProcess = true;
 
   // An observer notification that tells us when we can "resume" script
   // execution.
-  const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
+  const TOPICS = ["devtools-thread-instantiated", "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;
-          for (let file of breakpointFiles) {
-            // Pass an empty `source` object to workaround `source` function assertion
-            let sourceActor = threadActor.sources.source({originalUrl: file, source: {}});
-            sourceActor._getOrCreateBreakpointActor(new OriginalLocation(sourceActor, 1));
-          }
-        } catch (ex) {
-          info("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;
+    if (topic === "devtools-thread-instantiated") {
+      const threadActor = subject.wrappedJSObject;
+      threadActor.setBreakpointOnLoad(breakpointFiles);
+      return;
     }
+
     for (let topicToRemove of TOPICS) {
       _Services.obs.removeObserver(observe, topicToRemove);
     }
     callback();
   };
 
   for (let topic of TOPICS) {
     _Services.obs.addObserver(observe, topic);