Bug 860035 - script actors should handle breakpoints with columns; r=jimb
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 24 Jul 2013 17:46:49 -0700
changeset 152244 00b996131f8cfacdb2c9ecae5db0b21e7df11400
parent 152243 d2e44492e8a331e671b73832a8968493ff277125
child 152245 5899a4649b35c36d99e47e246c907f5951948dcf
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs860035
milestone25.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 860035 - script actors should handle breakpoints with columns; r=jimb
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_location-changes-bp.js
browser/devtools/debugger/test/browser_dbg_source_maps-01.js
browser/devtools/debugger/test/browser_dbg_source_maps-02.js
browser/devtools/debugger/test/browser_dbg_source_maps-03.js
browser/devtools/debugger/test/math.js
browser/devtools/debugger/test/math.map
browser/devtools/debugger/test/math.min.js
browser/devtools/debugger/test/minified.html
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/tests/unit/test_blackboxing-03.js
toolkit/devtools/server/tests/unit/test_blackboxing-05.js
toolkit/devtools/server/tests/unit/test_breakpoint-01.js
toolkit/devtools/server/tests/unit/test_breakpoint-03.js
toolkit/devtools/server/tests/unit/test_breakpoint-15.js
toolkit/devtools/server/tests/unit/test_breakpoint-16.js
toolkit/devtools/server/tests/unit/test_breakpoint-17.js
toolkit/devtools/server/tests/unit/test_sourcemaps-09.js
toolkit/devtools/server/tests/unit/test_stepping-06.js
toolkit/devtools/server/tests/unit/xpcshell.ini
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -104,16 +104,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_pause-exceptions-reload.js \
 	browser_dbg_multiple-windows.js \
 	browser_dbg_iframes.js \
 	browser_dbg_bfcache.js \
 	browser_dbg_progress-listener-bug.js \
 	browser_dbg_chrome-debugging.js \
 	browser_dbg_source_maps-01.js \
 	browser_dbg_source_maps-02.js \
+	browser_dbg_source_maps-03.js \
 	browser_dbg_step-out.js \
 	browser_dbg_event-listeners.js \
 	head.js \
 	$(NULL)
 
 MOCHITEST_BROWSER_PAGES = \
 	browser_dbg_blackboxing.html \
 	blackboxing_blackboxme.js \
@@ -146,16 +147,20 @@ MOCHITEST_BROWSER_PAGES = \
 	browser_dbg_function-search-02.html \
 	test-function-search-01.js \
 	test-function-search-02.js \
 	test-function-search-03.js \
 	binary_search.html \
 	binary_search.coffee \
 	binary_search.js \
 	binary_search.map \
+	math.js \
+	math.min.js \
+	math.map \
+	minified.html \
 	test-location-changes-bp.js \
 	test-location-changes-bp.html \
 	test-step-out.html \
 	test-pause-exceptions-reload.html \
 	test-event-listeners.html \
 	$(NULL)
 
 # Bug 888811 & bug 891176:
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-bp.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-bp.js
@@ -108,19 +108,19 @@ function testReloadPage()
 
 function clickAgain()
 {
   if (!sourcesShown || !tabNavigated) {
     return;
   }
 
   let controller = gDebugger.DebuggerController;
-  controller.activeThread.addOneTimeListener("framesadded", function() {
-    is(gDebugger.DebuggerController.activeThread.state, "paused",
-      "The breakpoint was hit.");
+  controller.activeThread.addOneTimeListener("paused", function(aEvent, aPacket) {
+    is(aPacket.why.type, "breakpoint",
+       "The breakpoint was hit.");
 
     let thread = gDebugger.DebuggerController.activeThread;
     thread.addOneTimeListener("paused", function test(aEvent, aPacket) {
       thread.addOneTimeListener("resumed", function() {
         executeSoon(closeDebuggerAndFinish);
       });
 
       is(aPacket.why.type, "debuggerStatement", "Execution has advanced to the next line.");
--- a/browser/devtools/debugger/test/browser_dbg_source_maps-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_source_maps-01.js
@@ -15,46 +15,48 @@ var gDebugger = null;
 
 function test()
 {
   let scriptShown = false;
   let framesAdded = false;
   let resumed = false;
   let testStarted = false;
 
-  Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", true);
-
-  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
-    resumed = true;
-    gTab = aTab;
-    gDebuggee = aDebuggee;
-    gPane = aPane;
-    gDebugger = gPane.panelWin;
+  SpecialPowers.pushPrefEnv({"set": [["devtools.debugger.source-maps-enabled", true]]}, () => {
+    debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+      resumed = true;
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPane = aPane;
+      gDebugger = gPane.panelWin;
 
-    gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
-      gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
-      ok(aEvent.detail.url.indexOf(".coffee") != -1,
-         "The debugger should show the source mapped coffee script file.");
-      ok(gDebugger.editor.getText().search(/isnt/) != -1,
-         "The debugger's editor should have the coffee script source displayed.");
+      gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+        gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+        ok(aEvent.detail.url.indexOf(".coffee") != -1,
+           "The debugger should show the source mapped coffee script file.");
+        ok(gDebugger.editor.getText().search(/isnt/) != -1,
+           "The debugger's editor should have the coffee script source displayed.");
 
-      testSetBreakpoint();
+        testSetBreakpoint();
+      });
     });
   });
 }
 
 function testSetBreakpoint() {
   let { activeThread } = gDebugger.DebuggerController;
   activeThread.interrupt(function (aResponse) {
     activeThread.setBreakpoint({
       url: EXAMPLE_URL + "binary_search.coffee",
       line: 5
     }, function (aResponse, bpClient) {
       ok(!aResponse.error,
          "Should be able to set a breakpoint in a coffee script file.");
+      ok(!aResponse.actualLocation,
+         "Should be able to set a breakpoint on line 5.");
       testSetBreakpointBlankLine();
     });
   });
 }
 
 function testSetBreakpointBlankLine() {
   let { activeThread } = gDebugger.DebuggerController;
   activeThread.setBreakpoint({
@@ -142,15 +144,14 @@ function waitForCaretPos(number, callbac
     }
     // We got the source editor at the expected line, it's safe to callback.
     window.clearInterval(intervalID);
     callback();
   }, 100);
 }
 
 registerCleanupFunction(function() {
-  Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebuggee = null;
   gDebugger = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_source_maps-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_source_maps-02.js
@@ -17,45 +17,45 @@ function test()
 {
   let scriptShown = false;
   let framesAdded = false;
   let resumed = false;
   let testStarted = false;
 
   gPrevPref = Services.prefs.getBoolPref(
     "devtools.debugger.source-maps-enabled");
-  Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", true);
-
-  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
-    resumed = true;
-    gTab = aTab;
-    gDebuggee = aDebuggee;
-    gPane = aPane;
-    gDebugger = gPane.panelWin;
+  SpecialPowers.pushPrefEnv({"set": [["devtools.debugger.source-maps-enabled", true]]}, () => {
+    debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+      resumed = true;
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPane = aPane;
+      gDebugger = gPane.panelWin;
 
-    gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
-      gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
-      // Show original sources should be already enabled.
-      is(gPrevPref, false,
-        "The source maps functionality should be disabled by default.");
-      is(gDebugger.Prefs.sourceMapsEnabled, true,
-        "The source maps pref should be true from startup.");
-      is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"),
-         "true", "Source maps should be enabled from startup. ")
+      gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+        gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+        // Show original sources should be already enabled.
+        is(gPrevPref, true,
+          "The source maps functionality should be enabled by default.");
+        is(gDebugger.Prefs.sourceMapsEnabled, true,
+          "The source maps pref should be true from startup.");
+        is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"),
+           "true", "Source maps should be enabled from startup. ")
 
-      ok(aEvent.detail.url.indexOf(".coffee") != -1,
-         "The debugger should show the source mapped coffee script file.");
-      ok(aEvent.detail.url.indexOf(".js") == -1,
-         "The debugger should not show the generated js script file.");
-      ok(gDebugger.editor.getText().search(/isnt/) != -1,
-         "The debugger's editor should have the coffee script source displayed.");
-      ok(gDebugger.editor.getText().search(/function/) == -1,
-         "The debugger's editor should not have the JS source displayed.");
+        ok(aEvent.detail.url.indexOf(".coffee") != -1,
+           "The debugger should show the source mapped coffee script file.");
+        ok(aEvent.detail.url.indexOf(".js") == -1,
+           "The debugger should not show the generated js script file.");
+        ok(gDebugger.editor.getText().search(/isnt/) != -1,
+           "The debugger's editor should have the coffee script source displayed.");
+        ok(gDebugger.editor.getText().search(/function/) == -1,
+           "The debugger's editor should not have the JS source displayed.");
 
-      testToggleGeneratedSource();
+        testToggleGeneratedSource();
+      });
     });
   });
 }
 
 function testToggleGeneratedSource() {
   gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
     gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
 
@@ -190,16 +190,15 @@ function waitForCaretPos(number, callbac
        "The right line is focused.")
     // We got the source editor at the expected line, it's safe to callback.
     window.clearInterval(intervalID);
     callback();
   }, 100);
 }
 
 registerCleanupFunction(function() {
-  Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebuggee = null;
   gDebugger = null;
   gPrevPref = null;
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_source_maps-03.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can debug minified javascript with source maps.
+ */
+
+const TAB_URL = EXAMPLE_URL + "minified.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gClient = null;
+
+let sourceShown = false;
+let hitDebuggerStatement = false;
+
+function test()
+{
+  SpecialPowers.pushPrefEnv({"set": [["devtools.debugger.source-maps-enabled", true]]}, () => {
+    debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPane = aPane;
+      gDebugger = gPane.panelWin;
+      gClient = gDebugger.DebuggerController.client;
+
+      testMinJSAndSourceMaps();
+    });
+  });
+}
+
+function testMinJSAndSourceMaps() {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) {
+    gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown);
+    sourceShown = true;
+
+    ok(aEvent.detail.url.indexOf("math.min.js") == -1,
+       "The debugger should not show the minified js");
+    ok(aEvent.detail.url.indexOf("math.js") != -1,
+       "The debugger should show the original js");
+    ok(gDebugger.editor.getText().split("\n").length > 10,
+       "The debugger's editor should have the original source displayed, " +
+       "not the whitespace stripped minified version");
+
+    startTest();
+  });
+
+  gClient.addListener("paused", function _onPaused(aEvent, aPacket) {
+    if (aPacket.type === "paused" && aPacket.why.type === "breakpoint") {
+      gClient.removeListener("paused", _onPaused);
+      hitDebuggerStatement = true;
+
+      startTest();
+    }
+  });
+
+  gClient.activeThread.interrupt(function (aResponse) {
+    ok(!aResponse.error, "Shouldn't be an error interrupting.");
+
+    gClient.activeThread.setBreakpoint({
+      url: EXAMPLE_URL + "math.js",
+      line: 30,
+      column: 10
+    }, function (aResponse) {
+      ok(!aResponse.error, "Shouldn't be an error setting a breakpoint.");
+      ok(!aResponse.actualLocation, "Shouldn't be an actualLocation.");
+
+      gClient.activeThread.resume(function (aResponse) {
+        ok(!aResponse.error, "There shouldn't be an error resuming.");
+        gDebuggee.arithmetic();
+      });
+    });
+  });
+}
+
+function startTest() {
+  if (sourceShown && hitDebuggerStatement) {
+    testCaretPosition();
+  }
+}
+
+function testCaretPosition() {
+  waitForCaretPos(29, function () {
+    closeDebuggerAndFinish();
+  });
+}
+
+function waitForCaretPos(number, callback)
+{
+  // Poll every few milliseconds until the source editor line is active.
+  let count = 0;
+  let intervalID = window.setInterval(function() {
+    info("count: " + count + ", caret at " + gDebugger.DebuggerView.editor.getCaretPosition().line);
+    if (++count > 50) {
+      ok(false, "Timed out while polling for the line.");
+      window.clearInterval(intervalID);
+      return closeDebuggerAndFinish();
+    }
+    if (gDebugger.DebuggerView.editor.getCaretPosition().line != number) {
+      return;
+    }
+    // We got the source editor at the expected line, it's safe to callback.
+    window.clearInterval(intervalID);
+    callback();
+  }, 100);
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebuggee = null;
+  gDebugger = null;
+  gClient = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/math.js
@@ -0,0 +1,45 @@
+function add(a, b, k) {
+  var result = a + b;
+  return k(result);
+}
+
+function sub(a, b, k) {
+  var result = a - b;
+  return k(result);
+}
+
+function mul(a, b, k) {
+  var result = a * b;
+  return k(result);
+}
+
+function div(a, b, k) {
+  var result = a / b;
+  return k(result);
+}
+
+function arithmetic() {
+  add(4, 4, function (a) {
+    // 8
+    sub(a, 2, function (b) {
+      // 6
+      mul(b, 3, function (c) {
+        // 18
+        div(c, 2, function (d) {
+          // 9
+          console.log(d);
+        });
+      });
+    });
+  });
+}
+
+// Compile with closure compiler and the following flags:
+//
+//     --compilation_level WHITESPACE_ONLY
+//     --source_map_format V3
+//     --create_source_map math.map
+//     --js_output_file    math.min.js
+//
+// And then append the sourceMappingURL comment directive to math.min.js
+// manually.
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/math.map
@@ -0,0 +1,8 @@
+{
+"version":3,
+"file":"math.min.js",
+"lineCount":1,
+"mappings":"AAAAA,QAASA,IAAG,CAACC,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBC,QAASA,IAAG,CAACJ,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBE,QAASA,IAAG,CAACL,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBG,QAASA,IAAG,CAACN,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBI,QAASA,WAAU,EAAG,CACpBR,GAAA,CAAI,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACO,CAAD,CAAI,CAErBF,GAAA,CAAIE,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBC,OAAAC,IAAA,CAAYF,CAAZ,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CADoB;",
+"sources":["math.js"],
+"names":["add","a","b","k","result","sub","mul","div","arithmetic","c","d","console","log"]
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/math.min.js
@@ -0,0 +1,2 @@
+function add(a,b,k){var result=a+b;return k(result)}function sub(a,b,k){var result=a-b;return k(result)}function mul(a,b,k){var result=a*b;return k(result)}function div(a,b,k){var result=a/b;return k(result)}function arithmetic(){add(4,4,function(a){sub(a,2,function(b){mul(b,3,function(c){div(c,2,function(d){console.log(d)})})})})};
+//@ sourceMappingURL=math.map
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/minified.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <script src="math.min.js"></script>
+  </body>
+</html>
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -297,17 +297,24 @@ DebuggerClient.requester = function DC_r
         if (!aResponse.from) {
           aResponse.from = from;
         }
       }
 
       // The callback is always the last parameter.
       let thisCallback = args[maxPosition + 1];
       if (thisCallback) {
-        thisCallback(aResponse);
+        try {
+          thisCallback(aResponse);
+        } catch (e) {
+          let msg = "Error executing callback passed to debugger client: "
+            + e + "\n" + e.stack;
+          dumpn(msg);
+          Cu.reportError(msg);
+        }
       }
 
       if (histogram) {
         histogram.add(+new Date - startTime);
       }
     }.bind(this));
 
   };
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2,16 +2,245 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 /**
+ * BreakpointStore objects keep track of all breakpoints that get set so that we
+ * can reset them when the same script is introduced to the thread again (such
+ * as after a refresh).
+ */
+function BreakpointStore() {
+  // If we have a whole-line breakpoint set at LINE in URL, then
+  //
+  //   this._wholeLineBreakpoints[URL][LINE]
+  //
+  // is an object
+  //
+  //   { url, line[, actor] }
+  //
+  // where the `actor` property is optional.
+  this._wholeLineBreakpoints = Object.create(null);
+
+  // If we have a breakpoint set at LINE, COLUMN in URL, then
+  //
+  //   this._breakpoints[URL][LINE][COLUMN]
+  //
+  // is an object
+  //
+  //   { url, line[, actor] }
+  //
+  // where the `actor` property is optional.
+  this._breakpoints = Object.create(null);
+}
+
+BreakpointStore.prototype = {
+
+  /**
+   * Add a breakpoint to the breakpoint store.
+   *
+   * @param Object aBreakpoint
+   *        The breakpoint to be added (not copied). It is an object with the
+   *        following properties:
+   *          - url
+   *          - line
+   *          - column (optional; omission implies that the breakpoint is for
+   *            the whole line)
+   *          - actor (optional)
+   */
+  addBreakpoint: function BS_addBreakpoint(aBreakpoint) {
+    let { url, line, column } = aBreakpoint;
+
+    if (column != null) {
+      if (!this._breakpoints[url]) {
+        this._breakpoints[url] = [];
+      }
+      if (!this._breakpoints[url][line]) {
+        this._breakpoints[url][line] = [];
+      }
+      this._breakpoints[url][line][column] = aBreakpoint;
+    } else {
+      // Add a breakpoint that breaks on the whole line.
+      if (!this._wholeLineBreakpoints[url]) {
+        this._wholeLineBreakpoints[url] = [];
+      }
+      this._wholeLineBreakpoints[url][line] = aBreakpoint;
+    }
+  },
+
+  /**
+   * Remove a breakpoint from the breakpoint store.
+   *
+   * @param Object aBreakpoint
+   *        The breakpoint to be removed. It is an object with the following
+   *        properties:
+   *          - url
+   *          - line
+   *          - column (optional)
+   */
+  removeBreakpoint: function BS_removeBreakpoint({ url, line, column }) {
+    if (column != null) {
+      if (this._breakpoints[url]) {
+        if (this._breakpoints[url][line]) {
+          delete this._breakpoints[url][line][column];
+
+          // If this was the last breakpoint on this line, delete the line from
+          // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield
+          // this line even though we no longer have breakpoints on
+          // it. Furthermore, we use Object.keys() instead of just checking
+          // `this._breakpoints[url].length` directly, because deleting
+          // properties from sparse arrays doesn't update the `length` property
+          // like adding them does.
+          if (Object.keys(this._breakpoints[url][line]).length === 0) {
+            delete this._breakpoints[url][line];
+          }
+        }
+      }
+    } else {
+      if (this._wholeLineBreakpoints[url]) {
+        delete this._wholeLineBreakpoints[url][line];
+      }
+    }
+  },
+
+  /**
+   * Get a breakpoint from the breakpoint store. Will throw an error if the
+   * breakpoint is not found unless you explicitly silence it.
+   *
+   * @param Object aLocation
+   *        The location of the breakpoint you are retrieving. It is an object
+   *        with the following properties:
+   *          - url
+   *          - line
+   *          - column (optional)
+   * @param bool aShouldThrow
+   *        Optional; defaults to true. Whether an error should be thrown when
+   *        there is no breakpoint at the specified locaiton.
+   */
+  getBreakpoint: function BS_getBreakpoint(aLocation, aShouldThrow=true) {
+    let { url, line, column } = aLocation;
+    dbg_assert(url != null);
+    dbg_assert(line != null);
+    for (let bp of this.findBreakpoints(aLocation)) {
+      // We will get whole line breakpoints before individual columns, so just
+      // return the first one and if they didn't specify a column then they will
+      // get the whole line breakpoint, and otherwise we will find the correct
+      // one.
+      return bp;
+    }
+    if (aShouldThrow) {
+      throw new Error("No breakpoint at url = " + url
+                      + ", line = " + line
+                      + ", column = " + column);
+    }
+    return null;
+  },
+
+  /**
+   * Iterate over the breakpoints in this breakpoint store. You can optionally
+   * provide search parameters to filter the set of breakpoints down to those
+   * that match your parameters.
+   *
+   * @param Object aSearchParams
+   *        Optional. An object with the following properties:
+   *          - url
+   *          - line (optional; requires the url property)
+   *          - column (optional; requires the line property)
+   */
+  findBreakpoints: function BS_findBreakpoints(aSearchParams={}) {
+    if (aSearchParams.column != null) {
+      dbg_assert(aSearchParams.line != null);
+    }
+    if (aSearchParams.line != null) {
+      dbg_assert(aSearchParams.url != null);
+    }
+
+    for (let url of this._iterUrls(aSearchParams.url)) {
+      for (let line of this._iterLines(url, aSearchParams.line)) {
+        // Always yield whole line breakpoints first. See comment in
+        // |BreakpointStore.prototype.getBreakpoint|.
+        if (aSearchParams.column == null
+            && this._wholeLineBreakpoints[url]
+            && this._wholeLineBreakpoints[url][line]) {
+          yield this._wholeLineBreakpoints[url][line];
+        }
+        for (let column of this._iterColumns(url, line, aSearchParams.column)) {
+          yield this._breakpoints[url][line][column];
+        }
+      }
+    }
+  },
+
+  _iterUrls: function BS__iterUrls(aUrl) {
+    if (aUrl) {
+      if (this._breakpoints[aUrl] || this._wholeLineBreakpoints[aUrl]) {
+        yield aUrl;
+      }
+    } else {
+      for (let url of Object.keys(this._wholeLineBreakpoints)) {
+        yield url;
+      }
+      for (let url of Object.keys(this._breakpoints)) {
+        if (url in this._wholeLineBreakpoints) {
+          continue;
+        }
+        yield url;
+      }
+    }
+  },
+
+  _iterLines: function BS__iterLines(aUrl, aLine) {
+    if (aLine != null) {
+      if ((this._wholeLineBreakpoints[aUrl]
+           && this._wholeLineBreakpoints[aUrl][aLine])
+          || (this._breakpoints[aUrl] && this._breakpoints[aUrl][aLine])) {
+        yield aLine;
+      }
+    } else {
+      const wholeLines = this._wholeLineBreakpoints[aUrl]
+        ? Object.keys(this._wholeLineBreakpoints[aUrl])
+        : [];
+      const columnLines = this._breakpoints[aUrl]
+        ? Object.keys(this._breakpoints[aUrl])
+        : [];
+
+      const lines = wholeLines.concat(columnLines).sort();
+
+      let lastLine;
+      for (let line of lines) {
+        if (line === lastLine) {
+          continue;
+        }
+        yield line;
+        lastLine = line;
+      }
+    }
+  },
+
+  _iterColumns: function BS__iterColumns(aUrl, aLine, aColumn) {
+    if (!this._breakpoints[aUrl] || !this._breakpoints[aUrl][aLine]) {
+      return;
+    }
+
+    if (aColumn != null) {
+      if (this._breakpoints[aUrl][aLine][aColumn]) {
+        yield aColumn;
+      }
+    } else {
+      for (let column in this._breakpoints[aUrl][aLine]) {
+        yield column;
+      }
+    }
+  },
+};
+
+/**
  * JSD2 actors.
  */
 /**
  * Creates a ThreadActor.
  *
  * ThreadActors manage a JSInspector object and manage execution/inspection
  * of debuggees.
  *
@@ -43,27 +272,27 @@ function ThreadActor(aHooks, aGlobal)
     useSourceMaps: false
   };
 }
 
 /**
  * The breakpoint store must be shared across instances of ThreadActor so that
  * page reloads don't blow away all of our breakpoints.
  */
-ThreadActor._breakpointStore = {};
+ThreadActor.breakpointStore = new BreakpointStore();
 
 ThreadActor.prototype = {
   actorPrefix: "context",
 
   get state() { return this._state; },
   get attached() this.state == "attached" ||
                  this.state == "running" ||
                  this.state == "paused",
 
-  get _breakpointStore() { return ThreadActor._breakpointStore; },
+  get breakpointStore() { return ThreadActor.breakpointStore; },
 
   get threadLifetimePool() {
     if (!this._threadLifetimePool) {
       this._threadLifetimePool = new ActorPool(this.conn);
       this.conn.addActorPool(this._threadLifetimePool);
       this._threadLifetimePool.objectActors = new WeakMap();
     }
     return this._threadLifetimePool;
@@ -282,17 +511,31 @@ ThreadActor.prototype = {
   _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason,
                                                  onPacket=function (k) k) {
     try {
       let packet = this._paused(aFrame);
       if (!packet) {
         return undefined;
       }
       packet.why = aReason;
-      resolve(onPacket(packet)).then(this.conn.send.bind(this.conn));
+
+      let { url, line, column } = packet.frame.where;
+      this.sources.getOriginalLocation(url, line, column).then(aOrigPosition => {
+        packet.frame.where = aOrigPosition;
+        resolve(onPacket(packet))
+          .then(null, error => {
+            reportError(error);
+            return {
+              error: "unknownError",
+              message: error.message + "\n" + error.stack
+            };
+          })
+          .then(packet => this.conn.send(packet));
+      });
+
       return this._nest();
     } catch(e) {
       reportError(e, "Got an exception during TA__pauseAndRespond: ");
       return undefined;
     }
   },
 
   /**
@@ -364,17 +607,17 @@ ThreadActor.prototype = {
         if (thread.sources.isBlackBoxed(this.script.url)) {
           return undefined;
         }
 
         // Note that we're popping this frame; we need to watch for
         // subsequent step events on its caller.
         this.reportedPop = true;
 
-        return pauseAndRespond(this, (aPacket) => {
+        return pauseAndRespond(this, aPacket => {
           aPacket.why.frameFinished = {};
           if (!aCompletion) {
             aPacket.why.frameFinished.terminated = true;
           } else if (aCompletion.hasOwnProperty("return")) {
             aPacket.why.frameFinished.return = createValueGrip(aCompletion.return);
           } else if (aCompletion.hasOwnProperty("yield")) {
             aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield);
           } else {
@@ -529,17 +772,19 @@ ThreadActor.prototype = {
       if (offsets[line]) {
         let location = { url: script.url, line: line };
         let resp = this._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 bpActor = this._breakpointStore[location.url][location.line].actor;
+        let bp = this.breakpointStore.getBreakpoint(location);
+        let bpActor = bp.actor;
+        dbg_assert(bp, "Breakpoint must exist");
         dbg_assert(bpActor, "Breakpoint actor must be created");
         this._hiddenBreakpoints.set(bpActor.actorID, bpActor);
         break;
       }
     }
   },
 
   /**
@@ -614,18 +859,18 @@ ThreadActor.prototype = {
     // frames if count is not defined.
     let frames = [];
     let promises = [];
     for (; frame && (!count || i < (start + count)); i++, frame=frame.older) {
       let form = this._createFrameActor(frame).form();
       form.depth = i;
       frames.push(form);
 
-      let promise = this.sources.getOriginalLocation(form.where.url,
-                                                     form.where.line)
+      let { url, line, column } = form.where;
+      let promise = this.sources.getOriginalLocation(url, line, column)
         .then(function (aOrigLocation) {
           form.where = aOrigLocation;
         });
       promises.push(promise);
     }
 
     return all(promises).then(function () {
       return { frames: frames };
@@ -657,140 +902,151 @@ ThreadActor.prototype = {
    * Handle a protocol request to set a breakpoint.
    */
   onSetBreakpoint: function TA_onSetBreakpoint(aRequest) {
     if (this.state !== "paused") {
       return { error: "wrongState",
                message: "Breakpoints can only be set while the debuggee is paused."};
     }
 
-    // XXX: `originalColumn` is never used. See bug 827639.
     let { url: originalSource,
           line: originalLine,
           column: originalColumn } = aRequest.location;
 
     let locationPromise = this.sources.getGeneratedLocation(originalSource,
-                                                            originalLine)
-    return locationPromise.then((aLocation) => {
-      let line = aLocation.line;
-      if (this.dbg.findScripts({ url: aLocation.url }).length == 0 ||
+                                                            originalLine,
+                                                            originalColumn);
+    return locationPromise.then(({url, line, column}) => {
+      if (line == null ||
           line < 0 ||
-          line == null) {
+          this.dbg.findScripts({ url: url }).length == 0) {
         return { error: "noScript" };
       }
 
-      let response = this._createAndStoreBreakpoint(aLocation);
+      let response = this._createAndStoreBreakpoint({
+        url: url,
+        line: line,
+        column: column
+      });
       // If the original location of our generated location is different from
       // the original location we attempted to set the breakpoint on, we will
       // need to know so that we can set actualLocation on the response.
-      let originalLocation = this.sources.getOriginalLocation(aLocation.url,
-                                                              aLocation.line);
+      let originalLocation = this.sources.getOriginalLocation(url, line, column);
 
       return all([response, originalLocation])
         .then(([aResponse, {url, line}]) => {
           if (aResponse.actualLocation) {
             let actualOrigLocation = this.sources.getOriginalLocation(
-              aResponse.actualLocation.url, aResponse.actualLocation.line);
-            return actualOrigLocation.then(function ({ url, line }) {
-              if (url !== originalSource || line !== originalLine) {
-                aResponse.actualLocation = { url: url, line: line };
+              aResponse.actualLocation.url,
+              aResponse.actualLocation.line,
+              aResponse.actualLocation.column);
+            return actualOrigLocation.then(function ({ url, line, column }) {
+              if (url !== originalSource
+                  || line !== originalLine
+                  || column !== originalColumn) {
+                aResponse.actualLocation = {
+                  url: url,
+                  line: line,
+                  column: column
+                };
               }
               return aResponse;
             });
           }
 
           if (url !== originalSource || line !== originalLine) {
             aResponse.actualLocation = { url: url, line: line };
           }
 
           return aResponse;
         });
     });
   },
 
   /**
-   * Create a breakpoint at the specified location and store it in the cache.
+   * Create a breakpoint at the specified location and store it in the
+   * cache. Takes ownership of `aLocation`.
+   *
+   * @param Object aLocation
+   *        An object of the form { url, line[, column] }
    */
   _createAndStoreBreakpoint: function (aLocation) {
-      // Add the breakpoint to the store for later reuse, in case it belongs to
-      // a script that hasn't appeared yet.
-      if (!this._breakpointStore[aLocation.url]) {
-        this._breakpointStore[aLocation.url] = [];
-      }
-      let scriptBreakpoints = this._breakpointStore[aLocation.url];
-      scriptBreakpoints[aLocation.line] = {
-        url: aLocation.url,
-        line: aLocation.line,
-        column: aLocation.column
-      };
-
-      return this._setBreakpoint(aLocation);
+    // Add the breakpoint to the store for later reuse, in case it belongs to a
+    // script that hasn't appeared yet.
+    this.breakpointStore.addBreakpoint(aLocation);
+    return this._setBreakpoint(aLocation);
   },
 
   /**
    * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
    * is being set contains no code, then the breakpoint will slide down to the
    * next line that has runnable code. In this case the server breakpoint cache
    * will be updated, so callers that iterate over the breakpoint cache should
    * take that into account.
    *
    * @param object aLocation
-   *        The location of the breakpoint as specified in the protocol.
+   *        The location of the breakpoint (in the generated source, if source
+   *        mapping).
    */
   _setBreakpoint: function TA__setBreakpoint(aLocation) {
-    let breakpoints = this._breakpointStore[aLocation.url];
-
-    // Get or create the breakpoint actor for the given location
     let actor;
-    if (breakpoints[aLocation.line].actor) {
-      actor = breakpoints[aLocation.line].actor;
+    let storedBp = this.breakpointStore.getBreakpoint(aLocation);
+    if (storedBp.actor) {
+      actor = storedBp.actor;
     } else {
-      actor = breakpoints[aLocation.line].actor = new BreakpointActor(this, {
+      storedBp.actor = actor = new BreakpointActor(this, {
         url: aLocation.url,
-        line: aLocation.line
+        line: aLocation.line,
+        column: aLocation.column
       });
       this._hooks.addToParentPool(actor);
     }
 
     // Find all scripts matching the given location
     let scripts = this.dbg.findScripts(aLocation);
     if (scripts.length == 0) {
       return {
         error: "noScript",
         actor: actor.actorID
       };
     }
 
    /**
-     * For each script, if the given line has at least one entry point, set
-     * breakpoint on the bytecode offet for each of them.
-     */
-    let found = false;
+    * For each script, if the given line has at least one entry point, set a
+    * breakpoint on the bytecode offets for each of them.
+    */
+
+    // Debugger.Script -> array of offset mappings
+    let scriptsAndOffsetMappings = new Map();
+
     for (let script of scripts) {
-      let offsets = script.getLineOffsets(aLocation.line);
-      if (offsets.length > 0) {
-        for (let offset of offsets) {
-          script.setBreakpoint(offset, actor);
+      this._findClosestOffsetMappings(aLocation,
+                                      script,
+                                      scriptsAndOffsetMappings);
+    }
+
+    if (scriptsAndOffsetMappings.size > 0) {
+      for (let [script, mappings] of scriptsAndOffsetMappings) {
+        for (let offsetMapping of mappings) {
+          script.setBreakpoint(offsetMapping.offset, actor);
         }
         actor.addScript(script, this);
-        found = true;
       }
-    }
-    if (found) {
+
       return {
         actor: actor.actorID
       };
     }
 
    /**
-     * If we get here, no breakpoint was set. This is because the given line
-     * has no entry points, for example because it is empty. As a fallback
-     * strategy, we try to set the breakpoint on the smallest line greater
-     * than or equal to the given line that as at least one entry point.
-     */
+    * If we get here, no breakpoint was set. This is because the given line
+    * has no entry points, for example because it is empty. As a fallback
+    * strategy, we try to set the breakpoint on the smallest line greater
+    * than or equal to the given line that as at least one entry point.
+    */
 
     // Find all innermost scripts matching the given location
     let scripts = this.dbg.findScripts({
       url: aLocation.url,
       line: aLocation.line,
       innermost: true
     });
 
@@ -817,58 +1073,123 @@ ThreadActor.prototype = {
             };
           }
           found = true;
           break;
         }
       }
     }
     if (found) {
-      if (breakpoints[actualLocation.line] &&
-          breakpoints[actualLocation.line].actor) {
+      let existingBp = this.breakpointStore.getBreakpoint(actualLocation, false);
+      if (existingBp && existingBp.actor) {
         /**
          * We already have a breakpoint actor for the actual location, so
          * actor we created earlier is now redundant. Delete it, update the
          * breakpoint store, and return the actor for the actual location.
          */
         actor.onDelete();
-        delete breakpoints[aLocation.line];
+        this.breakpointStore.removeBreakpoint(aLocation);
         return {
-          actor: breakpoints[actualLocation.line].actor.actorID,
+          actor: existingBp.actor.actorID,
           actualLocation: actualLocation
         };
       } else {
         /**
          * We don't have a breakpoint actor for the actual location yet.
          * Instead or creating a new actor, reuse the actor we created earlier,
          * and update the breakpoint store.
          */
         actor.location = actualLocation;
-        breakpoints[actualLocation.line] = breakpoints[aLocation.line];
-        delete breakpoints[aLocation.line];
-        // WARNING: This overwrites aLocation.line
-        breakpoints[actualLocation.line].line = actualLocation.line;
+        this.breakpointStore.addBreakpoint({
+          actor: actor,
+          url: actualLocation.url,
+          line: actualLocation.line,
+          column: actualLocation.column
+        });
+        this.breakpointStore.removeBreakpoint(aLocation);
         return {
           actor: actor.actorID,
           actualLocation: actualLocation
         };
       }
     }
 
     /**
      * If we get here, no line matching the given line was found, so just
-     * epically.
+     * fail epically.
      */
     return {
       error: "noCodeAtLineColumn",
       actor: actor.actorID
     };
   },
 
   /**
+   * Find all of the offset mappings associated with `aScript` that are closest
+   * to `aTargetLocation`. If new offset mappings are found that are closer to
+   * `aTargetOffset` than the existing offset mappings inside
+   * `aScriptsAndOffsetMappings`, we empty that map and only consider the
+   * closest offset mappings. If there is no column in `aTargetLocation`, we add
+   * all offset mappings that are on the given line.
+   *
+   * @param Object aTargetLocation
+   *        An object of the form { url, line[, column] }.
+   * @param Debugger.Script aScript
+   *        The script in which we are searching for offsets.
+   * @param Map aScriptsAndOffsetMappings
+   *        A Map object which maps Debugger.Script instances to arrays of
+   *        offset mappings. This is an out param.
+   */
+  _findClosestOffsetMappings: function TA__findClosestOffsetMappings(aTargetLocation,
+                                                                     aScript,
+                                                                     aScriptsAndOffsetMappings) {
+    let offsetMappings = aScript.getAllColumnOffsets()
+      .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
+
+    // If we are given a column, we will try and break only at that location,
+    // otherwise we will break anytime we get on that line.
+
+    if (aTargetLocation.column == null) {
+      if (offsetMappings.length) {
+        aScriptsAndOffsetMappings.set(aScript, offsetMappings);
+      }
+      return;
+    }
+
+    // Attempt to find the current closest offset distance from the target
+    // location by grabbing any offset mapping in the map by doing one iteration
+    // and then breaking (they all have the same distance from the target
+    // location).
+    let closestDistance = Infinity;
+    if (aScriptsAndOffsetMappings.size) {
+      for (let mappings of aScriptsAndOffsetMappings.values()) {
+        closestDistance = Math.abs(aTargetLocation.column - mappings[0].columnNumber);
+        break;
+      }
+    }
+
+    for (let mapping of offsetMappings) {
+      let currentDistance = Math.abs(aTargetLocation.column - mapping.columnNumber);
+
+      if (currentDistance > closestDistance) {
+        continue;
+      } else if (currentDistance < closestDistance) {
+        closestDistance = currentDistance;
+        aScriptsAndOffsetMappings.clear();
+        aScriptsAndOffsetMappings.set(aScript, [mapping]);
+      } else {
+        if (!aScriptsAndOffsetMappings.has(aScript)) {
+          aScriptsAndOffsetMappings.set(aScript, []);
+        }
+        aScriptsAndOffsetMappings.get(aScript).push(mapping);
+      }
+    }
+  },
+
+  /**
    * Get the script and source lists from the debugger.
    *
    * TODO bug 637572: we should be dealing with sources directly, not inferring
    * them through scripts.
    */
   _discoverSources: function TA__discoverSources() {
     // Only get one script per url.
     let scriptsByUrl = {};
@@ -891,19 +1212,18 @@ ThreadActor.prototype = {
   /**
    * Disassociate all breakpoint actors from their scripts and clear the
    * breakpoint handlers. This method can be used when the thread actor intends
    * to keep the breakpoint store, but needs to clear any actual breakpoints,
    * e.g. due to a page navigation. This way the breakpoint actors' script
    * caches won't hold on to the Debugger.Script objects leaking memory.
    */
   disableAllBreakpoints: function () {
-    for (let url in this._breakpointStore) {
-      for (let line in this._breakpointStore[url]) {
-        let bp = this._breakpointStore[url][line];
+    for (let bp of this.breakpointStore.findBreakpoints()) {
+      if (bp.actor) {
         bp.actor.removeScripts();
       }
     }
   },
 
   /**
    * Handle a protocol request to pause the debuggee.
    */
@@ -1007,17 +1327,17 @@ ThreadActor.prototype = {
 
     if (this._framePool.has(aFrameID)) {
       return this._framePool.get(aFrameID).frame;
     }
 
     return undefined;
   },
 
-  _paused: function TA_paused(aFrame) {
+  _paused: function TA__paused(aFrame) {
     // We don't handle nested pauses correctly.  Don't try - if we're
     // paused, just continue running whatever code triggered the pause.
     // We don't want to actually have nested pauses (although we
     // have nested event loops).  If code runs in the debuggee during
     // a pause, it should cause the actor to resume (dropping
     // pause-lifetime actors etc) and then repause when complete.
 
     if (this.state === "paused") {
@@ -1498,31 +1818,29 @@ ThreadActor.prototype = {
    * @returns true, if the script was added; false otherwise.
    */
   _addScript: function TA__addScript(aScript) {
     if (!this._allowSource(aScript.url)) {
       return false;
     }
 
     // Set any stored breakpoints.
-    let existing = this._breakpointStore[aScript.url];
-    if (existing) {
-      let endLine = aScript.startLine + aScript.lineCount - 1;
-      // Iterate over the lines backwards, so that sliding breakpoints don't
-      // affect the loop.
-      for (let line = existing.length - 1; line >= aScript.startLine; line--) {
-        let bp = existing[line];
-        // Only consider breakpoints that are not already associated with
-        // scripts, and limit search to the line numbers contained in the new
-        // script.
-        if (bp && !bp.actor.scripts.length && line <= endLine) {
-          this._setBreakpoint(bp);
-        }
+
+    let endLine = aScript.startLine + aScript.lineCount - 1;
+    for (let bp of this.breakpointStore.findBreakpoints({ url: aScript.url })) {
+      // Only consider breakpoints that are not already associated with
+      // scripts, and limit search to the line numbers contained in the new
+      // script.
+      if (!bp.actor.scripts.length
+          && bp.line >= aScript.startLine
+          && bp.line <= endLine) {
+        this._setBreakpoint(bp);
       }
     }
+
     return true;
   },
 
 };
 
 ThreadActor.prototype.requestTypes = {
   "attach": ThreadActor.prototype.onAttach,
   "detach": ThreadActor.prototype.onDetach,
@@ -2320,18 +2638,21 @@ FrameActor.prototype = {
       let envActor = this.threadActor
         .createEnvironmentActor(this.frame.environment,
                                 this.frameLifetimePool);
       form.environment = envActor.form();
     }
     form.this = this.threadActor.createValueGrip(this.frame.this);
     form.arguments = this._args();
     if (this.frame.script) {
-      form.where = { url: this.frame.script.url,
-                     line: this.frame.script.getOffsetLine(this.frame.offset) };
+      form.where = {
+        url: this.frame.script.url,
+        line: this.frame.script.getOffsetLine(this.frame.offset),
+        column: getOffsetColumn(this.frame.offset, this.frame.script)
+      };
       form.isBlackBoxed = this.threadActor.sources.isBlackBoxed(this.frame.script.url)
     }
 
     if (!this.frame.older) {
       form.oldest = true;
     }
 
     return form;
@@ -2436,40 +2757,31 @@ BreakpointActor.prototype = {
     let reason = {};
     if (this.threadActor._hiddenBreakpoints.has(this.actorID)) {
       reason.type = "pauseOnDOMEvents";
     } else {
       reason.type = "breakpoint";
       // TODO: add the rest of the breakpoints on that line (bug 676602).
       reason.actors = [ this.actorID ];
     }
-    return this.threadActor._pauseAndRespond(aFrame, reason, (aPacket) => {
-      let { url, line } = aPacket.frame.where;
-      return this.threadActor.sources.getOriginalLocation(url, line)
-        .then(function (aOrigPosition) {
-          aPacket.frame.where = aOrigPosition;
-          return aPacket;
-        });
-    });
+    return this.threadActor._pauseAndRespond(aFrame, reason);
   },
 
   /**
    * Handle a protocol request to remove this breakpoint.
    *
    * @param aRequest object
    *        The protocol request object.
    */
   onDelete: function BA_onDelete(aRequest) {
     // Remove from the breakpoint store.
-    let scriptBreakpoints = this.threadActor._breakpointStore[this.location.url];
-    delete scriptBreakpoints[this.location.line];
+    this.threadActor.breakpointStore.removeBreakpoint(this.location);
     this.threadActor._hooks.removeFromParentPool(this);
     // Remove the actual breakpoint from the associated scripts.
     this.removeScripts();
-
     return { from: this.actorID };
   }
 };
 
 BreakpointActor.prototype.requestTypes = {
   "delete": BreakpointActor.prototype.onDelete
 };
 
@@ -2893,17 +3205,17 @@ ThreadSources.prototype = {
    * Return a promise of a SourceMapConsumer for the source map located at
    * |aAbsSourceMapURL|, which must be absolute. If there is already such a
    * promise extant, return it.
    */
   _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL) {
     if (aAbsSourceMapURL in this._sourceMaps) {
       return this._sourceMaps[aAbsSourceMapURL];
     } else {
-      let promise = fetch(aAbsSourceMapURL).then((rawSourceMap) => {
+      let promise = fetch(aAbsSourceMapURL).then(rawSourceMap => {
         let map = new SourceMapConsumer(rawSourceMap);
         let base = aAbsSourceMapURL.replace(/\/[^\/]+$/, '/');
         if (base.indexOf("data:") !== 0) {
           map.sourceRoot = map.sourceRoot
             ? this._normalize(map.sourceRoot, base)
             : base;
         }
         return map;
@@ -2911,73 +3223,74 @@ ThreadSources.prototype = {
       this._sourceMaps[aAbsSourceMapURL] = promise;
       return promise;
     }
   },
 
   /**
    * Returns a promise of the location in the original source if the source is
    * source mapped, otherwise a promise of the same location.
-   *
-   * TODO bug 637572: take/return a column
    */
-  getOriginalLocation: function TS_getOriginalLocation(aSourceUrl, aLine) {
+  getOriginalLocation:
+  function TS_getOriginalLocation(aSourceUrl, aLine, aColumn) {
     if (aSourceUrl in this._sourceMapsByGeneratedSource) {
       return this._sourceMapsByGeneratedSource[aSourceUrl]
         .then(function (aSourceMap) {
-          let { source, line } = aSourceMap.originalPositionFor({
-            source: aSourceUrl,
+          let { source, line, column } = aSourceMap.originalPositionFor({
             line: aLine,
-            column: Infinity
+            column: aColumn
           });
           return {
             url: source,
-            line: line
+            line: line,
+            column: column
           };
         });
     }
 
     // No source map
     return resolve({
       url: aSourceUrl,
-      line: aLine
+      line: aLine,
+      column: aColumn
     });
   },
 
   /**
    * Returns a promise of the location in the generated source corresponding to
    * the original source and line given.
    *
    * When we pass a script S representing generated code to |sourceMap|,
    * above, that returns a promise P. The process of resolving P populates
    * the tables this function uses; thus, it won't know that S's original
    * source URLs map to S until P is resolved.
-   *
-   * TODO bug 637572: take/return a column
    */
-  getGeneratedLocation: function TS_getGeneratedLocation(aSourceUrl, aLine) {
+  getGeneratedLocation:
+  function TS_getGeneratedLocation(aSourceUrl, aLine, aColumn) {
     if (aSourceUrl in this._sourceMapsByOriginalSource) {
       return this._sourceMapsByOriginalSource[aSourceUrl]
         .then((aSourceMap) => {
-          let { line } = aSourceMap.generatedPositionFor({
+          let { line, column } = aSourceMap.generatedPositionFor({
             source: aSourceUrl,
             line: aLine,
-            column: Infinity
+            column: aColumn == null ? Infinity : aColumn
           });
           return {
             url: this._generatedUrlsByOriginalUrl[aSourceUrl],
-            line: line
+            line: line,
+            column: column
           };
         });
     }
 
     // No source map
     return resolve({
       url: aSourceUrl,
-      line: aLine
+      line: aLine,
+      column: aColumn
     });
   },
 
   /**
    * Returns true if URL for the given source is black boxed.
    *
    * @param aURL String
    *        The URL of the source which we are checking whether it is black
@@ -3026,16 +3339,41 @@ ThreadSources.prototype = {
     for (let url in this._sourceActors) {
       yield this._sourceActors[url];
     }
   }
 };
 
 // Utility functions.
 
+// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
+// implemented.
+function getOffsetColumn(aOffset, aScript) {
+  let bestOffsetMapping = null;
+  for (let offsetMapping of aScript.getAllColumnOffsets()) {
+    if (!bestOffsetMapping ||
+        (offsetMapping.offset <= aOffset &&
+         offsetMapping.offset > bestOffsetMapping.offset)) {
+      bestOffsetMapping = offsetMapping;
+    }
+  }
+
+  if (!bestOffsetMapping) {
+    // XXX: Try not to completely break the experience of using the debugger for
+    // the user by assuming column 0. Simultaneously, report the error so that
+    // there is a paper trail if the assumption is bad and the debugging
+    // experience becomes wonky.
+    reportError(new Error("Could not find a column for offset " + aOffset
+                          + " in the script " + aScript));
+    return 0;
+  }
+
+  return bestOffsetMapping.columnNumber;
+}
+
 /**
  * Utility function for updating an object with the properties of another
  * object.
  *
  * @param aTarget Object
  *        The object being updated.
  * @param aNewAttrs Object
  *        The new attributes being set on the target.
--- a/toolkit/devtools/server/tests/unit/test_blackboxing-03.js
+++ b/toolkit/devtools/server/tests/unit/test_blackboxing-03.js
@@ -3,16 +3,17 @@
 
 /**
  * Test that we don't stop at debugger statements inside black boxed sources.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gBpClient;
 
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-black-box", function(aResponse, aTabClient, aThreadClient) {
@@ -27,17 +28,18 @@ const BLACK_BOXED_URL = "http://example.
 const SOURCE_URL = "http://example.com/source.js";
 
 function test_black_box()
 {
   gClient.addOneTimeListener("paused", function () {
     gThreadClient.setBreakpoint({
       url: SOURCE_URL,
       line: 4
-    }, function ({error}) {
+    }, function ({error}, bpClient) {
+      gBpClient = bpClient;
       do_check_true(!error, "Should not get an error: " + error);
       gThreadClient.resume(test_black_box_dbg_statement);
     });
   });
 
   Components.utils.evalInSandbox(
     "" + function doStuff(k) { // line 1
       debugger;                // line 2 - Break here
@@ -71,34 +73,30 @@ function test_black_box_dbg_statement() 
     let sourceClient = gThreadClient.source(sources.filter(s => s.url == BLACK_BOXED_URL)[0]);
 
     sourceClient.blackBox(function ({error}) {
       do_check_true(!error, "Should not get an error: " + error);
 
       gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
         do_check_eq(aPacket.why.type, "breakpoint",
                     "We should pass over the debugger statement.");
-        gThreadClient.resume(test_unblack_box_dbg_statement.bind(null, sourceClient));
+        gBpClient.remove(function ({error}) {
+          do_check_true(!error, "Should not get an error: " + error);
+          gThreadClient.resume(test_unblack_box_dbg_statement.bind(null, sourceClient));
+        });
       });
       gDebuggee.runTest();
     });
   });
 }
 
 function test_unblack_box_dbg_statement(aSourceClient) {
   aSourceClient.unblackBox(function ({error}) {
     do_check_true(!error, "Should not get an error: " + error);
 
     gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       do_check_eq(aPacket.why.type, "debuggerStatement",
                   "We should stop at the debugger statement again");
-
-      // We will hit the breakpoint on resume, so do this nastiness to skip over it.
-      gClient.addOneTimeListener(
-        "paused",
-        gThreadClient.resume.bind(
-          gThreadClient,
-          finishClient.bind(null, gClient)));
-      gThreadClient.resume();
+      finishClient(gClient);
     });
     gDebuggee.runTest();
   });
 }
--- a/toolkit/devtools/server/tests/unit/test_blackboxing-05.js
+++ b/toolkit/devtools/server/tests/unit/test_blackboxing-05.js
@@ -12,17 +12,22 @@ var gThreadClient;
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-black-box");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-black-box", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
-      test_black_box();
+      // XXX: We have to do an executeSoon so that the error isn't caught and
+      // reported by DebuggerClient.requester (because we are using the local
+      // transport and share a stack) which causes the test to fail.
+      Services.tm.mainThread.dispatch({
+        run: test_black_box
+      }, Ci.nsIThread.DISPATCH_NORMAL);
     });
   });
   do_test_pending();
 }
 
 const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
 const SOURCE_URL = "http://example.com/source.js";
 
--- a/toolkit/devtools/server/tests/unit/test_breakpoint-01.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-01.js
@@ -22,17 +22,20 @@ function run_test()
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     let path = getFilePath('test_breakpoint-01.js');
-    let location = { url: path, line: gDebuggee.line0 + 3};
+    let location = {
+      url: path,
+      line: gDebuggee.line0 + 3
+    };
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
         // Check the return value.
         do_check_eq(aPacket.type, "paused");
         do_check_eq(aPacket.frame.where.url, path);
         do_check_eq(aPacket.frame.where.line, location.line);
         do_check_eq(aPacket.why.type, "breakpoint");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
@@ -50,13 +53,14 @@ function test_simple_breakpoint()
       });
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
 
     });
 
   });
 
-  gDebuggee.eval("var line0 = Error().lineNumber;\n" +
-                 "debugger;\n" +   // line0 + 1
-                 "var a = 1;\n" +  // line0 + 2
-                 "var b = 2;\n");  // line0 + 3
+  Components.utils.evalInSandbox("var line0 = Error().lineNumber;\n" +
+                                 "debugger;\n" +   // line0 + 1
+                                 "var a = 1;\n" +  // line0 + 2
+                                 "var b = 2;\n",   // line0 + 3
+                                 gDebuggee);
 }
--- a/toolkit/devtools/server/tests/unit/test_breakpoint-03.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-03.js
@@ -27,16 +27,17 @@ function run_test()
 
 function test_skip_breakpoint()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     let path = getFilePath('test_breakpoint-03.js');
     let location = { url: path, line: gDebuggee.line0 + 3};
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       // Check that the breakpoint has properly skipped forward one line.
+      do_check_true(!!aResponse.actualLocation);
       do_check_eq(aResponse.actualLocation.url, location.url);
       do_check_eq(aResponse.actualLocation.line, location.line + 1);
       gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
         // Check the return value.
         do_check_eq(aPacket.type, "paused");
         do_check_eq(aPacket.frame.where.url, path);
         do_check_eq(aPacket.frame.where.line, location.line + 1);
         do_check_eq(aPacket.why.type, "breakpoint");
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-15.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that adding a breakpoint in the same place returns the same actor.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_same_breakpoint();
+    });
+  });
+  do_test_pending();
+}
+
+function test_same_breakpoint()
+{
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    let path = getFilePath('test_breakpoint-01.js');
+    let location = {
+      url: path,
+      line: gDebuggee.line0 + 3
+    };
+    gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
+      gThreadClient.setBreakpoint(location, function (aResponse, secondBpClient) {
+        do_check_eq(bpClient.actor, secondBpClient.actor,
+                    "Should get the same actor w/ whole line breakpoints");
+        let location = {
+          url: path,
+          line: gDebuggee.line0 + 2,
+          column: 6
+        };
+        gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
+          gThreadClient.setBreakpoint(location, function (aResponse, secondBpClient) {
+            do_check_eq(bpClient.actor, secondBpClient.actor,
+                        "Should get the same actor column breakpoints");
+            finishClient(gClient);
+          });
+        });
+      });
+    });
+
+  });
+
+  Components.utils.evalInSandbox("var line0 = Error().lineNumber;\n" +
+                                 "debugger;\n" +   // line0 + 1
+                                 "var a = 1;\n" +  // line0 + 2
+                                 "var b = 2;\n",   // line0 + 3
+                                 gDebuggee);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-16.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that we can set breakpoints in columns, not just lines.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-breakpoints");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestTabAndResume(gClient,
+                           "test-breakpoints",
+                           function (aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_column_breakpoint();
+    });
+  });
+  do_test_pending();
+}
+
+function test_column_breakpoint()
+{
+  const location = {
+    url: "https://example.com/foo.js",
+    line: 1,
+    column: 55
+  };
+
+  // Debugger statement
+  gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    let timesBreakpointHit = 0;
+
+    gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
+      gThreadClient.addListener("paused", function _onPaused(aEvent, aPacket) {
+        do_check_eq(aPacket.type, "paused");
+        do_check_eq(aPacket.why.type, "breakpoint");
+        do_check_eq(aPacket.why.actors[0], bpClient.actor);
+        do_check_eq(aPacket.frame.where.url, location.url);
+        do_check_eq(aPacket.frame.where.line, location.line);
+        do_check_eq(aPacket.frame.where.column, location.column);
+
+        do_check_eq(gDebuggee.acc, timesBreakpointHit);
+        do_check_eq(aPacket.frame.environment.bindings.variables.i.value,
+                    timesBreakpointHit);
+
+        if (++timesBreakpointHit === 3) {
+          gThreadClient.removeListener("paused", _onPaused);
+          bpClient.remove(function (aResponse) {
+            gThreadClient.resume(() => finishClient(gClient));
+          });
+        } else {
+          gThreadClient.resume();
+        }
+      });
+
+      // Continue until the breakpoint is hit.
+      gThreadClient.resume();
+    });
+
+  });
+
+  let code =
+"(function () { debugger; this.acc = 0; for (let i = 0; i < 3; i++) this.acc++; }());";
+
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 location.url, 1);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-17.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that when we add 2 breakpoints to the same line at different columns and
+ * then remove one of them, we don't remove them both.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-breakpoints");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-breakpoints", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_breakpoints_columns();
+    });
+  });
+  do_test_pending();
+}
+
+const URL = "http://example.com/benderbendingrodriguez.js";
+
+const code =
+"(" + function (global) {
+  global.foo = function () {
+    Math.abs(-1); Math.log(0.5);
+    debugger;
+  };
+  debugger;
+} + "(this))";
+
+const firstLocation = {
+  url: URL,
+  line: 3,
+  column: 4
+};
+
+const secondLocation = {
+  url: URL,
+  line: 3,
+  column: 18
+};
+
+function test_breakpoints_columns() {
+  gClient.addOneTimeListener("paused", set_breakpoints);
+
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8", URL, 1);
+}
+
+function set_breakpoints() {
+  let first, second;
+
+  gThreadClient.setBreakpoint(firstLocation, function ({ error, actualLocation },
+                                                       aBreakpointClient) {
+    do_check_true(!error, "Should not get an error setting the breakpoint");
+    do_check_true(!actualLocation, "Should not get an actualLocation");
+    first = aBreakpointClient;
+
+    gThreadClient.setBreakpoint(secondLocation, function ({ error, actualLocation },
+                                                          aBreakpointClient) {
+      do_check_true(!error, "Should not get an error setting the breakpoint");
+      do_check_true(!actualLocation, "Should not get an actualLocation");
+      second = aBreakpointClient;
+
+      test_different_actors(first, second);
+    });
+  });
+}
+
+function test_different_actors(aFirst, aSecond) {
+  do_check_neq(aFirst.actor, aSecond.actor,
+               "Each breakpoint should have a different actor");
+  test_remove_one(aFirst, aSecond);
+}
+
+function test_remove_one(aFirst, aSecond) {
+  aFirst.remove(function ({error}) {
+    do_check_true(!error, "Should not get an error removing a breakpoint");
+
+    let hitSecond;
+    gClient.addListener("paused", function _onPaused(aEvent, {why, frame}) {
+      if (why.type == "breakpoint") {
+        hitSecond = true;
+        do_check_eq(why.actors.length, 1,
+                    "Should only be paused because of one breakpoint actor");
+        do_check_eq(why.actors[0], aSecond.actor,
+                    "Should be paused because of the correct breakpoint actor");
+        do_check_eq(frame.where.line, secondLocation.line,
+                    "Should be at the right line");
+        do_check_eq(frame.where.column, secondLocation.column,
+                    "Should be at the right column");
+        return void gThreadClient.resume();
+      }
+
+      if (why.type == "debuggerStatement") {
+        gClient.removeListener("paused", _onPaused);
+        do_check_true(hitSecond,
+                      "We should still hit `second`, but not `first`.");
+
+        return void finishClient(gClient);
+      }
+
+      do_check_true(false, "Should never get here");
+    });
+
+    gThreadClient.resume(() => gDebuggee.foo());
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-09.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that source maps and breakpoints work with minified javascript.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-source-map");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-source-map", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_minified();
+    });
+  });
+  do_test_pending();
+}
+
+function test_minified()
+{
+  let newSourceFired = false;
+
+  gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) {
+    do_check_eq(aEvent, "newSource");
+    do_check_eq(aPacket.type, "newSource");
+    do_check_true(!!aPacket.source);
+
+    do_check_eq(aPacket.source.url, "foo.js",
+                "The new source should be foo.js");
+    do_check_eq(aPacket.source.url.indexOf("foo.min.js"), -1,
+                "The new source should not be the minified file");
+
+    newSourceFired = true;
+  });
+
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    do_check_eq(aEvent, "paused");
+    do_check_eq(aPacket.why.type, "debuggerStatement");
+
+    const location = {
+      url: "foo.js",
+      line: 5
+    };
+
+    gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
+      do_check_true(!aResponse.error);
+      testHitBreakpoint();
+    });
+  });
+
+  // This is the original foo.js, which was then minified with uglifyjs version
+  // 2.2.5 and the "--mangle" option.
+  //
+  // (function () {
+  //   debugger;
+  //   function foo(n) {
+  //     var bar = n + n;
+  //     var unused = null;
+  //     return bar;
+  //   }
+  //   for (var i = 0; i < 10; i++) {
+  //     foo(i);
+  //   }
+  // }());
+
+  let code = '(function(){debugger;function r(r){var n=r+r;var u=null;return n}for(var n=0;n<10;n++){r(n)}})();\n//# sourceMappingURL=data:text/json,{"file":"foo.min.js","version":3,"sources":["foo.js"],"names":["foo","n","bar","unused","i"],"mappings":"CAAC,WACC,QACA,SAASA,GAAIC,GACX,GAAIC,GAAMD,EAAIA,CACd,IAAIE,GAAS,IACb,OAAOD,GAET,IAAK,GAAIE,GAAI,EAAGA,EAAI,GAAIA,IAAK,CAC3BJ,EAAII"}';
+
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 "http://example.com/foo.min.js", 1);
+}
+
+function testHitBreakpoint(timesHit=0) {
+  gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    ++timesHit;
+
+    do_check_eq(aEvent, "paused");
+    do_check_eq(aPacket.why.type, "breakpoint");
+
+    if (timesHit === 10) {
+      gThreadClient.resume(() => finishClient(gClient));
+    } else {
+      testHitBreakpoint(timesHit);
+    }
+  });
+
+  gThreadClient.resume();
+}
--- a/toolkit/devtools/server/tests/unit/test_stepping-06.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-06.js
@@ -12,17 +12,22 @@ var gThreadClient;
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
-      test_simple_stepping();
+      // XXX: We have to do an executeSoon so that the error isn't caught and
+      // reported by DebuggerClient.requester (because we are using the local
+      // transport and share a stack) which causes the test to fail.
+      Services.tm.mainThread.dispatch({
+        run: test_simple_stepping
+      }, Ci.nsIThread.DISPATCH_NORMAL);
     });
   });
   do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -83,16 +83,19 @@ reason = bug 820380
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-13.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-14.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
+[test_breakpoint-15.js]
+[test_breakpoint-16.js]
+[test_breakpoint-17.js]
 [test_listsources-01.js]
 [test_listsources-02.js]
 [test_listsources-03.js]
 [test_new_source-01.js]
 [test_sources_backwards_compat-01.js]
 [test_sources_backwards_compat-02.js]
 [test_sourcemaps-01.js]
 [test_sourcemaps-02.js]
@@ -103,16 +106,17 @@ reason = bug 820380
 [test_sourcemaps-05.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-06.js]
 [test_sourcemaps-07.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-08.js]
+[test_sourcemaps-09.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_interrupt.js]