Repository: go-delve/delve
Pull request #2597 (2021-07-16 20:44:15)

service/dap: page stack frames
Returning monotonically increasing totalFrames values for subsequent requests can be used to enforce paging in the client. If we are not at the end of the stackFrames that are returned, we return a higher total frames to suggest to the client that they should request more frames.

suzmue: Here is a screen capture of stack frame paging:


https://user-images.githubusercontent.com/6634754/126826199-a540cbc6-bbcf-4ed3-84ce-02bd6f4968af.mov



aarzilli: TestLaunchRequestWithStackTraceDepth fails:

```
=== RUN   TestLaunchRequestWithStackTraceDepth
DAP server listening at: [::]:39689
2021-07-23T18:59:19Z debug layer=dap DAP server pid = 16389
Connecting to server at: [::]:39689
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":1,"type":"request","command":"initialize","arguments":{"adapterID":"go","locale":"en-us","linesStartAt1":true,"columnsStartAt1":true,"pathFormat":"path","supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true}}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":true,"supportsConditionalBreakpoints":true,"supportsEvaluateForHovers":true,"supportsSetVariable":true,"supportsExceptionInfoRequest":true,"supportTerminateDebuggee":true,"supportsDelayedStackTraceLoading":true,"supportsClipboardContext":true}}
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":2,"type":"request","command":"launch","arguments":{"mode":"exec","program":"/tmp/increment.a443c8eb","stackTraceDepth":0}}
2021-07-23T18:59:19Z debug layer=dap debug backend is ''
2021-07-23T18:59:19Z debug layer=dap running program in /tmp

2021-07-23T18:59:19Z info layer=debugger launching process with args: [/tmp/increment.a443c8eb]
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"initialized"}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":2,"success":true,"command":"launch"}
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":3,"type":"request","command":"setBreakpoints","arguments":{"source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"breakpoints":[{"line":8}]}}
2021-07-23T18:59:19Z info layer=debugger created breakpoint: &api.Breakpoint{ID:1, Name:"sourceBp Path=\"/delve/_fixtures/increment.go\"  Line=8 Column=0", Addr:0x4ba494, Addrs:[]uint64{0x4ba494}, File:"/delve/_fixtures/increment.go", Line:8, FunctionName:"main.Increment", Cond:"", HitCond:"", Tracepoint:false, TraceReturn:false, Goroutine:false, Stacktrace:0, Variables:[]string(nil), LoadArgs:(*api.LoadConfig)(nil), LoadLocals:(*api.LoadConfig)(nil), WatchExpr:"", WatchType:0x0, HitCount:map[string]uint64{}, TotalHitCount:0x0, Disabled:false}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":3,"success":true,"command":"setBreakpoints","body":{"breakpoints":[{"id":1,"verified":true,"source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"line":8}]}}
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":4,"type":"request","command":"configurationDone","arguments":{}}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":4,"success":true,"command":"configurationDone"}
2021-07-23T18:59:19Z debug layer=debugger continuing
2021-07-23T18:59:19Z debug layer=dap "continue" command stopped - reason "breakpoint", location /delve/_fixtures/increment.go:8
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"stopped","body":{"reason":"breakpoint","threadId":1,"allThreadsStopped":true,"hitBreakpointIds":[1]}}
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":5,"type":"request","command":"stackTrace","arguments":{"threadId":1,"format":{}}}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":5,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1000,"name":"main.Increment","source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"line":8,"column":0},{"id":1001,"name":"main.Increment","source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"line":11,"column":0},{"id":1002,"name":"main.Increment","source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"line":11,"column":0},{"id":1003,"name":"main.main","source":{"name":"increment.go","path":"/delve/_fixtures/increment.go"},"line":17,"column":0},{"id":1004,"name":"runtime.main","source":{"name":"proc.go","path":"/usr/local/go/go1.15.14/src/runtime/proc.go","presentationHint":"deemphasize"},"line":204,"column":0},{"id":1005,"name":"runtime.goexit","source":{"name":"asm_amd64.s","path":"/usr/local/go/go1.15.14/src/runtime/asm_amd64.s","presentationHint":"deemphasize"},"line":1374,"column":0}],"totalF
rames":6}}
    server_test.go:2098: 
        got  len(StackFrames)=6
        want 1
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":6,"type":"request","command":"continue","arguments":{"threadId":1}}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":6,"success":true,"command":"continue","body":{"allThreadsContinued":true}}
2021-07-23T18:59:19Z debug layer=debugger continuing
4
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"terminated","body":{}}
2021-07-23T18:59:19Z debug layer=dap [<- from client]{"seq":7,"type":"request","command":"disconnect","arguments":{"terminateDebuggee":true}}
2021-07-23T18:59:19Z debug layer=debugger halting
2021-07-23T18:59:19Z debug layer=dap halt returned state: Process 16772 has exited with status 0
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"output","body":{"category":"console","output":"Process 16772 has exited with status 0\n","source":{}}}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"output","body":{"category":"console","output":"Detaching\n","source":{}}}
2021-07-23T18:59:19Z debug layer=debugger detaching
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"response","request_seq":7,"success":true,"command":"disconnect"}
2021-07-23T18:59:19Z debug layer=dap [-> to client]{"seq":0,"type":"event","event":"terminated","body":{}}
2021-07-23T18:59:19Z debug layer=dap DAP server stopping...
2021-07-23T18:59:19Z debug layer=dap DAP server stopped
--- FAIL: TestLaunchRequestWithStackTraceDepth (0.42s)
```

Also:

```
2021-07-23T19:02:20Z error layer=dap recovered panic: runtime error: index out of range [0] with length 0
goroutine 693 [running]:
runtime/debug.Stack(0xc000e870d8, 0x835160, 0xc000434a38)
	/usr/local/go/src/runtime/debug/stack.go:24 +0x9f
github.com/go-delve/delve/service/dap.(*Server).recoverPanic(0xc000ae00e0, 0x8d8408, 0xc000982840)
	/tmp/cirrus-ci-build/service/dap/server.go:398 +0x76
panic(0x835160, 0xc000434a38)
	/usr/local/go/src/runtime/panic.go:965 +0x1b9
github.com/go-delve/delve/service/debugger.(*Debugger).Launch(0xc000538f20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1)
	/tmp/cirrus-ci-build/service/debugger/debugger.go:248 +0x6f6
github.com/go-delve/delve/service/debugger.New(0xc0006f5200, 0x0, 0x0, 0x0, 0xc000af3300, 0x809ba0, 0x1)
	/tmp/cirrus-ci-build/service/debugger/debugger.go:190 +0x5e5
github.com/go-delve/delve/service/dap.(*Server).onAttachRequest.func1(0xc000ae00e0, 0xc000e87790)
	/tmp/cirrus-ci-build/service/dap/server.go:1561 +0x89
github.com/go-delve/delve/service/dap.(*Server).onAttachRequest(0xc000ae00e0, 0xc000982840)
	/tmp/cirrus-ci-build/service/dap/server.go:1562 +0x3cc
github.com/go-delve/delve/service/dap.(*Server).handleRequest(0xc000ae00e0, 0x8d8408, 0xc000982840)
	/tmp/cirrus-ci-build/service/dap/server.go:575 +0x1b8a
github.com/go-delve/delve/service/dap.(*Server).serveDAPCodec(0xc000ae00e0)
	/tmp/cirrus-ci-build/service/dap/server.go:389 +0x105
github.com/go-delve/delve/service/dap.(*Server).Run.func1(0xc000ae00e0)
	/tmp/cirrus-ci-build/service/dap/server.go:354 +0xda
created by github.com/go-delve/delve/service/dap.(*Server).Run
	/tmp/cirrus-ci-build/service/dap/server.go:333 +0x3f
```

Patch:
diff --git a/service/dap/server.go b/service/dap/server.go
index 28c93995..14aafbcc 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -1676,7 +1676,19 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
 	}
 
 	goroutineID := request.Arguments.ThreadId
-	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
+	start := request.Arguments.StartFrame
+	if start < 0 {
+		start = 0
+	}
+	levels := s.args.stackTraceDepth
+	if request.Arguments.Levels > 0 {
+		levels = request.Arguments.Levels
+	}
+
+	// Since the backend doesn't support paging, we load all frames up to
+	// the requested depth and then slice them here per
+	// `supportsDelayedStackTraceLoading` capability.
+	frames, err := s.debugger.Stacktrace(goroutineID, start+levels-1, 0)
 	if err != nil {
 		s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", err.Error())
 		return
@@ -1688,36 +1700,35 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
 		isSystemGoroutine = g.System(s.debugger.Target())
 	}
 
-	stackFrames := make([]dap.StackFrame, len(frames))
-	for i, frame := range frames {
+	stackFrames := []dap.StackFrame{} // initialize to empty, since nil is not an accepted response.
+	for i := 0; i < levels && i+start < len(frames); i++ {
+		frame := frames[start+i]
 		loc := &frame.Call
-		uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, i})
-		stackFrames[i] = dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc)}
+		uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, start + i})
+		stackFrame := dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc)}
 		if loc.File != "<autogenerated>" {
 			clientPath := s.toClientPath(loc.File)
-			stackFrames[i].Source = dap.Source{Name: filepath.Base(clientPath), Path: clientPath}
+			stackFrame.Source = dap.Source{Name: filepath.Base(clientPath), Path: clientPath}
 		}
-		stackFrames[i].Column = 0
+		stackFrame.Column = 0
 
 		packageName := fnPackageName(loc)
 		if !isSystemGoroutine && packageName == "runtime" {
-			stackFrames[i].Source.PresentationHint = "deemphasize"
+			stackFrame.Source.PresentationHint = "deemphasize"
 		}
+		stackFrames = append(stackFrames, stackFrame)
 	}
-	// Since the backend doesn't support paging, we load all frames up to
-	// pre-configured depth every time and then slice them here per
-	// `supportsDelayedStackTraceLoading` capability.
-	// TODO(polina): consider optimizing this, so subsequent stack requests
-	// slice already loaded frames and handles instead of reloading every time.
-	if request.Arguments.StartFrame > 0 {
-		stackFrames = stackFrames[min(request.Arguments.StartFrame, len(stackFrames)):]
-	}
-	if request.Arguments.Levels > 0 {
-		stackFrames = stackFrames[:min(request.Arguments.Levels, len(stackFrames))]
+
+	totalFrames := len(frames)
+	if len(frames) >= start+levels && !frames[len(frames)-1].Bottom {
+		// We don't know the exact number of available stack frames, so
+		// add an arbitrary number so the client knows to request additional
+		// frames.
+		totalFrames += s.args.stackTraceDepth
 	}
 	response := &dap.StackTraceResponse{
 		Response: *newResponse(request.Request),
-		Body:     dap.StackTraceResponseBody{StackFrames: stackFrames, TotalFrames: len(frames)},
+		Body:     dap.StackTraceResponseBody{StackFrames: stackFrames, TotalFrames: totalFrames},
 	}
 	s.send(response)
 }
@@ -2656,6 +2667,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 		bpState = g.Thread.Breakpoint()
 	}
 	// Check if this goroutine ID is stopped at a breakpoint.
+	includeStackTrace := true
 	if bpState != nil && bpState.Breakpoint != nil && (bpState.Breakpoint.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) {
 		switch bpState.Breakpoint.Name {
 		case proc.FatalThrow:
@@ -2691,7 +2703,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 			s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", err.Error())
 			return
 		}
-		if state == nil || state.CurrentThread == nil || g.Thread == nil || state.CurrentThread.ID != g.Thread.ThreadID() {
+		if s.exceptionErr.Error() != "next while nexting" && (state == nil || state.CurrentThread == nil || g.Thread == nil || state.CurrentThread.ID != g.Thread.ThreadID()) {
 			s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", fmt.Sprintf("no exception found for goroutine %d", goroutineID))
 			return
 		}
@@ -2700,28 +2712,20 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 		if body.Description == "bad access" {
 			body.Description = BetterBadAccessError
 		}
+		if body.Description == "next while nexting" {
+			body.ExceptionId = "invalid command"
+			body.Description = BetterNextWhileNextingError
+			includeStackTrace = false
+		}
 	}
 
-	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
-	if err == nil {
-		apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
-		if err == nil {
-			var buf bytes.Buffer
-			fmt.Fprintln(&buf, "Stack:")
-			userLoc := g.UserCurrent()
-			userFuncPkg := fnPackageName(&userLoc)
-			api.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false, func(s api.Stackframe) bool {
-				// Include all stack frames if the stack trace is for a system goroutine,
-				// otherwise, skip runtime stack frames.
-				if userFuncPkg == "runtime" {
-					return true
-				}
-				return s.Location.Function != nil && !strings.HasPrefix(s.Location.Function.Name(), "runtime.")
-			})
-			body.Details.StackTrace = buf.String()
+	if includeStackTrace {
+		frames, err := s.stacktrace(goroutineID, g)
+		if err != nil {
+			body.Details.StackTrace = fmt.Sprintf("Error getting stack trace: %s", err.Error())
+		} else {
+			body.Details.StackTrace = frames
 		}
-	} else {
-		body.Details.StackTrace = fmt.Sprintf("Error getting stack trace: %s", err.Error())
 	}
 	response := &dap.ExceptionInfoResponse{
 		Response: *newResponse(request.Request),
@@ -2730,6 +2734,31 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
 	s.send(response)
 }
 
+func (s *Server) stacktrace(goroutineID int, g *proc.G) (string, error) {
+	frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
+	if err != nil {
+		return "", err
+	}
+	apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
+	if err != nil {
+		return "", err
+	}
+
+	var buf bytes.Buffer
+	fmt.Fprintln(&buf, "Stack:")
+	userLoc := g.UserCurrent()
+	userFuncPkg := fnPackageName(&userLoc)
+	api.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false, func(s api.Stackframe) bool {
+		// Include all stack frames if the stack trace is for a system goroutine,
+		// otherwise, skip runtime stack frames.
+		if userFuncPkg == "runtime" {
+			return true
+		}
+		return s.Location.Function != nil && !strings.HasPrefix(s.Location.Function.Name(), "runtime.")
+	})
+	return buf.String(), nil
+}
+
 func (s *Server) throwReason(goroutineID int) (string, error) {
 	return s.getExprString("s", goroutineID, 0)
 }
@@ -2819,6 +2848,8 @@ func newEvent(event string) *dap.Event {
 
 const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]
 Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)`
+const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint.
+Use 'Continue' to resume the original step command.`
 
 func (s *Server) resetHandlesForStoppedEvent() {
 	s.stackFrameHandles.reset()
@@ -2903,6 +2934,12 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
 		if stopped.Body.Text == "bad access" {
 			stopped.Body.Text = BetterBadAccessError
 		}
+		if stopped.Body.Text == "next while nexting" {
+			stopped.Body.Description = "invalid command"
+			stopped.Body.Text = BetterNextWhileNextingError
+			s.logToConsole(fmt.Sprintf("%s: %s", stopped.Body.Description, stopped.Body.Text))
+		}
+
 		state, err := s.debugger.State( /*nowait*/ true)
 		if err == nil {
 			stopped.Body.ThreadId = stoppedGoroutineID(state)
@@ -2913,6 +2950,11 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
 	// error while this one completes, it is possible that the error response
 	// will arrive after this stopped event.
 	s.send(stopped)
+
+	// Send an output event with more information if next is in progress.
+	if state != nil && state.NextInProgress {
+		s.logToConsole("Step interrupted by a breakpoint. Use 'Continue' to resume the original step command.")
+	}
 }
 
 func (s *Server) toClientPath(path string) string {


Test patch:
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 982e7cf8..42216239 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -597,24 +597,32 @@ func TestPreSetBreakpoint(t *testing.T) {
 	})
 }
 
-// checkStackFrames is a helper for verifying the values within StackTraceResponse.
+// checkStackFramesExact is a helper for verifying the values within StackTraceResponse.
 //     wantStartName - name of the first returned frame (ignored if "")
 //     wantStartLine - file line of the first returned frame (ignored if <0).
 //     wantStartID - id of the first frame returned (ignored if wantFrames is 0).
 //     wantFrames - number of frames returned (length of StackTraceResponse.Body.StackFrames array).
 //     wantTotalFrames - total number of stack frames available (StackTraceResponse.Body.TotalFrames).
-func checkStackFrames(t *testing.T, got *dap.StackTraceResponse,
+func checkStackFramesExact(t *testing.T, got *dap.StackTraceResponse,
 	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int) {
 	t.Helper()
-	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames)
+	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, true)
 }
 
-func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceResponse,
+func checkStackFramesHasMore(t *testing.T, got *dap.StackTraceResponse,
 	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int) {
 	t.Helper()
-	if got.Body.TotalFrames != wantTotalFrames {
+	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, false)
+}
+func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceResponse,
+	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int, totalExact bool) {
+	t.Helper()
+	if totalExact && got.Body.TotalFrames != wantTotalFrames {
 		t.Errorf("%s\ngot  %#v\nwant TotalFrames=%d", testName, got.Body.TotalFrames, wantTotalFrames)
+	} else if !totalExact && got.Body.TotalFrames < wantTotalFrames {
+		t.Errorf("%s\ngot  %#v\nwant TotalFrames>=%d", testName, got.Body.TotalFrames, wantTotalFrames)
 	}
+
 	if len(got.Body.StackFrames) != wantFrames {
 		t.Errorf("%s\ngot  len(StackFrames)=%d\nwant %d", testName, len(got.Body.StackFrames), wantFrames)
 	} else {
@@ -812,10 +820,9 @@ func TestStackTraceRequest(t *testing.T) {
 					// repeated requests at the same breakpoint
 					// would assign next block of unique ids to them each time.
 					const NumFrames = 6
-					reqIndex := -1
-					frameID := func(frameIndex int) int {
-						reqIndex++
-						return startHandle + NumFrames*reqIndex + frameIndex
+					reqIndex := 0
+					frameID := func() int {
+						return startHandle + reqIndex
 					}
 
 					tests := map[string]struct {
@@ -826,24 +833,102 @@ func TestStackTraceRequest(t *testing.T) {
 						wantStartFrame      int
 						wantFramesReturned  int
 						wantFramesAvailable int
+						exact               bool
 					}{
-						"all frame levels from 0 to NumFrames":    {0, NumFrames, "main.Increment", 8, 0, NumFrames, NumFrames},
-						"subset of frames from 1 to -1":           {1, NumFrames - 1, "main.Increment", 11, 1, NumFrames - 1, NumFrames},
-						"load stack in pages: first half":         {0, NumFrames / 2, "main.Increment", 8, 0, NumFrames / 2, NumFrames},
-						"load stack in pages: second half":        {NumFrames / 2, NumFrames, "main.main", 17, NumFrames / 2, NumFrames / 2, NumFrames},
-						"zero levels means all levels":            {0, 0, "main.Increment", 8, 0, NumFrames, NumFrames},
-						"zero levels means all remaining levels":  {NumFrames / 2, 0, "main.main", 17, NumFrames / 2, NumFrames / 2, NumFrames},
-						"negative levels treated as 0 (all)":      {0, -10, "main.Increment", 8, 0, NumFrames, NumFrames},
-						"OOB levels is capped at available len":   {0, NumFrames + 1, "main.Increment", 8, 0, NumFrames, NumFrames},
-						"OOB levels is capped at available len 1": {1, NumFrames + 1, "main.Increment", 11, 1, NumFrames - 1, NumFrames},
-						"negative startFrame treated as 0":        {-10, 0, "main.Increment", 8, 0, NumFrames, NumFrames},
-						"OOB startFrame returns empty trace":      {NumFrames, 0, "main.Increment", -1, -1, 0, NumFrames},
+						"all frame levels from 0 to NumFrames":    {0, NumFrames, "main.Increment", 8, 0, NumFrames, NumFrames, true},
+						"subset of frames from 1 to -1":           {1, NumFrames - 1, "main.Increment", 11, 1, NumFrames - 1, NumFrames, true},
+						"load stack in pages: first half":         {0, NumFrames / 2, "main.Increment", 8, 0, NumFrames / 2, NumFrames, false},
+						"load stack in pages: second half":        {NumFrames / 2, NumFrames, "main.main", 17, NumFrames / 2, NumFrames / 2, NumFrames, true},
+						"zero levels means all levels":            {0, 0, "main.Increment", 8, 0, NumFrames, NumFrames, true},
+						"zero levels means all remaining levels":  {NumFrames / 2, 0, "main.main", 17, NumFrames / 2, NumFrames / 2, NumFrames, true},
+						"negative levels treated as 0 (all)":      {0, -10, "main.Increment", 8, 0, NumFrames, NumFrames, true},
+						"OOB levels is capped at available len":   {0, NumFrames + 1, "main.Increment", 8, 0, NumFrames, NumFrames, true},
+						"OOB levels is capped at available len 1": {1, NumFrames + 1, "main.Increment", 11, 1, NumFrames - 1, NumFrames, true},
+						"negative startFrame treated as 0":        {-10, 0, "main.Increment", 8, 0, NumFrames, NumFrames, true},
+						"OOB startFrame returns empty trace":      {NumFrames, 0, "main.Increment", -1, -1, 0, NumFrames, true},
 					}
 					for name, tc := range tests {
 						client.StackTraceRequest(1, tc.startFrame, tc.levels)
 						stResp = client.ExpectStackTraceResponse(t)
 						checkStackFramesNamed(name, t, stResp,
-							tc.wantStartName, tc.wantStartLine, frameID(tc.wantStartFrame), tc.wantFramesReturned, tc.wantFramesAvailable)
+							tc.wantStartName, tc.wantStartLine, frameID(), tc.wantFramesReturned, tc.wantFramesAvailable, tc.exact)
+						reqIndex += len(stResp.Body.StackFrames)
+					}
+				},
+				disconnect: false,
+			}, {
+				// Stop at line 18
+				execute: func() {
+					// Frame ids get reset at each breakpoint.
+					client.StackTraceRequest(1, 0, 0)
+					stResp = client.ExpectStackTraceResponse(t)
+					checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3)
+				},
+				disconnect: false,
+			}})
+	})
+	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
+		var stResp *dap.StackTraceResponse
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Set breakpoints
+			fixture.Source, []int{8, 18},
+			[]onBreakpoint{{
+				// Stop at line 8
+				execute: func() {
+					// Even though the stack frames do not change,
+					// repeated requests at the same breakpoint
+					// would assign next block of unique ids to them each time.
+					const NumFrames = 6
+
+					var frames []dap.StackFrame
+
+					for start, levels := 0, 1; start < NumFrames; {
+						client.StackTraceRequest(1, start, levels)
+						stResp = client.ExpectStackTraceResponse(t)
+						frames = append(frames, stResp.Body.StackFrames...)
+						if stResp.Body.TotalFrames < NumFrames {
+							t.Errorf("got  %#v\nwant TotalFrames>=%d\n", stResp.Body.TotalFrames, NumFrames)
+						}
+
+						if len(stResp.Body.StackFrames) < levels {
+							t.Errorf("got  len(StackFrames)=%d\nwant >=%d\n", len(stResp.Body.StackFrames), levels)
+						}
+
+						start += len(stResp.Body.StackFrames)
+					}
+
+					// TODO check all the frames.
+					want := []struct {
+						wantName string
+						wantLine int
+					}{
+						{"main.Increment", 8},
+						{"main.Increment", 11},
+						{"main.Increment", 11},
+						{"main.main", 17},
+						{"runtime.main", 0},
+						{"runtime.goexit", 0},
+					}
+					for i, frame := range frames {
+						frameId := startHandle + i
+						if frame.Id != frameId {
+							t.Errorf("got  %#v\nwant Id=%d\n", frame, frameId)
+						}
+
+						// Verify the name and line corresponding to the first returned frame (if any).
+						// This is useful when the first frame is the frame corresponding to the breakpoint at
+						// a predefined line. Line values < 0 are a signal to skip the check (which can be useful
+						// for frames in the third-party code, where we do not control the lines).
+						if want[i].wantLine > 0 && frame.Line != want[i].wantLine {
+							t.Errorf("got  Line=%d\nwant %d\n", frame.Line, want[i].wantLine)
+						}
+						if want[i].wantName != "" && frame.Name != want[i].wantName {
+							t.Errorf("got  Name=%s\nwant %s\n", frame.Name, want[i].wantName)
+						}
 					}
 				},
 				disconnect: false,
@@ -853,7 +938,7 @@ func TestStackTraceRequest(t *testing.T) {
 					// Frame ids get reset at each breakpoint.
 					client.StackTraceRequest(1, 0, 0)
 					stResp = client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stResp, "main.main", 18, startHandle, 3, 3)
+					checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3)
 				},
 				disconnect: false,
 			}})
@@ -887,7 +972,7 @@ func TestScopesAndVariablesRequests(t *testing.T) {
 						startLineno = -1
 					}
 
-					checkStackFrames(t, stack, "main.foobar", startLineno, 1000, 4, 4)
+					checkStackFramesExact(t, stack, "main.foobar", startLineno, 1000, 4, 4)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1101,7 +1186,7 @@ func TestScopesAndVariablesRequests(t *testing.T) {
 					// Frame ids get reset at each breakpoint.
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.barfoo", 27, 1000, 5, 5)
+					checkStackFramesExact(t, stack, "main.barfoo", 27, 1000, 5, 5)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1154,7 +1239,7 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.main", -1, 1000, 3, 3)
+					checkStackFramesExact(t, stack, "main.main", -1, 1000, 3, 3)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1166,7 +1251,7 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.main", -1, 1000, 3, 3)
+					checkStackFramesExact(t, stack, "main.main", -1, 1000, 3, 3)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1439,7 +1524,7 @@ func TestScopesRequestsOptimized(t *testing.T) {
 						startLineno = -1
 					}
 
-					checkStackFrames(t, stack, "main.foobar", startLineno, 1000, 4, 4)
+					checkStackFramesExact(t, stack, "main.foobar", startLineno, 1000, 4, 4)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1454,7 +1539,7 @@ func TestScopesRequestsOptimized(t *testing.T) {
 					// Frame ids get reset at each breakpoint.
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.barfoo", 27, 1000, 5, 5)
+					checkStackFramesExact(t, stack, "main.barfoo", 27, 1000, 5, 5)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1904,7 +1989,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.main", 36, 1000, 3, 3)
+					checkStackFramesExact(t, stack, "main.main", 36, 1000, 3, 3)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -1926,7 +2011,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
 
 					client.StackTraceRequest(1, 0, 20)
 					stack = client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "", 13, 1000, 4, 4)
+					checkStackFramesExact(t, stack, "", 13, 1000, 4, 4)
 
 					client.ScopesRequest(1000)
 					scopes = client.ExpectScopesResponse(t)
@@ -1970,7 +2055,7 @@ func TestShadowedVariables(t *testing.T) {
 				execute: func() {
 					client.StackTraceRequest(1, 0, 20)
 					stack := client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stack, "main.main", 13, 1000, 3, 3)
+					checkStackFramesExact(t, stack, "main.main", 13, 1000, 3, 3)
 
 					client.ScopesRequest(1000)
 					scopes := client.ExpectScopesResponse(t)
@@ -2010,7 +2095,7 @@ func TestLaunchRequestWithStackTraceDepth(t *testing.T) {
 				execute: func() {
 					client.StackTraceRequest(1, 0, 0)
 					stResp = client.ExpectStackTraceResponse(t)
-					checkStackFrames(t, stResp, "main.Increment", 8, 1000, 2 /*returned*/, 2 /*available*/)
+					checkStackFramesHasMore(t, stResp, "main.Increment", 8, 1000, 1 /*returned*/, 2 /*available*/)
 				},
 				disconnect: false,
 			}})
@@ -3548,6 +3633,22 @@ func TestStepOutPreservesGoroutine(t *testing.T) {
 			}})
 	})
 }
+func checkStopOnNextWhileNextingError(t *testing.T, client *daptest.Client, threadID int) {
+	t.Helper()
+	oe := client.ExpectOutputEvent(t)
+	if oe.Body.Category != "console" || oe.Body.Output != fmt.Sprintf("invalid command: %s\n", BetterNextWhileNextingError) {
+		t.Errorf("\ngot  %#v\nwant Category=\"console\" Output=\"invalid command: %s\\n\"", oe, BetterNextWhileNextingError)
+	}
+	se := client.ExpectStoppedEvent(t)
+	if se.Body.ThreadId != threadID || se.Body.Reason != "exception" || se.Body.Description != "invalid command" || se.Body.Text != BetterNextWhileNextingError {
+		t.Errorf("\ngot  %#v\nwant ThreadId=%d Reason=\"exception\" Description=\"invalid command\" Text=\"%s\"", se, threadID, BetterNextWhileNextingError)
+	}
+	client.ExceptionInfoRequest(1)
+	eInfo := client.ExpectExceptionInfoResponse(t)
+	if eInfo.Body.ExceptionId != "invalid command" || eInfo.Body.Description != BetterNextWhileNextingError {
+		t.Errorf("\ngot  %#v\nwant ExceptionId=\"invalid command\" Text=\"%s\"", eInfo, BetterNextWhileNextingError)
+	}
+}
 
 func TestBadAccess(t *testing.T) {
 	if runtime.GOOS != "darwin" || testBackend != "lldb" {
@@ -3588,15 +3689,99 @@ func TestBadAccess(t *testing.T) {
 
 					client.NextRequest(1)
 					client.ExpectNextResponse(t)
-					expectStoppedOnError("next while nexting")
+					checkStopOnNextWhileNextingError(t, client, 1)
 
 					client.StepInRequest(1)
 					client.ExpectStepInResponse(t)
-					expectStoppedOnError("next while nexting")
+					checkStopOnNextWhileNextingError(t, client, 1)
 
 					client.StepOutRequest(1)
 					client.ExpectStepOutResponse(t)
-					expectStoppedOnError("next while nexting")
+					checkStopOnNextWhileNextingError(t, client, 1)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
+// TestNextWhileNexting is inspired by command_test.TestIssue387 and tests
+// that when 'next' is interrupted by a 'breakpoint', calling 'next'
+// again will produce an error with a helpful message, and 'continue'
+// will resume the program.
+func TestNextWhileNexting(t *testing.T) {
+	if runtime.GOOS == "freebsd" {
+		t.Skip("test is not valid on FreeBSD")
+	}
+	// a breakpoint triggering during a 'next' operation will interrupt 'next''
+	// Unlike the test for the terminal package, we cannot be certain
+	// of the number of breakpoints we expect to hit, since multiple
+	// breakpoints being hit at the same time is not supported in dap stopped
+	// events.
+	runTest(t, "issue387", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Set breakpoints
+			fixture.Source, []int{15},
+			[]onBreakpoint{{ // Stop at line 15
+				execute: func() {
+					checkStop(t, client, 1, "main.main", 15)
+
+					client.SetBreakpointsRequest(fixture.Source, []int{8})
+					client.ExpectSetBreakpointsResponse(t)
+
+					client.ContinueRequest(1)
+					client.ExpectContinueResponse(t)
+
+					bpSe := client.ExpectStoppedEvent(t)
+					threadID := bpSe.Body.ThreadId
+					checkStop(t, client, threadID, "main.dostuff", 8)
+
+					for pos := 9; pos < 11; pos++ {
+						client.NextRequest(threadID)
+						client.ExpectNextResponse(t)
+
+						stepInProgress := true
+						for stepInProgress {
+							m := client.ExpectStoppedEvent(t)
+							switch m.Body.Reason {
+							case "step":
+								if !m.Body.AllThreadsStopped {
+									t.Errorf("got %#v, want Reason=\"step\", AllThreadsStopped=true", m)
+								}
+								checkStop(t, client, m.Body.ThreadId, "main.dostuff", pos)
+								stepInProgress = false
+							case "breakpoint":
+								if !m.Body.AllThreadsStopped {
+									t.Errorf("got %#v, want Reason=\"breakpoint\", AllThreadsStopped=true", m)
+								}
+
+								if stepInProgress {
+									// We encountered a breakpoint on a different thread. We should have to resume execution
+									// using continue.
+									oe := client.ExpectOutputEvent(t)
+									if oe.Body.Category != "console" || !strings.Contains(oe.Body.Output, "Step interrupted by a breakpoint.") {
+										t.Errorf("\ngot  %#v\nwant Category=\"console\" Output=\"Step interrupted by a breakpoint.\"", oe)
+									}
+									client.NextRequest(m.Body.ThreadId)
+									client.ExpectNextResponse(t)
+									checkStopOnNextWhileNextingError(t, client, m.Body.ThreadId)
+									// Continue since we have not finished the step request.
+									client.ContinueRequest(threadID)
+									client.ExpectContinueResponse(t)
+								} else {
+									checkStop(t, client, m.Body.ThreadId, "main.dostuff", 8)
+									// Switch to stepping on this thread instead.
+									pos = 8
+									threadID = m.Body.ThreadId
+								}
+							default:
+								t.Fatalf("got %#v, want StoppedEvent on step or breakpoint", m)
+							}
+						}
+					}
 				},
 				disconnect: true,
 			}})