Revert "Backed out changeset c209b1927107 (bug 923975) for failing at mochitest/browser_dbg_rr_stepping-02.js, etc. a=backout"
☠☠ backed out by 5dce4548074f ☠ ☠
authorJason Laster <jlaster@mozilla.com>
Tue, 02 Oct 2018 14:40:47 -0400
changeset 495046 3599e95a53b7c3cdfe7b6ee049afe71e24713a9a
parent 495003 0fd47bc8f7c2758ce9b9d47e860054be0b2260a5
child 495047 9ef38ce418186b95b50cdddb9ea38bb5a7291050
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs923975
milestone64.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
Revert "Backed out changeset c209b1927107 (bug 923975) for failing at mochitest/browser_dbg_rr_stepping-02.js, etc. a=backout" This reverts commit 203706dd9c9fc63ee67e88274ea604ff9a1cd492.
devtools/client/debugger/new/test/mochitest/browser_dbg-stepping.js
devtools/client/debugger/new/test/mochitest/browser_dbg_rr_stepping-02.js
devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
devtools/client/debugger/test/mochitest/browser_dbg_step-out.js
devtools/server/actors/thread.js
devtools/server/tests/unit/head_dbg.js
devtools/server/tests/unit/test_breakpoint-13.js
devtools/server/tests/unit/test_breakpoint-14.js
devtools/server/tests/unit/test_stepping-01.js
devtools/server/tests/unit/test_stepping-03.js
devtools/server/tests/unit/test_stepping-06.js
devtools/server/tests/unit/test_stepping-08.js
devtools/server/tests/unit/xpcshell.ini
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-stepping.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-stepping.js
@@ -19,11 +19,11 @@ add_task(async function test() {
   await stepIn(dbg);
   await stepIn(dbg);
   await stepIn(dbg);
   await stepIn(dbg);
   await stepIn(dbg);
   await stepIn(dbg);
   await stepIn(dbg);
 
-  assertDebugLine(dbg, 42308);
+  assertDebugLine(dbg, 42267);
   assertPausedLocation(dbg);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_stepping-02.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_stepping-02.js
@@ -16,15 +16,15 @@ async function test() {
   await client.interrupt();
   await setBreakpoint(client, "doc_rr_basic.html", 22);
   await rewindToLine(client, 22);
   await stepInToLine(client, 25);
   await stepOverToLine(client, 26);
   await stepOverToLine(client, 27);
   await reverseStepInToLine(client, 33);
   await reverseStepOverToLine(client, 32);
-  await reverseStepOutToLine(client, 26);
-  await reverseStepOverToLine(client, 25);
+  await reverseStepOutToLine(client, 23);
+  await reverseStepOverToLine(client, 22);
 
   await toolbox.destroy();
   await gBrowser.removeTab(tab);
   finish();
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
@@ -44,22 +44,23 @@ function test() {
 
     // Information for sub-tests. When 'key' is synthesized 'keyRepeat' times,
     // cursor should be at 'caretLine' of this test..
     let stepTests = [
       {key: "KEY_F11", keyRepeat: 1, caretLine: 16},
       {key: "KEY_F11", keyRepeat: 2, caretLine: 18},
       {key: "KEY_F11", keyRepeat: 2, caretLine: 27},
       {key: "KEY_F10", keyRepeat: 1, caretLine: 27},
-      {key: "KEY_F11", keyRepeat: 1, caretLine: 18},
-      {key: "KEY_F11", keyRepeat: 5, caretLine: 32},
-      {key: "KEY_F11", modifier:"Shift", keyRepeat: 1, caretLine: 29},
-      {key: "KEY_F11", modifier:"Shift", keyRepeat: 2, caretLine: 34},
-      {key: "KEY_F11", modifier:"Shift", keyRepeat: 2, caretLine: 34}
+      {key: "KEY_F11", keyRepeat: 1, caretLine: 19},
+      {key: "KEY_F11", keyRepeat: 5, caretLine: 29},
+      {key: "KEY_F11", modifier:"Shift", keyRepeat: 1, caretLine: 32},
+      {key: "KEY_F11", modifier:"Shift", keyRepeat: 1, caretLine: 34},
+      {key: "KEY_F11", modifier:"Shift", keyRepeat: 1, caretLine: 34}
     ];
+
     // Trigger script that stops at debugger statement
     executeSoon(() => generateMouseClickInTab(gTab,
       "content.document.getElementById('start')"));
     yield waitForPause(gThreadClient);
 
     // Focus the console and add event listener to track whether it loses focus
     // (Must happen after generateMouseClickInTab() call)
     let consoleLostFocus = false;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_step-out.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_step-out.js
@@ -49,28 +49,17 @@ function testNormalReturn() {
       gDebugger);
   });
 
   generateMouseClickInTab(gTab, "content.document.getElementById('return')");
 }
 
 function testReturnWithException() {
   waitForCaretAndScopes(gPanel, 24).then(() => {
-    waitForCaretAndScopes(gPanel, 26).then(() => {
-      let innerScope = gVars.getScopeAtIndex(0);
-      let exceptionVar = innerScope.get("<exception>");
-
-      is(exceptionVar.name, "<exception>",
-        "Should have the right property name for the returned value.");
-      is(exceptionVar.value, "boom",
-        "Should have the right property value for the returned value.");
-      ok(exceptionVar._internalItem, "Should be an internal item");
-      ok(exceptionVar._target.hasAttribute("pseudo-item"),
-         "Element should be marked as a pseudo-item");
-
+    waitForCaretAndScopes(gPanel, 31).then(() => {
       resumeDebuggee().then(() => closeDebuggerAndFinish(gPanel));
     });
 
     EventUtils.sendMouseEvent({ type: "mousedown" },
       gDebugger.document.getElementById("step-out"),
       gDebugger);
   });
 
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -83,16 +83,17 @@ const ThreadActor = ActorClassWithSpec(t
 
     this.global = global;
 
     this._allEventsListener = this._allEventsListener.bind(this);
     this.onNewSourceEvent = this.onNewSourceEvent.bind(this);
     this.onUpdatedSourceEvent = this.onUpdatedSourceEvent.bind(this);
 
     this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
+    this.createCompletionGrip = this.createCompletionGrip.bind(this);
     this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
     this.onNewScript = this.onNewScript.bind(this);
     this.objectGrip = this.objectGrip.bind(this);
     this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
     this._onWindowReady = this._onWindowReady.bind(this);
     this._onOpeningRequest = this._onOpeningRequest.bind(this);
     EventEmitter.on(this._parent, "window-ready", this._onWindowReady);
 
@@ -517,45 +518,53 @@ const ThreadActor = ActorClassWithSpec(t
       if (this.sources.isBlackBoxed(url)) {
         return undefined;
       }
 
       return pauseAndRespond(frame);
     };
   },
 
-  _makeOnPop: function({ thread, pauseAndRespond, createValueGrip: createValueGripHook,
-                          startLocation }) {
+  _makeOnPop: function({ thread, pauseAndRespond, startLocation, steppingType }) {
     const result = function(completion) {
       // onPop is called with 'this' set to the current frame.
       const generatedLocation = thread.sources.getFrameLocation(this);
-      const { originalSourceActor } = thread.unsafeSynchronize(
+      const originalLocation = thread.unsafeSynchronize(
         thread.sources.getOriginalLocation(generatedLocation)
       );
 
+      const { originalSourceActor } = originalLocation;
       const url = originalSourceActor.url;
 
       if (thread.sources.isBlackBoxed(url)) {
         return undefined;
       }
 
       // Note that we're popping this frame; we need to watch for
       // subsequent step events on its caller.
       this.reportedPop = true;
 
+      if (steppingType == "finish") {
+        const parentFrame = thread._getNextStepFrame(this);
+        if (parentFrame && parentFrame.script) {
+          const { onStep } = thread._makeSteppingHooks(
+            originalLocation, "next", false, completion
+          );
+          parentFrame.onStep = onStep;
+          return undefined;
+        }
+      }
+
       return pauseAndRespond(this, packet => {
-        packet.why.frameFinished = {};
-        if (!completion) {
-          packet.why.frameFinished.terminated = true;
-        } else if (completion.hasOwnProperty("return")) {
-          packet.why.frameFinished.return = createValueGripHook(completion.return);
-        } else if (completion.hasOwnProperty("yield")) {
-          packet.why.frameFinished.return = createValueGripHook(completion.yield);
+        if (completion) {
+          thread.createCompletionGrip(packet, completion);
         } else {
-          packet.why.frameFinished.throw = createValueGripHook(completion.throw);
+          packet.why.frameFinished = {
+            terminated: true
+          };
         }
         return packet;
       });
     };
 
     // When stepping out, we don't want to stop at a breakpoint that
     // happened to be set exactly at the spot where we stepped out.
     // See bug 970469.  We record the original location here and check
@@ -564,17 +573,17 @@ const ThreadActor = ActorClassWithSpec(t
     // frame, if we did we'd also have to find the appropriate spot to
     // clear it.
     result.originalLocation = startLocation;
 
     return result;
   },
 
   // Return whether reaching a script offset should be considered a distinct
-  // "step" from another location in the same frame.
+  // "step" from another location.
   _intraFrameLocationIsStepTarget: function(startLocation, script, offset) {
     // Only allow stepping stops at entry points for the line.
     if (!script.getOffsetLocation(offset).isEntryPoint) {
       return false;
     }
 
     // Cases when we have executed enough within a frame to consider a "step"
     // to have occured:
@@ -616,17 +625,17 @@ const ThreadActor = ActorClassWithSpec(t
     }
 
     // NOTE: if we do not find a pause point we want to
     // fall back on the old behavior (Case 3)
     return lineChanged;
   },
 
   _makeOnStep: function({ thread, pauseAndRespond, startFrame,
-                          startLocation, steppingType }) {
+                          startLocation, steppingType, completion }) {
     // Breaking in place: we should always pause.
     if (steppingType === "break") {
       return () => pauseAndRespond(this);
     }
 
     // Otherwise take what a "step" means into consideration.
     return function() {
       // onStep is called with 'this' set to the current frame.
@@ -641,33 +650,50 @@ const ThreadActor = ActorClassWithSpec(t
       // 1. We are in a source mapped region, but inside a null mapping
       //    (doesn't correlate to any region of original source)
       // 2. The source we are in is black boxed.
       if (newLocation.originalUrl == null
           || thread.sources.isBlackBoxed(newLocation.originalUrl)) {
         return undefined;
       }
 
-      // A step has occurred if we have changed frames.
-      if (this !== startFrame) {
-        return pauseAndRespond(this);
-      }
-
       // A step has occurred if we reached a step target.
       if (thread._intraFrameLocationIsStepTarget(startLocation,
                                                  this.script, this.offset)) {
-        return pauseAndRespond(this);
+        return pauseAndRespond(
+          this,
+          packet => thread.createCompletionGrip(packet, completion)
+        );
       }
 
       // Otherwise, let execution continue (we haven't executed enough code to
       // consider this a "step" yet).
       return undefined;
     };
   },
 
+  createCompletionGrip: function(packet, completion) {
+    if (!completion) {
+      return packet;
+    }
+
+    const createGrip = value => createValueGrip(value, this._pausePool, this.objectGrip);
+    packet.why.frameFinished = {};
+
+    if (completion.hasOwnProperty("return")) {
+      packet.why.frameFinished.return = createGrip(completion.return);
+    } else if (completion.hasOwnProperty("yield")) {
+      packet.why.frameFinished.return = createGrip(completion.yield);
+    } else if (completion.hasOwnProperty("throw")) {
+      packet.why.frameFinished.throw = createGrip(completion.throw);
+    }
+
+    return packet;
+  },
+
   /**
    * When replaying, we need to specify the offsets where a frame's onStep hook
    * should fire. Given that we are stepping forward (rewind == false) or
    * backwards (rewinding == true), return an array of all the step targets
    * that could be reached next from startLocation.
    */
   _findReplayingStepOffsets: function(startLocation, frame, rewinding) {
     const worklist = [frame.offset], seen = [], result = [];
@@ -691,33 +717,33 @@ const ThreadActor = ActorClassWithSpec(t
       }
     }
     return result;
   },
 
   /**
    * Define the JS hook functions for stepping.
    */
-  _makeSteppingHooks: function(startLocation, steppingType, rewinding) {
+  _makeSteppingHooks: function(startLocation, steppingType, rewinding, completion) {
     // Bind these methods and state because some of the hooks are called
     // with 'this' set to the current frame. Rather than repeating the
     // binding in each _makeOnX method, just do it once here and pass it
     // in to each function.
     const steppingHookState = {
       pauseAndRespond: (frame, onPacket = k=>k) => this._pauseAndRespond(
         frame,
         { type: "resumeLimit" },
         onPacket
       ),
-      createValueGrip: v => createValueGrip(v, this._pausePool, this.objectGrip),
       thread: this,
       startFrame: this.youngestFrame,
       startLocation: startLocation,
       steppingType: steppingType,
-      rewinding: rewinding
+      rewinding: rewinding,
+      completion
     };
 
     return {
       onEnterFrame: this._makeOnEnterFrame(steppingHookState),
       onPop: this._makeOnPop(steppingHookState),
       onStep: this._makeOnStep(steppingHookState)
     };
   },
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -841,8 +841,37 @@ function getInflatedStackLocations(threa
     const frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
     locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
     stackIndex = stackEntry[STACK_PREFIX_SLOT];
   }
 
   // The profiler tree is inverted, so reverse the array.
   return locations.reverse();
 }
+
+async function setupTestFromUrl(url) {
+  do_test_pending();
+
+  const { createRootActor } = require("xpcshell-test/testactors");
+  DebuggerServer.setRootActor(createRootActor);
+  DebuggerServer.init(() => true);
+
+  const global = createTestGlobal("test");
+  DebuggerServer.addTestGlobal(global);
+
+  const debuggerClient = new DebuggerClient(DebuggerServer.connectPipe());
+  await connect(debuggerClient);
+
+  const { tabs } = await listTabs(debuggerClient);
+  const tab = findTab(tabs, "test");
+  const [, tabClient] = await attachTarget(debuggerClient, tab);
+
+  const [, threadClient] = await attachThread(tabClient);
+  await resume(threadClient);
+
+  const sourceUrl = getFileUrl(url);
+  const promise = waitForNewSource(threadClient, sourceUrl);
+  loadSubScript(sourceUrl, global);
+  const { source } = await promise;
+
+  const sourceClient = threadClient.source(source);
+  return { global, debuggerClient, threadClient, sourceClient };
+}
--- a/devtools/server/tests/unit/test_breakpoint-13.js
+++ b/devtools/server/tests/unit/test_breakpoint-13.js
@@ -63,28 +63,23 @@ function test_simple_breakpoint() {
           // Check that the breakpoint wasn't the reason for this pause, but
           // that the frame is about to be popped while stepping.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 3);
           Assert.notEqual(packet.why.type, "breakpoint");
           Assert.equal(packet.why.type, "resumeLimit");
           Assert.equal(packet.why.frameFinished.return.type, "undefined");
         },
         function(packet) {
-          // The foo function call frame was just popped from the stack.
+          // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(gDebuggee.a, 1);
           Assert.equal(gDebuggee.b, undefined);
-          Assert.equal(packet.frame.where.line, gDebuggee.line0 + 5);
-          Assert.equal(packet.why.type, "resumeLimit");
-          Assert.equal(packet.poppedFrames.length, 1);
-        },
-        function(packet) {
-          // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 6);
           Assert.notEqual(packet.why.type, "debuggerStatement");
           Assert.equal(packet.why.type, "resumeLimit");
+          Assert.equal(packet.poppedFrames.length, 1);
         },
         function(packet) {
           // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 7);
           Assert.notEqual(packet.why.type, "debuggerStatement");
           Assert.equal(packet.why.type, "resumeLimit");
         },
       ];
--- a/devtools/server/tests/unit/test_breakpoint-14.js
+++ b/devtools/server/tests/unit/test_breakpoint-14.js
@@ -61,28 +61,23 @@ function test_simple_breakpoint() {
         function(packet) {
           // The frame is about to be popped while stepping.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 3);
           Assert.notEqual(packet.why.type, "breakpoint");
           Assert.equal(packet.why.type, "resumeLimit");
           Assert.equal(packet.why.frameFinished.return.type, "undefined");
         },
         function(packet) {
-          // The foo function call frame was just popped from the stack.
+          // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(gDebuggee.a, 1);
           Assert.equal(gDebuggee.b, undefined);
-          Assert.equal(packet.frame.where.line, gDebuggee.line0 + 5);
-          Assert.equal(packet.why.type, "resumeLimit");
-          Assert.equal(packet.poppedFrames.length, 1);
-        },
-        function(packet) {
-          // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 6);
           Assert.notEqual(packet.why.type, "debuggerStatement");
           Assert.equal(packet.why.type, "resumeLimit");
+          Assert.equal(packet.poppedFrames.length, 1);
         },
         function(packet) {
           // Check that the debugger statement wasn't the reason for this pause.
           Assert.equal(packet.frame.where.line, gDebuggee.line0 + 7);
           Assert.notEqual(packet.why.type, "debuggerStatement");
           Assert.equal(packet.why.type, "resumeLimit");
         },
       ];
--- a/devtools/server/tests/unit/test_stepping-01.js
+++ b/devtools/server/tests/unit/test_stepping-01.js
@@ -1,81 +1,91 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint-disable no-shadow, max-nested-callbacks */
 
 "use strict";
 
 /**
- * Check basic step-over functionality.
+ * Check scenarios where we're leaving function a and
+ * going to the function b's call-site.
  */
 
-var gDebuggee;
-var gClient;
-var gCallback;
+async function testFinish({threadClient, debuggerClient}) {
+  await resume(threadClient);
+  await close(debuggerClient);
+
+  do_test_finished();
+}
 
-function run_test() {
-  do_test_pending();
-  run_test_with_server(DebuggerServer, function() {
-    run_test_with_server(WorkerDebuggerServer, do_test_finished);
-  });
+async function invokeAndPause({global, debuggerClient}, expression) {
+  return executeOnNextTickAndWaitForPause(
+    () => Cu.evalInSandbox(expression, global),
+    debuggerClient
+  );
+}
+
+async function step({threadClient, debuggerClient}, cmd) {
+  return cmd(debuggerClient, threadClient);
 }
 
-function run_test_with_server(server, callback) {
-  gCallback = callback;
-  initTestDebuggerServer(server);
-  gDebuggee = addTestGlobal("test-stepping", server);
-  gClient = new DebuggerClient(server.connectPipe());
-  gClient.connect(test_simple_stepping);
+function getPauseLocation(packet) {
+  const {line, column} = packet.frame.where;
+  return {line, column};
+}
+
+function getPauseReturn(packet) {
+  dump(`>> getPauseReturn yo ${JSON.stringify(packet.why)}\n`);
+  return packet.why.frameFinished.return;
+}
+
+async function steps(dbg, sequence) {
+  const locations = [];
+  for (const cmd of sequence) {
+    const packet = await step(dbg, cmd);
+    locations.push(getPauseLocation(packet));
+  }
+  return locations;
 }
 
-async function test_simple_stepping() {
-  const [attachResponse,, threadClient] = await attachTestTabAndResume(gClient,
-                                                                       "test-stepping");
-  ok(!attachResponse.error, "Should not get an error attaching");
-
-  dumpn("Evaluating test code and waiting for first debugger statement");
-  const dbgStmt = await executeOnNextTickAndWaitForPause(evaluateTestCode, gClient);
-  equal(dbgStmt.frame.where.line, 2, "Should be at debugger statement on line 2");
-  equal(gDebuggee.a, undefined);
-  equal(gDebuggee.b, undefined);
+async function stepOutOfA(dbg, func, expectedLocation) {
+  await invokeAndPause(dbg, `${func}()`);
+  await steps(dbg, [stepOver, stepIn]);
 
-  dumpn("Step Over to line 3");
-  const step1 = await stepOver(gClient, threadClient);
-  equal(step1.type, "paused");
-  equal(step1.why.type, "resumeLimit");
-  equal(step1.frame.where.line, 3);
-  equal(gDebuggee.a, undefined);
-  equal(gDebuggee.b, undefined);
+  dump(`>>> oof\n`);
+  const packet = await step(dbg, stepOut);
+  dump(`>>> foo\n`);
 
-  dumpn("Step Over to line 4");
-  const step3 = await stepOver(gClient, threadClient);
-  equal(step3.type, "paused");
-  equal(step3.why.type, "resumeLimit");
-  equal(step3.frame.where.line, 4);
-  equal(gDebuggee.a, 1);
-  equal(gDebuggee.b, undefined);
+  deepEqual(getPauseLocation(packet), expectedLocation, `step out location in ${func}`);
 
-  dumpn("Step Over to line 4 to leave the frame");
-  const step4 = await stepOver(gClient, threadClient);
-  equal(step4.type, "paused");
-  equal(step4.why.type, "resumeLimit");
-  equal(step4.frame.where.line, 4);
-  equal(gDebuggee.a, 1);
-  equal(gDebuggee.b, 2);
-
-  finishClient(gClient, gCallback);
+  await resume(dbg.threadClient);
 }
 
-function evaluateTestCode() {
-  /* eslint-disable */
-  Cu.evalInSandbox(
-    `                                   // 1
-    debugger;                           // 2
-    var a = 1;                          // 3
-    var b = 2;`,                        // 4
-    gDebuggee,
-    "1.8",
-    "test_stepping-01-test-code.js",
-    1
-  );
-  /* eslint-disable */
-}
\ No newline at end of file
+async function stepOverInA(dbg, func, expectedLocation) {
+  await invokeAndPause(dbg, `${func}()`);
+  await steps(dbg, [stepOver, stepIn, stepOver]);
+
+  let packet = await step(dbg, stepOver);
+  dump(`>> stepOverInA hi\n`);
+  equal(getPauseReturn(packet).ownPropertyLength, 1, "a() is returning obj");
+
+  packet = await step(dbg, stepOver);
+  deepEqual(getPauseLocation(packet), expectedLocation, `step out location in ${func}`);
+
+  await resume(dbg.threadClient);
+}
+
+async function testStep(dbg, func, expectedLocation) {
+  await stepOverInA(dbg, func, expectedLocation);
+  await stepOutOfA(dbg, func, expectedLocation);
+}
+
+function run_test() {
+  return (async function() {
+    const dbg = await setupTestFromUrl("stepping.js");
+
+    await testStep(dbg, "arithmetic", {line: 16, column: 8});
+    await testStep(dbg, "composition", {line: 21, column: 2});
+    await testStep(dbg, "chaining", {line: 26, column: 6});
+
+    await testFinish(dbg);
+  })();
+}
--- a/devtools/server/tests/unit/test_stepping-03.js
+++ b/devtools/server/tests/unit/test_stepping-03.js
@@ -32,17 +32,17 @@ async function test_simple_stepping() {
                                                                        "test-stepping");
   ok(!attachResponse.error, "Should not get an error attaching");
 
   dumpn("Evaluating test code and waiting for first debugger statement");
   await executeOnNextTickAndWaitForPause(evaluateTestCode, gClient);
 
   const step1 = await stepOut(gClient, threadClient);
   equal(step1.type, "paused");
-  equal(step1.frame.where.line, 6);
+  equal(step1.frame.where.line, 8);
   equal(step1.why.type, "resumeLimit");
 
   equal(gDebuggee.a, 1);
   equal(gDebuggee.b, 2);
 
   finishClient(gClient, gCallback);
 }
 
@@ -58,9 +58,9 @@ function evaluateTestCode() {
     f();                                // 7
     `,                                  // 8
     gDebuggee,
     "1.8",
     "test_stepping-01-test-code.js",
     1
   );
   /* eslint-disable */
-}
\ No newline at end of file
+}
--- a/devtools/server/tests/unit/test_stepping-06.js
+++ b/devtools/server/tests/unit/test_stepping-06.js
@@ -3,108 +3,145 @@
 /* eslint-disable no-shadow, max-nested-callbacks */
 
 "use strict";
 
 /**
  * Check that stepping out of a function returns the right return value.
  */
 
-var gDebuggee;
-var gClient;
-var gThreadClient;
-var gCallback;
+async function invokeAndPause({global, debuggerClient}, expression) {
+  return executeOnNextTickAndWaitForPause(
+    () => Cu.evalInSandbox(expression, global),
+    debuggerClient
+  );
+}
 
-function run_test() {
-  run_test_with_server(DebuggerServer, function() {
-    run_test_with_server(WorkerDebuggerServer, do_test_finished);
-  });
-  do_test_pending();
+async function step({threadClient, debuggerClient}, cmd) {
+  return cmd(debuggerClient, threadClient);
+}
+
+function getPauseLocation(packet) {
+  const {line, column} = packet.frame.where;
+  return {line, column};
 }
 
-function run_test_with_server(server, callback) {
-  gCallback = callback;
-  initTestDebuggerServer(server);
-  gDebuggee = addTestGlobal("test-stack", server);
-  gClient = new DebuggerClient(server.connectPipe());
-  gClient.connect().then(function() {
-    attachTestTabAndResume(
-      gClient, "test-stack",
-      function(response, tabClient, threadClient) {
-        gThreadClient = threadClient;
-        // 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.dispatchToMainThread({
-          run: test_simple_stepping
-        });
-      });
-  });
+function getFrameFinished(packet) {
+  return packet.why.frameFinished;
+}
+
+async function steps(dbg, sequence) {
+  const locations = [];
+  for (const cmd of sequence) {
+    const packet = await step(dbg, cmd);
+    locations.push(getPauseLocation(packet));
+  }
+  return locations;
+}
+
+async function testFinish({threadClient, debuggerClient}) {
+  await resume(threadClient);
+  await close(debuggerClient);
+
+  do_test_finished();
 }
 
-async function test_simple_stepping() {
-  await executeOnNextTickAndWaitForPause(evaluateTestCode, gClient);
+async function testRet(dbg) {
+  let packet;
+
+  info(`1. Test returning from doRet via stepping over`);
+  await invokeAndPause(dbg, `doRet()`);
+  await steps(dbg, [stepOver, stepIn, stepOver]);
+  packet = await step(dbg, stepOver);
 
-  const step1 = await stepOut(gClient, gThreadClient);
-  equal(step1.type, "paused");
-  equal(step1.frame.where.line, 6);
-  equal(step1.why.type, "resumeLimit");
-  equal(step1.why.frameFinished.return, 10);
+  deepEqual(
+    getPauseLocation(packet),
+    {line: 6, column: 0},
+    `completion location in doRet`
+  );
+  deepEqual(
+    getFrameFinished(packet),
+    {"return": 2}, `completion value`);
 
-  gThreadClient.resume();
-  const step2 = await waitForPause(gThreadClient);
-  equal(step2.type, "paused");
-  equal(step2.frame.where.line, 8);
-  equal(step2.why.type, "debuggerStatement");
+  await resume(dbg.threadClient);
 
-  gThreadClient.stepOut();
-  const step3 = await waitForPause(gThreadClient);
-  equal(step3.type, "paused");
-  equal(step3.frame.where.line, 9);
-  equal(step3.why.type, "resumeLimit");
-  equal(step3.why.frameFinished.return.type, "undefined");
+  info(`2. Test leaving from doRet via stepping out`);
+  await invokeAndPause(dbg, `doRet()`);
+  await steps(dbg, [stepOver, stepIn]);
 
-  gThreadClient.resume();
-  const step4 = await waitForPause(gThreadClient);
+  packet = await step(dbg, stepOut);
 
-  equal(step4.type, "paused");
-  equal(step4.frame.where.line, 11);
+  deepEqual(
+    getPauseLocation(packet),
+    {line: 15, column: 2},
+    `completion location in doThrow`
+  );
 
-  gThreadClient.stepOut();
-  const step5 = await waitForPause(gThreadClient);
-  equal(step5.type, "paused");
-  equal(step5.frame.where.line, 12);
-  equal(step5.why.type, "resumeLimit");
-  equal(step5.why.frameFinished.throw, "ah");
+  deepEqual(
+    getFrameFinished(packet),
+    {"return": 2},
+    `completion completion value`
+  );
 
-  finishClient(gClient, gCallback);
+  await resume(dbg.threadClient);
 }
 
-function evaluateTestCode() {
-  /* eslint-disable */
-  Cu.evalInSandbox(
-    `                                   //  1
-    function f() {                      //  2
-      debugger;                         //  3
-      var a = 10;                       //  4
-      return a;                         //  5
-    }                                   //  6
-    function g() {                      //  7
-      debugger;                         //  8
-    }                                   //  9
-    function h() {                      // 10
-      debugger;                         // 11
-      throw 'ah';                       // 12
-      return 2;                         // 13
-    }                                   // 14
-    f()                                 // 15
-    g()                                 // 16
-    try {                               // 17
-      h();                              // 18
-    } catch (ex) { };                   // 19
-    `,                                  // 20
-    gDebuggee,
-    "1.8",
-    "test_stepping-07-test-code.js",
-    1
+async function testThrow(dbg) {
+  let packet;
+
+  info(`3. Test leaving from doThrow via stepping over`);
+  await invokeAndPause(dbg, `doThrow()`);
+  await steps(dbg, [stepOver, stepOver, stepIn]);
+  packet = await step(dbg, stepOver);
+
+  deepEqual(
+    getPauseLocation(packet),
+    {line: 9, column: 8},
+    `completion location in doThrow`
+  );
+
+  deepEqual(
+    getFrameFinished(packet).throw.class,
+    "Error",
+    `completion value class`
+  );
+  deepEqual(
+    getFrameFinished(packet).throw.preview.message,
+    "yo",
+    `completion value preview`
   );
-  /* eslint-enable */
+
+  await resume(dbg.threadClient);
+
+  info(`4. Test leaving from doThrow via stepping out`);
+  await invokeAndPause(dbg, `doThrow()`);
+  await steps(dbg, [stepOver, stepOver, stepIn]);
+
+  packet = await step(dbg, stepOut);
+  deepEqual(
+    getPauseLocation(packet),
+    {line: 22, column: 14},
+    `completion location in doThrow`
+  );
+
+  deepEqual(
+    getFrameFinished(packet).throw.class,
+    "Error",
+    `completion completion value class`
+  );
+  deepEqual(
+    getFrameFinished(packet).throw.preview.message,
+    "yo",
+    `completion completion value preview`
+  );
+  await resume(dbg.threadClient);
 }
+
+function run_test() {
+  return (async function() {
+    const dbg = await setupTestFromUrl("completions.js");
+
+    await testRet(dbg);
+    await testThrow(dbg);
+
+    await testFinish(dbg);
+  })();
+}
--- a/devtools/server/tests/unit/test_stepping-08.js
+++ b/devtools/server/tests/unit/test_stepping-08.js
@@ -41,17 +41,17 @@ async function testStepOutWithBreakpoint
 
   dumpn("Step in to innerFunction");
   const step1 = await stepIn(gClient, threadClient);
   equal(step1.frame.where.line, 7);
 
   dumpn("Step out of innerFunction");
   const step2 = await stepOut(gClient, threadClient);
   // The bug was that we'd stop again at the breakpoint on line 7.
-  equal(step2.frame.where.line, 10);
+  equal(step2.frame.where.line, 4);
 
   finishClient(gClient, gCallback);
 }
 
 function evaluateTestCode() {
   /* eslint-disable */
   Cu.evalInSandbox(
     `                                   //  1
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -1,26 +1,28 @@
 [DEFAULT]
 tags = devtools
 head = head_dbg.js
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
 support-files =
   babel_and_browserify_script_with_source_map.js
+  completions.js
   source-map-data/sourcemapped.coffee
   source-map-data/sourcemapped.map
   post_init_global_actors.js
   post_init_target_scoped_actors.js
   pre_init_global_actors.js
   pre_init_target_scoped_actors.js
   registertestactors-lazy.js
   sourcemapped.js
   testactors.js
   hello-actor.js
+  stepping.js
   setBreakpoint-on-column.js
   setBreakpoint-on-column-in-gcd-script.js
   setBreakpoint-on-column-with-no-offsets.js
   setBreakpoint-on-column-with-no-offsets-in-gcd-script.js
   setBreakpoint-on-line.js
   setBreakpoint-on-line-in-gcd-script.js
   setBreakpoint-on-line-with-multiple-offsets.js
   setBreakpoint-on-line-with-multiple-statements.js