Bug 711164 - Add support for stepping to the debugger; r=rcampbell
authorPanos Astithas <past@mozilla.com>
Sun, 18 Mar 2012 08:50:43 +0200
changeset 89597 6e1983c0efbb98e7d94ff0dcceff58b0964181b3
parent 89596 9b911534decac87236a0913c7a037f9c640daf7d
child 89598 551a912cd950f016afa95b2519d9891c80e6127f
push id618
push userpastithas@mozilla.com
push dateSun, 18 Mar 2012 07:00:58 +0000
treeherderfx-team@551a912cd950 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs711164
milestone14.0a1
Bug 711164 - Add support for stepping to the debugger; r=rcampbell
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/locales/en-US/chrome/browser/devtools/debugger.dtd
toolkit/devtools/debugger/dbg-client.jsm
toolkit/devtools/debugger/server/dbg-script-actors.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-01.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-02.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-03.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-05.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-06.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-07.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-08.js
toolkit/devtools/debugger/tests/unit/test_breakpoint-09.js
toolkit/devtools/debugger/tests/unit/test_stepping-01.js
toolkit/devtools/debugger/tests/unit/test_stepping-02.js
toolkit/devtools/debugger/tests/unit/test_stepping-03.js
toolkit/devtools/debugger/tests/unit/test_stepping-04.js
toolkit/devtools/debugger/tests/unit/xpcshell.ini
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -276,51 +276,84 @@ DebuggerView.Stackframes = {
     if (ThreadState.activeThread.paused) {
       ThreadState.activeThread.resume();
     } else {
       ThreadState.activeThread.interrupt();
     }
   },
 
   /**
+   * Listener handling the step over button click event.
+   */
+  _onStepOverClick: function DVF__onStepOverClick() {
+    ThreadState.activeThread.stepOver();
+  },
+
+  /**
+   * Listener handling the step in button click event.
+   */
+  _onStepInClick: function DVF__onStepInClick() {
+    ThreadState.activeThread.stepIn();
+  },
+
+  /**
+   * Listener handling the step out button click event.
+   */
+  _onStepOutClick: function DVF__onStepOutClick() {
+    ThreadState.activeThread.stepOut();
+  },
+
+  /**
    * Specifies if the active thread has more frames which need to be loaded.
    */
   _dirty: false,
 
   /**
    * The cached stackframes container.
    */
   _frames: null,
 
   /**
    * Initialization function, called when the debugger is initialized.
    */
   initialize: function DVF_initialize() {
     let close = document.getElementById("close");
     let resume = document.getElementById("resume");
+    let stepOver = document.getElementById("step-over");
+    let stepIn = document.getElementById("step-in");
+    let stepOut = document.getElementById("step-out");
     let frames = document.getElementById("stackframes");
 
     close.addEventListener("click", this._onCloseButtonClick, false);
     resume.addEventListener("click", this._onResumeButtonClick, false);
+    stepOver.addEventListener("click", this._onStepOverClick, false);
+    stepIn.addEventListener("click", this._onStepInClick, false);
+    stepOut.addEventListener("click", this._onStepOutClick, false);
     frames.addEventListener("scroll", this._onFramesScroll, false);
     window.addEventListener("resize", this._onFramesScroll, false);
 
     this._frames = frames;
   },
 
   /**
    * Destruction function, called when the debugger is shut down.
    */
   destroy: function DVF_destroy() {
     let close = document.getElementById("close");
     let resume = document.getElementById("resume");
+    let stepOver = document.getElementById("step-over");
+    let stepIn = document.getElementById("step-in");
+    let stepOut = document.getElementById("step-out");
     let frames = this._frames;
 
     close.removeEventListener("click", this._onCloseButtonClick, false);
     resume.removeEventListener("click", this._onResumeButtonClick, false);
+    stepOver.removeEventListener("click", this._onStepOverClick, false);
+    stepIn.removeEventListener("click", this._onStepInClick, false);
+    stepOut.removeEventListener("click", this._onStepOutClick, false);
     frames.removeEventListener("click", this._onFramesClick, false);
     frames.removeEventListener("scroll", this._onFramesScroll, false);
     window.removeEventListener("resize", this._onFramesScroll, false);
 
     this._frames = null;
   }
 };
 
@@ -1193,8 +1226,11 @@ DebuggerView.Scripts = {
   }
 };
 
 
 let DVF = DebuggerView.Stackframes;
 DVF._onFramesScroll = DVF._onFramesScroll.bind(DVF);
 DVF._onCloseButtonClick = DVF._onCloseButtonClick.bind(DVF);
 DVF._onResumeButtonClick = DVF._onResumeButtonClick.bind(DVF);
+DVF._onStepOverClick = DVF._onStepOverClick.bind(DVF);
+DVF._onStepInClick = DVF._onStepInClick.bind(DVF);
+DVF._onStepOutClick = DVF._onStepOutClick.bind(DVF);
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -68,16 +68,19 @@
     <xul:commandset id="editMenuCommands"/>
     <xul:commandset id="sourceEditorCommands"/>
     <xul:keyset id="sourceEditorKeys"/>
 
     <div id="body" class="vbox flex">
         <xul:toolbar id="dbg-toolbar">
             <xul:button id="close">&debuggerUI.closeButton;</xul:button>
             <xul:button id="resume"/>
+            <xul:button id="step-over">&debuggerUI.stepOverButton;</xul:button>
+            <xul:button id="step-in">&debuggerUI.stepInButton;</xul:button>
+            <xul:button id="step-out">&debuggerUI.stepOutButton;</xul:button>
             <xul:menulist id="scripts"/>
         </xul:toolbar>
         <div id="dbg-content" class="hbox flex">
             <div id="stack" class="vbox">
                 <div class="title unselectable">&debuggerUI.stackTitle;</div>
                 <div id="stackframes" class="vbox flex"></div>
             </div>
             <div id="script" class="vbox flex">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -14,16 +14,28 @@
 <!-- LOCALIZATION NOTE (debuggerMenu.commandkey): This is the command key that
   -  launches the debugger UI. Do not translate this one! -->
 <!ENTITY debuggerMenu.commandkey     "S">
 
 <!-- LOCALIZATION NOTE (debuggerUI.closeButton): This is the label for the
   -  button that closes the debugger UI. -->
 <!ENTITY debuggerUI.closeButton      "Close">
 
+<!-- LOCALIZATION NOTE (debuggerUI.stepOverButton): This is the label for the
+  -  button that steps over a function call. -->
+<!ENTITY debuggerUI.stepOverButton   "Step Over">
+
+<!-- LOCALIZATION NOTE (debuggerUI.stepInButton): This is the label for the
+  -  button that steps into a function call. -->
+<!ENTITY debuggerUI.stepInButton     "Step In">
+
+<!-- LOCALIZATION NOTE (debuggerUI.stepOutButton): This is the label for the
+  -  button that steps out of a function call. -->
+<!ENTITY debuggerUI.stepOutButton    "Step Out">
+
 <!-- LOCALIZATION NOTE (debuggerUI.stackTitle): This is the label for the
   -  widget that displays the call stack frames in the debugger. -->
 <!ENTITY debuggerUI.stackTitle       "Call stack">
 
 <!-- LOCALIZATION NOTE (debuggerUI.scriptTitle): This is the label for the
   -  widget that displays the source code for the script that is currently
   -  being inspected in the debugger. -->
 <!ENTITY debuggerUI.scriptTitle      "Script">
--- a/toolkit/devtools/debugger/dbg-client.jsm
+++ b/toolkit/devtools/debugger/dbg-client.jsm
@@ -536,42 +536,77 @@ ThreadClient.prototype = {
 
   _assertPaused: function TC_assertPaused(aCommand) {
     if (!this.paused) {
       throw aCommand + " command sent while not paused.";
     }
   },
 
   /**
-   * Resume a paused thread.
+   * Resume a paused thread. If the optional aLimit parameter is present, then
+   * the thread will also pause when that limit is reached.
    *
    * @param function aOnResponse
    *        Called with the response packet.
+   * @param [optional] object aLimit
+   *        An object with a type property set to the appropriate limit (next,
+   *        step, or finish) per the remote debugging protocol specification.
    */
-  resume: function TC_resume(aOnResponse) {
+  resume: function TC_resume(aOnResponse, aLimit) {
     this._assertPaused("resume");
 
     // Put the client in a tentative "resuming" state so we can prevent
     // further requests that should only be sent in the paused state.
     this._state = "resuming";
 
     let self = this;
-    let packet = { to: this._actor, type: DebugProtocolTypes.resume };
+    let packet = { to: this._actor, type: DebugProtocolTypes.resume,
+                   resumeLimit: aLimit };
     this._client.request(packet, function(aResponse) {
       if (aResponse.error) {
         // There was an error resuming, back to paused state.
         self._state = "paused";
       }
       if (aOnResponse) {
         aOnResponse(aResponse);
       }
     });
   },
 
   /**
+   * Step over a function call.
+   *
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  stepOver: function TC_stepOver(aOnResponse) {
+    this.resume(aOnResponse, { type: "next" });
+  },
+
+  /**
+   * Step into a function call.
+   *
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  stepIn: function TC_stepIn(aOnResponse) {
+    this.resume(aOnResponse, { type: "step" });
+  },
+
+  /**
+   * Step out of a function call.
+   *
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  stepOut: function TC_stepOut(aOnResponse) {
+    this.resume(aOnResponse, { type: "finish" });
+  },
+
+  /**
    * Interrupt a running thread.
    *
    * @param function aOnResponse
    *        Called with the response packet.
    */
   interrupt: function TC_interrupt(aOnResponse) {
     let packet = { to: this._actor, type: DebugProtocolTypes.interrupt };
     this._client.request(packet, function(aResponse) {
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -192,22 +192,140 @@ ThreadActor.prototype = {
     }
   },
 
   onDetach: function TA_onDetach(aRequest) {
     this.disconnect();
     return { type: "detached" };
   },
 
+  /**
+   * Pause the debuggee, by entering a nested event loop, and return a 'paused'
+   * packet to the client.
+   *
+   * @param Debugger.Frame aFrame
+   *        The newest debuggee frame in the stack.
+   * @param object aReason
+   *        An object with a 'type' property containing the reason for the pause.
+   */
+  _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason) {
+    try {
+      let packet = this._paused(aFrame);
+      if (!packet) {
+        return undefined;
+      }
+      packet.why = aReason;
+      this.conn.send(packet);
+      return this._nest();
+    } catch(e) {
+      Cu.reportError("Got an exception during TA__pauseAndRespond: " + e +
+                     ": " + e.stack);
+      return undefined;
+    }
+  },
+
+  /**
+   * Handle a protocol request to resume execution of the debuggee.
+   */
   onResume: function TA_onResume(aRequest) {
+    if (aRequest && aRequest.forceCompletion) {
+      // TODO: remove this when Debugger.Frame.prototype.pop is implemented in
+      // bug 736733.
+      if (typeof this.frame.pop != "function") {
+        return { error: "notImplemented",
+                 message: "forced completion is not yet implemented." };
+      }
+
+      this.dbg.getNewestFrame().pop(aRequest.completionValue);
+      let packet = this._resumed();
+      DebuggerServer.xpcInspector.exitNestedEventLoop();
+      return { type: "resumeLimit", frameFinished: aRequest.forceCompletion };
+    }
+
+    if (aRequest && aRequest.resumeLimit) {
+      // Bind these methods because some of the hooks are called with 'this'
+      // set to the current frame.
+      let pauseAndRespond = this._pauseAndRespond.bind(this);
+      let createValueGrip = this.createValueGrip.bind(this);
+
+      let startFrame = this._youngestFrame;
+      let startLine;
+      if (this._youngestFrame.script) {
+        let offset = this._youngestFrame.offset;
+        startLine = this._youngestFrame.script.getOffsetLine(offset);
+      }
+
+      // Define the JS hook functions for stepping.
+
+      let onEnterFrame = function TA_onEnterFrame(aFrame) {
+        return pauseAndRespond(aFrame, { type: "resumeLimit" });
+      };
+
+      let onPop = function TA_onPop(aCompletion) {
+        // onPop is called with 'this' set to the current frame.
+
+        // Note that we're popping this frame; we need to watch for
+        // subsequent step events on its caller.
+        this.reportedPop = true;
+
+        return pauseAndRespond(this, { type: "resumeLimit" });
+      }
+
+      let onStep = function TA_onStep() {
+        // onStep is called with 'this' set to the current frame.
+
+        // If we've changed frame or line, then report that.
+        if (this !== startFrame ||
+            (this.script &&
+             this.script.getOffsetLine(this.offset) != startLine)) {
+          return pauseAndRespond(this, { type: "resumeLimit" });
+        }
+
+        // Otherwise, let execution continue.
+        return undefined;
+      }
+
+      switch (aRequest.resumeLimit.type) {
+        case "step":
+          this.dbg.onEnterFrame = onEnterFrame;
+          // Fall through.
+        case "next":
+          let stepFrame = this._getNextStepFrame(startFrame);
+          if (stepFrame) {
+            stepFrame.onStep = onStep;
+            stepFrame.onPop = onPop;
+          }
+          break;
+        case "finish":
+          stepFrame = this._getNextStepFrame(startFrame);
+          if (stepFrame) {
+            stepFrame.onPop = onPop;
+          }
+          break;
+        default:
+          return { error: "badParameterType",
+                   message: "Unknown resumeLimit type" };
+      }
+    }
     let packet = this._resumed();
     DebuggerServer.xpcInspector.exitNestedEventLoop();
     return packet;
   },
 
+  /**
+   * Helper method that returns the next frame when stepping.
+   */
+  _getNextStepFrame: function TA__getNextStepFrame(aFrame) {
+    let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame;
+    if (!stepFrame || !stepFrame.script) {
+      stepFrame = null;
+    }
+    return stepFrame;
+  },
+
   onClientEvaluate: function TA_onClientEvaluate(aRequest) {
     if (this.state !== "paused") {
       return { error: "wrongState",
                message: "Debuggee must be paused to evaluate code." };
     };
 
     let frame = this._requestFrame(aRequest.frame);
     if (!frame) {
@@ -467,16 +585,23 @@ ThreadActor.prototype = {
     // 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") {
       return undefined;
     }
 
+    // Clear stepping hooks.
+    this.dbg.onEnterFrame = undefined;
+    if (aFrame) {
+      aFrame.onStep = undefined;
+      aFrame.onPop = undefined;
+    }
+
     this._state = "paused";
 
     // Save the pause frame (if any) as the youngest frame for
     // stack viewing.
     this._youngestFrame = aFrame;
 
     // Create the actor pool that will hold the pause actor and its
     // children.
@@ -725,29 +850,17 @@ ThreadActor.prototype = {
   /**
    * A function that the engine calls when a debugger statement has been
    * executed in the specified frame.
    *
    * @param aFrame Debugger.Frame
    *        The stack frame that contained the debugger statement.
    */
   onDebuggerStatement: function TA_onDebuggerStatement(aFrame) {
-    try {
-      let packet = this._paused(aFrame);
-      if (!packet) {
-        return undefined;
-      }
-      packet.why = { type: "debuggerStatement" };
-      this.conn.send(packet);
-      return this._nest();
-    } catch(e) {
-      Cu.reportError("Got an exception during onDebuggerStatement: " + e +
-                     ": " + e.stack);
-      return undefined;
-    }
+    return this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
   },
 
   /**
    * A function that the engine calls when a new script has been loaded into a
    * debuggee compartment. If the new code is part of a function, aFunction is
    * a Debugger.Object reference to the function object. (Not all code is part
    * of a function; for example, the code appearing in a <script> tag that is
    * outside of any functions defined in that tag would be passed to
@@ -1148,18 +1261,30 @@ FrameActor.prototype = {
 
   /**
    * Handle a protocol request to pop this frame from the stack.
    *
    * @param aRequest object
    *        The protocol request object.
    */
   onPop: function FA_onPop(aRequest) {
-    return { error: "notImplemented",
-             message: "Popping frames is not yet implemented." };
+    // TODO: remove this when Debugger.Frame.prototype.pop is implemented
+    if (typeof this.frame.pop != "function") {
+      return { error: "notImplemented",
+               message: "Popping frames is not yet implemented." };
+    }
+
+    while (this.frame != this.threadActor.dbg.getNewestFrame()) {
+      this.threadActor.dbg.getNewestFrame().pop();
+    }
+    this.frame.pop(aRequest.completionValue);
+
+    // TODO: return the watches property when frame pop watch actors are
+    // implemented.
+    return { from: this.actorID };
   }
 };
 
 FrameActor.prototype.requestTypes = {
   "pop": FrameActor.prototype.onPop,
 };
 
 
@@ -1184,29 +1309,19 @@ BreakpointActor.prototype = {
 
   /**
    * A function that the engine calls when a breakpoint has been hit.
    *
    * @param aFrame Debugger.Frame
    *        The stack frame that contained the breakpoint.
    */
   hit: function BA_hit(aFrame) {
-    try {
-      let packet = this.threadActor._paused(aFrame);
-      if (!packet) {
-        return undefined;
-      }
-      // TODO: add the rest of the breakpoints on that line.
-      packet.why = { type: "breakpoint", actors: [ this.actorID ] };
-      this.conn.send(packet);
-      return this.threadActor._nest();
-    } catch(e) {
-      Cu.reportError("Got an exception during hit: " + e + ': ' + e.stack);
-      return undefined;
-    }
+    // TODO: add the rest of the breakpoints on that line (bug 676602).
+    let reason = { type: "breakpoint", actors: [ this.actorID ] };
+    return this.threadActor._pauseAndRespond(aFrame, reason);
   },
 
   /**
    * Handle a protocol request to remove this breakpoint.
    *
    * @param aRequest object
    *        The protocol request object.
    */
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-01.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-01.js
@@ -10,32 +10,37 @@ var gClient;
 var gThreadClient;
 
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function () {
-    attachTestGlobalClientAndResume(gClient, "test-stack", function (aResponse, aThreadClient) {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_breakpoint();
     });
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     let path = getFilePath('test_breakpoint-01.js');
-    gThreadClient.setBreakpoint({ url: path, line: gDebuggee.line0 + 3}, function (aResponse, bpClient) {
+    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);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-02.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-02.js
@@ -10,39 +10,44 @@ var gClient;
 var gThreadClient;
 
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-stack");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function () {
-    attachTestGlobalClientAndResume(gClient, "test-stack", function (aResponse, aThreadClient) {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
       gThreadClient = aThreadClient;
       test_breakpoint_running();
     });
   });
   do_test_pending();
 }
 
 function test_breakpoint_running()
 {
   let path = getFilePath('test_breakpoint-01.js');
+  let location = { url: path, line: gDebuggee.line0 + 3};
 
   gDebuggee.eval("var line0 = Error().lineNumber;\n" +
                  "var a = 1;\n" +  // line0 + 1
                  "var b = 2;\n");  // line0 + 2
 
   // Setting the breakpoint later should interrupt the debuggee.
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     do_check_eq(aPacket.type, "paused");
+    do_check_eq(aPacket.frame.where.url, path);
+    do_check_eq(aPacket.frame.where.line, location);
     do_check_eq(aPacket.why.type, "interrupted");
   });
 
-  gThreadClient.setBreakpoint({ url: path, line: gDebuggee.line0 + 3}, function(aResponse) {
+  gThreadClient.setBreakpoint(location, function(aResponse) {
     // Eval scripts don't stick around long enough for the breakpoint to be set,
     // so just make sure we got the expected response from the actor.
     do_check_eq(aResponse.error, "noScript");
 
     do_execute_soon(function() {
       finishClient(gClient);
     });
   });
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-03.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-03.js
@@ -32,16 +32,18 @@ function test_skip_breakpoint()
     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_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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
@@ -31,16 +31,18 @@ function test_child_breakpoint()
     let path = getFilePath('test_breakpoint-04.js');
     let location = { url: path, line: gDebuggee.line0 + 3};
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       // actualLocation is not returned when breakpoints don't skip forward.
       do_check_eq(aResponse.actualLocation, undefined);
       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);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-05.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-05.js
@@ -33,16 +33,18 @@ function test_child_skip_breakpoint()
     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_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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-06.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-06.js
@@ -33,16 +33,18 @@ function test_nested_breakpoint()
     let location = { url: path, line: gDebuggee.line0 + 5};
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       // Check that the breakpoint has properly skipped forward one line.
       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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-07.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-07.js
@@ -33,16 +33,18 @@ function test_second_child_skip_breakpoi
     let location = { url: path, line: gDebuggee.line0 + 6};
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       // Check that the breakpoint has properly skipped forward one line.
       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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-08.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-08.js
@@ -33,16 +33,18 @@ function test_child_skip_breakpoint()
     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_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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-09.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-09.js
@@ -27,28 +27,34 @@ function run_test()
 
 function test_remove_breakpoint()
 {
   let done = false;
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     let path = getFilePath('test_breakpoint-09.js');
     let location = { url: path, line: gDebuggee.line0 + 1};
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
+      // Check that the breakpoint has properly skipped forward one line.
+      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");
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           done = true;
-          gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+          gThreadClient.addOneTimeListener("paused",
+                                           function (aEvent, aPacket) {
             // The breakpoint should not be hit again.
             gThreadClient.resume(function () {
               do_check_true(false);
             });
           });
           gThreadClient.resume();
         });
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_stepping-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check basic step-over functionality.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_simple_stepping();
+    });
+  });
+  do_test_pending();
+}
+
+function test_simple_stepping()
+{
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+      // Check the return value.
+      do_check_eq(aPacket.type, "paused");
+      do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 2);
+      do_check_eq(aPacket.why.type, "resumeLimit");
+      // Check that stepping worked.
+      do_check_eq(gDebuggee.a, undefined);
+      do_check_eq(gDebuggee.b, undefined);
+
+      gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+        // Check the return value.
+        do_check_eq(aPacket.type, "paused");
+        do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
+        do_check_eq(aPacket.why.type, "resumeLimit");
+        // Check that stepping worked.
+        do_check_eq(gDebuggee.a, 1);
+        do_check_eq(gDebuggee.b, undefined);
+
+        gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+          // Check the return value.
+          do_check_eq(aPacket.type, "paused");
+          // When leaving a stack frame the line number doesn't change.
+          do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
+          do_check_eq(aPacket.why.type, "resumeLimit");
+          // Check that stepping worked.
+          do_check_eq(gDebuggee.a, 1);
+          do_check_eq(gDebuggee.b, 2);
+
+          gThreadClient.resume(function () {
+            finishClient(gClient);
+          });
+        });
+        gThreadClient.stepOver();
+      });
+      gThreadClient.stepOver();
+
+    });
+    gThreadClient.stepOver();
+
+  });
+
+  gDebuggee.eval("var line0 = Error().lineNumber;\n" +
+                 "debugger;\n" +   // line0 + 1
+                 "var a = 1;\n" +  // line0 + 2
+                 "var b = 2;\n");  // line0 + 3
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_stepping-02.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check basic step-in functionality.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_simple_stepping();
+    });
+  });
+  do_test_pending();
+}
+
+function test_simple_stepping()
+{
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+      // Check the return value.
+      do_check_eq(aPacket.type, "paused");
+      do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 2);
+      do_check_eq(aPacket.why.type, "resumeLimit");
+      // Check that stepping worked.
+      do_check_eq(gDebuggee.a, undefined);
+      do_check_eq(gDebuggee.b, undefined);
+
+      gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+        // Check the return value.
+        do_check_eq(aPacket.type, "paused");
+        do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
+        do_check_eq(aPacket.why.type, "resumeLimit");
+        // Check that stepping worked.
+        do_check_eq(gDebuggee.a, 1);
+        do_check_eq(gDebuggee.b, undefined);
+
+        gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+          // Check the return value.
+          do_check_eq(aPacket.type, "paused");
+          // When leaving a stack frame the line number doesn't change.
+          do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
+          do_check_eq(aPacket.why.type, "resumeLimit");
+          // Check that stepping worked.
+          do_check_eq(gDebuggee.a, 1);
+          do_check_eq(gDebuggee.b, 2);
+
+          gThreadClient.resume(function () {
+            finishClient(gClient);
+          });
+        });
+        gThreadClient.stepIn();
+      });
+      gThreadClient.stepIn();
+
+    });
+    gThreadClient.stepIn();
+
+  });
+
+  gDebuggee.eval("var line0 = Error().lineNumber;\n" +
+                 "debugger;\n" +   // line0 + 1
+                 "var a = 1;\n" +  // line0 + 2
+                 "var b = 2;\n");  // line0 + 3
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_stepping-03.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check basic step-out functionality.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_simple_stepping();
+    });
+  });
+  do_test_pending();
+}
+
+function test_simple_stepping()
+{
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+      // Check the return value.
+      do_check_eq(aPacket.type, "paused");
+      do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 4);
+      do_check_eq(aPacket.why.type, "resumeLimit");
+      // Check that stepping worked.
+      do_check_eq(gDebuggee.a, 1);
+      do_check_eq(gDebuggee.b, 2);
+
+      gThreadClient.resume(function () {
+        finishClient(gClient);
+      });
+    });
+    gThreadClient.stepOut();
+
+  });
+
+  gDebuggee.eval("var line0 = Error().lineNumber;\n" +
+                 "function f() {\n" + // line0 + 1
+                 "  debugger;\n" +    // line0 + 2
+                 "  this.a = 1;\n" +  // line0 + 3
+                 "  this.b = 2;\n" +  // line0 + 4
+                 "}\n" +              // line0 + 5
+                 "f();\n");           // line0 + 6
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_stepping-04.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that stepping over a function call does not pause inside the function.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestGlobalClientAndResume(gClient,
+                                    "test-stack",
+                                    function (aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_simple_stepping();
+    });
+  });
+  do_test_pending();
+}
+
+function test_simple_stepping()
+{
+  gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+    gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+      // Check the return value.
+      do_check_eq(aPacket.type, "paused");
+      do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5);
+      do_check_eq(aPacket.why.type, "resumeLimit");
+      // Check that stepping worked.
+      do_check_eq(gDebuggee.a, undefined);
+      do_check_eq(gDebuggee.b, undefined);
+
+      gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+        // Check the return value.
+        do_check_eq(aPacket.type, "paused");
+        do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6);
+        do_check_eq(aPacket.why.type, "resumeLimit");
+        // Check that stepping worked.
+        do_check_eq(gDebuggee.a, 1);
+        do_check_eq(gDebuggee.b, undefined);
+
+        gThreadClient.resume(function () {
+          finishClient(gClient);
+        });
+      });
+      gThreadClient.stepOver();
+
+    });
+    gThreadClient.stepOver();
+
+  });
+
+  gDebuggee.eval("var line0 = Error().lineNumber;\n" +
+                 "function f() {\n" + // line0 + 1
+                 "  this.a = 1;\n" +  // line0 + 2
+                 "}\n" +              // line0 + 3
+                 "debugger;\n" +      // line0 + 4
+                 "f();\n" +           // line0 + 5
+                 "let b = 2;\n");     // line0 + 6
+}
--- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini
@@ -44,8 +44,12 @@ tail =
 [test_breakpoint-09.js]
 [test_breakpoint-10.js]
 [test_listscripts-01.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_interrupt.js]
+[test_stepping-01.js]
+[test_stepping-02.js]
+[test_stepping-03.js]
+[test_stepping-04.js]