Bug 892605 - part 2: add 'dbg blackbox' and 'dbg unblackbox' gcli commands; r=vporof
authorNick Fitzgerald <fitzgen@gmail.com>
Sat, 27 Jul 2013 10:50:57 -0700
changeset 140218 aa190a916dcc3a1dac3655558e87790be5320a87
parent 140217 893b7d8e7b175de977c8aeb0cdb7eb161a02521d
child 140219 ad20fa11511883f4db8cd7ad585dc3979a700a5a
push id1949
push usernfitzgerald@mozilla.com
push dateSat, 27 Jul 2013 17:51:10 +0000
treeherderfx-team@aa190a916dcc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs892605
milestone25.0a1
Bug 892605 - part 2: add 'dbg blackbox' and 'dbg unblackbox' gcli commands; r=vporof
browser/devtools/debugger/CmdDebugger.jsm
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_cmd_blackbox.js
browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
toolkit/devtools/client/dbg-client.jsm
--- a/browser/devtools/debugger/CmdDebugger.jsm
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -419,16 +419,132 @@ gcli.addCommand({
     });
     div.appendChild(ol);
 
     return div;
   }
 });
 
 /**
+ * Define the 'dbg blackbox' and 'dbg unblackbox' commands.
+ */
+[
+  {
+    name: "blackbox",
+    clientMethod: "blackBox",
+    l10nPrefix: "dbgBlackBox"
+  },
+  {
+    name: "unblackbox",
+    clientMethod: "unblackBox",
+    l10nPrefix: "dbgUnBlackBox"
+  }
+].forEach(function (cmd) {
+  const lookup = function (id) {
+    return gcli.lookup(cmd.l10nPrefix + id);
+  };
+
+  gcli.addCommand({
+    name: "dbg " + cmd.name,
+    description: lookup("Desc"),
+    params: [
+      {
+        name: "source",
+        type: {
+          name: "selection",
+          data: function (context) {
+            let dbg = getPanel(context, "jsdebugger");
+            return dbg
+              ? [s for (s of dbg._view.Sources.values)]
+              : [];
+          }
+        },
+        description: lookup("SourceDesc"),
+        defaultValue: null
+      },
+      {
+        name: "glob",
+        type: "string",
+        description: lookup("GlobDesc"),
+        defaultValue: null
+      }
+    ],
+    returnType: "dom",
+    exec: function (args, context) {
+      const dbg = getPanel(context, "jsdebugger");
+      const doc = context.environment.chromeDocument;
+      if (!dbg) {
+        throw new Error(gcli.lookup("debuggerClosed"));
+      }
+
+      const { promise, resolve, reject } = context.defer();
+      const { activeThread } = dbg._controller;
+      const globRegExp = args.glob
+        ? globToRegExp(args.glob)
+        : null;
+
+      // Filter the sources down to those that we will need to black box.
+
+      function shouldBlackBox(source) {
+        return globRegExp && globRegExp.test(source.url)
+          || args.source && source.url == args.source;
+      }
+
+      const toBlackBox = [s.attachment.source
+                          for (s of dbg._view.Sources.items)
+                          if (shouldBlackBox(s.attachment.source))];
+
+      // If we aren't black boxing any sources, bail out now.
+
+      if (toBlackBox.length === 0) {
+        const empty = createXHTMLElement(doc, "div");
+        empty.textContent = lookup("EmptyDesc");
+        return void resolve(empty);
+      }
+
+      // Send the black box request to each source we are black boxing. As we
+      // get responses, accumulate the results in `blackBoxed`.
+
+      const blackBoxed = [];
+
+      for (let source of toBlackBox) {
+        activeThread.source(source)[cmd.clientMethod](function ({ error }) {
+          if (error) {
+            blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
+          } else {
+            blackBoxed.push(source.url);
+          }
+
+          if (toBlackBox.length === blackBoxed.length) {
+            displayResults();
+          }
+        });
+      }
+
+      // List the results for the user.
+
+      function displayResults() {
+        const results = doc.createElement("div");
+        results.textContent = lookup("NonEmptyDesc");
+        const list = createXHTMLElement(doc, "ul");
+        results.appendChild(list);
+        for (let result of blackBoxed) {
+          const item = createXHTMLElement(doc, "li");
+          item.textContent = result;
+          list.appendChild(item);
+        }
+        resolve(results);
+      }
+
+      return promise;
+    }
+  });
+});
+
+/**
  * A helper to create xhtml namespaced elements
  */
 function createXHTMLElement(document, tagname) {
   return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
 }
 
 /**
  * A helper to go from a command context to a debugger panel
@@ -447,8 +563,37 @@ function getPanel(context, id, options =
     let toolbox = gDevTools.getToolbox(target);
     if (toolbox) {
       return toolbox.getPanel(id);
     } else {
       return undefined;
     }
   }
 }
+
+/**
+ * Converts a glob to a regular expression
+ */
+function globToRegExp(glob) {
+  const reStr = glob
+  // Escape existing regular expression syntax
+    .replace(/\\/g, "\\\\")
+    .replace(/\//g, "\\/")
+    .replace(/\^/g, "\\^")
+    .replace(/\$/g, "\\$")
+    .replace(/\+/g, "\\+")
+    .replace(/\?/g, "\\?")
+    .replace(/\./g, "\\.")
+    .replace(/\(/g, "\\(")
+    .replace(/\)/g, "\\)")
+    .replace(/\=/g, "\\=")
+    .replace(/\!/g, "\\!")
+    .replace(/\|/g, "\\|")
+    .replace(/\{/g, "\\{")
+    .replace(/\}/g, "\\}")
+    .replace(/\,/g, "\\,")
+    .replace(/\[/g, "\\[")
+    .replace(/\]/g, "\\]")
+    .replace(/\-/g, "\\-")
+  // Turn * into the match everything wildcard
+    .replace(/\*/g, ".*")
+  return new RegExp("^" + reStr + "$");
+}
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -885,44 +885,47 @@ StackFrames.prototype = {
 /**
  * Keeps the source script list up-to-date, using the thread client's
  * source script cache.
  */
 function SourceScripts() {
   this._onNewGlobal = this._onNewGlobal.bind(this);
   this._onNewSource = this._onNewSource.bind(this);
   this._onSourcesAdded = this._onSourcesAdded.bind(this);
+  this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
 }
 
 SourceScripts.prototype = {
   get activeThread() DebuggerController.activeThread,
   get debuggerClient() DebuggerController.client,
   _newSourceTimeout: null,
 
   /**
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("SourceScripts is connecting...");
     this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.addListener("newSource", this._onNewSource);
+    this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
     this._handleTabNavigation();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("SourceScripts is disconnecting...");
     window.clearTimeout(this._newSourceTimeout);
     this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.removeListener("newSource", this._onNewSource);
+    this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   _handleTabNavigation: function() {
     if (!this.activeThread) {
       return;
@@ -1021,16 +1024,26 @@ SourceScripts.prototype = {
     DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that scripts have been added.
     window.dispatchEvent(document, "Debugger:AfterSourcesAdded");
   },
 
   /**
+   * Handler for the debugger client's 'blackboxchange' notification.
+   */
+  _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
+    const item = DebuggerView.Sources.getItemByValue(url);
+    if (item) {
+      DebuggerView.Sources.callMethod("checkItem", item.target, !isBlackBoxed);
+    }
+  },
+
+  /**
    * Set the black boxed status of the given source.
    *
    * @param Object aSource
    *        The source form.
    * @param bool aBlackBoxFlag
    *        True to black box the source, false to un-black box it.
    */
   blackBox: function(aSource, aBlackBoxFlag) {
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -13,16 +13,17 @@ include $(DEPTH)/config/autoconf.mk
 MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_aaa_run_first_leaktest.js \
 	browser_dbg_blackboxing-01.js \
 	browser_dbg_blackboxing-02.js \
 	browser_dbg_blackboxing-03.js \
 	browser_dbg_blackboxing-04.js \
 	browser_dbg_clean-exit.js \
 	browser_dbg_cmd.js \
+	browser_dbg_cmd_blackbox.js \
 	browser_dbg_cmd_break.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs-01.js \
 	browser_dbg_listtabs-02.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_globalactor-01.js \
 	browser_dbg_nav-01.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd_blackbox.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the 'dbg blackbox' and 'dbg unblackbox' commands work as they
+// should.
+
+const TEST_URL = EXAMPLE_URL + "browser_dbg_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "blackboxing_blackboxme.js";
+const BLACKBOXONE_URL = EXAMPLE_URL + "blackboxing_one.js";
+const BLACKBOXTWO_URL = EXAMPLE_URL + "blackboxing_two.js";
+const BLACKBOXTHREE_URL = EXAMPLE_URL + "blackboxing_three.js";
+
+let gcli = Cu.import("resource://gre/modules/devtools/gcli.jsm", {}).gcli;
+
+let gTarget;
+let gPanel;
+let gOptions;
+let gDebugger;
+let gClient;
+let gThreadClient;
+let gTab;
+
+function cmd(typed, expectedNumEvents=1) {
+  const deferred = promise.defer();
+
+  let timesFired = 0;
+  gThreadClient.addListener("blackboxchange", function _onBlackBoxChange() {
+    if (++timesFired === expectedNumEvents) {
+      gThreadClient.removeListener("blackboxchange", _onBlackBoxChange);
+      deferred.resolve();
+    }
+  });
+
+  helpers.audit(gOptions, [{
+    setup: typed,
+    exec: {}
+  }]);
+
+  return deferred.promise;
+}
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URL, function(options) {
+    gOptions = options;
+    gTarget = options.target;
+    return gDevTools.showToolbox(options.target, "jsdebugger")
+      .then(setupGlobals)
+      .then(waitForDebuggerSources)
+      .then(testBlackBoxSource)
+      .then(testUnBlackBoxSource)
+      .then(testBlackBoxGlob)
+      .then(testUnBlackBoxGlob)
+      .then(null, function (error) {
+        ok(false, "Got an error: " + error.message + "\n" + error.stack);
+      })
+      .then(finishUp);
+  });
+}
+
+function setupGlobals(toolbox) {
+  gTab = gBrowser.selectedTab;
+  gPanel = toolbox.getCurrentPanel();
+  gDebugger = gPanel.panelWin;
+  gClient = gDebugger.gClient;
+  gThreadClient = gClient.activeThread;
+}
+
+function waitForDebuggerSources() {
+  const deferred = promise.defer();
+  gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown() {
+    gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown, false);
+    deferred.resolve();
+  }, false);
+  return deferred.promise;
+}
+
+function testBlackBoxSource() {
+  return cmd("dbg blackbox " + BLACKBOXME_URL)
+    .then(function () {
+      const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
+      ok(!checkbox.checked,
+         "Should be able to black box a specific source");
+    });
+}
+
+function testUnBlackBoxSource() {
+  return cmd("dbg unblackbox " + BLACKBOXME_URL)
+    .then(function () {
+      const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
+      ok(checkbox.checked,
+         "Should be able to stop black boxing a specific source");
+    });
+}
+
+function testBlackBoxGlob() {
+  return cmd("dbg blackbox --glob *blackboxing_t*.js", 2)
+    .then(function () {
+      ok(getBlackBoxCheckbox(BLACKBOXME_URL).checked,
+         "blackboxme should not be black boxed because it doesn't match the glob");
+      ok(getBlackBoxCheckbox(BLACKBOXONE_URL).checked,
+         "blackbox_one should not be black boxed because it doesn't match the glob");
+
+      ok(!getBlackBoxCheckbox(BLACKBOXTWO_URL).checked,
+         "blackbox_two should be black boxed because it matches the glob");
+      ok(!getBlackBoxCheckbox(BLACKBOXTHREE_URL).checked,
+         "blackbox_three should be black boxed because it matches the glob");
+    });
+}
+
+function testUnBlackBoxGlob() {
+  return cmd("dbg unblackbox --glob *blackboxing_t*.js", 2)
+    .then(function () {
+      ok(getBlackBoxCheckbox(BLACKBOXTWO_URL).checked,
+         "blackbox_two should be un-black boxed because it matches the glob");
+      ok(getBlackBoxCheckbox(BLACKBOXTHREE_URL).checked,
+         "blackbox_three should be un-black boxed because it matches the glob");
+    });
+}
+
+function finishUp() {
+  gTarget = null;
+  gPanel = null;
+  gOptions = null;
+  gClient = null;
+  gThreadClient = null;
+  gDebugger = null;
+  closeDebuggerAndFinish();
+}
+
+function getBlackBoxCheckbox(url) {
+  return gDebugger.document.querySelector(
+    ".side-menu-widget-item[tooltiptext=\""
+      + url + "\"] .side-menu-widget-item-checkbox");
+}
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -410,16 +410,66 @@ dbgStepInDesc=Executes the current state
 # LOCALIZATION NOTE (dbgStepOutDesc) A very short string used to describe the
 # function of the dbg step out command.
 dbgStepOutDesc=Steps out of the current function and up one level if the function is nested. If in the main body, the script is executed to the end, or to the next breakpoint. The skipped statements are executed, but not stepped through
 
 # LOCALIZATION NOTE (dbgListSourcesDesc) A very short string used to describe the
 # function of the dbg list command.
 dbgListSourcesDesc=List the source URLs loaded in the debugger
 
+# LOCALIZATION NOTE (dbgBlackBoxDesc) A very short string used to describe the
+# function of the 'dbg blackbox' command.
+dbgBlackBoxDesc=Black box sources in the debugger
+
+# LOCALIZATION NOTE (dbgBlackBoxSourceDesc) A very short string used to describe the
+# 'source' parameter to the 'dbg blackbox' command.
+dbgBlackBoxSourceDesc=A specific source to black box
+
+# LOCALIZATION NOTE (dbgBlackBoxGlobDesc) A very short string used to describe the
+# 'glob' parameter to the 'dbg blackbox' command.
+dbgBlackBoxGlobDesc=Black box all sources that match this glob (for example: "*.min.js")
+
+# LOCALIZATION NOTE (dbgBlackBoxEmptyDesc) A very short string used to let the
+# user know that no sources were black boxed.
+dbgBlackBoxEmptyDesc=(No sources black boxed)
+
+# LOCALIZATION NOTE (dbgBlackBoxNonEmptyDesc) A very short string used to let the
+# user know which sources were black boxed.
+dbgBlackBoxNonEmptyDesc=The following sources were black boxed:
+
+# LOCALIZATION NOTE (dbgBlackBoxErrorDesc) A very short string used to let the
+# user know there was an error black boxing a source (whose url follows this
+# text).
+dbgBlackBoxErrorDesc=Error black boxing:
+
+# LOCALIZATION NOTE (dbgUnBlackBoxDesc) A very short string used to describe the
+# function of the 'dbg unblackbox' command.
+dbgUnBlackBoxDesc=Stop black boxing sources in the debugger
+
+# LOCALIZATION NOTE (dbgUnBlackBoxSourceDesc) A very short string used to describe the
+# 'source' parameter to the 'dbg unblackbox' command.
+dbgUnBlackBoxSourceDesc=A specific source to stop black boxing
+
+# LOCALIZATION NOTE (dbgUnBlackBoxGlobDesc) A very short string used to describe the
+# 'glob' parameter to the 'dbg blackbox' command.
+dbgUnBlackBoxGlobDesc=Stop black boxing all sources that match this glob (for example: "*.min.js")
+
+# LOCALIZATION NOTE (dbgUnBlackBoxEmptyDesc) A very short string used to let the
+# user know that we did not stop black boxing any sources.
+dbgUnBlackBoxEmptyDesc=(Did not stop black boxing any sources)
+
+# LOCALIZATION NOTE (dbgUnBlackBoxNonEmptyDesc) A very short string used to let the
+# user know which sources we stopped black boxing.
+dbgUnBlackBoxNonEmptyDesc=Stopped black boxing the following sources:
+
+# LOCALIZATION NOTE (dbgUnBlackBoxErrorDesc) A very short string used to let the
+# user know there was an error black boxing a source (whose url follows this
+# text).
+dbgUnBlackBoxErrorDesc=Error stopping black boxing:
+
 # LOCALIZATION NOTE (consolecloseDesc) A very short description of the
 # 'console close' command. This string is designed to be shown in a menu
 # alongside the command name, which is why it should be as short as possible.
 consolecloseDesc=Close the console
 
 # LOCALIZATION NOTE (consoleopenDesc) A very short description of the
 # 'console open' command. This string is designed to be shown in a menu
 # alongside the command name, which is why it should be as short as possible.
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1756,16 +1756,17 @@ function SourceClient(aClient, aForm) {
 }
 
 SourceClient.prototype = {
   get _transport() this._client._transport,
   get _activeThread() this._client.activeThread,
   get isBlackBoxed() this._isBlackBoxed,
   get actor() this._form.actor,
   get request() this._client.request,
+  get url() this._form.url,
 
   /**
    * Black box this SourceClient's source.
    *
    * @param aCallback Function
    *        The callback function called when we receive the response from the server.
    */
   blackBox: DebuggerClient.requester({