service/dap: switch goroutines when stepping (#2403)

* service/dap: switch goroutine before stepping

The correct goroutine needs to be selected when stepping in order
for the step to execute to the correct location.

* handle next in progress while stepping

* Add tests for steps when switching goroutine

* remove nextInProgress handling

* add new step out test and review debug state check

* update text of stopped event and set goroutine id
This commit is contained in:
Suzy Mueller 2021-04-02 12:19:16 -04:00 committed by GitHub
parent 743f243841
commit 370ec4e6e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 201 additions and 9 deletions

@ -863,28 +863,44 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) {
// onNextRequest handles 'next' request. // onNextRequest handles 'next' request.
// This is a mandatory request to support. // This is a mandatory request to support.
func (s *Server) onNextRequest(request *dap.NextRequest) { func (s *Server) onNextRequest(request *dap.NextRequest) {
// This ignores threadId argument to match the original vscode-go implementation.
// TODO(polina): use SwitchGoroutine to change the current goroutine.
s.send(&dap.NextResponse{Response: *newResponse(request.Request)}) s.send(&dap.NextResponse{Response: *newResponse(request.Request)})
s.doCommand(api.Next) s.doStepCommand(api.Next, request.Arguments.ThreadId)
} }
// onStepInRequest handles 'stepIn' request // onStepInRequest handles 'stepIn' request
// This is a mandatory request to support. // This is a mandatory request to support.
func (s *Server) onStepInRequest(request *dap.StepInRequest) { func (s *Server) onStepInRequest(request *dap.StepInRequest) {
// This ignores threadId argument to match the original vscode-go implementation.
// TODO(polina): use SwitchGoroutine to change the current goroutine.
s.send(&dap.StepInResponse{Response: *newResponse(request.Request)}) s.send(&dap.StepInResponse{Response: *newResponse(request.Request)})
s.doCommand(api.Step) s.doStepCommand(api.Step, request.Arguments.ThreadId)
} }
// onStepOutRequest handles 'stepOut' request // onStepOutRequest handles 'stepOut' request
// This is a mandatory request to support. // This is a mandatory request to support.
func (s *Server) onStepOutRequest(request *dap.StepOutRequest) { func (s *Server) onStepOutRequest(request *dap.StepOutRequest) {
// This ignores threadId argument to match the original vscode-go implementation.
// TODO(polina): use SwitchGoroutine to change the current goroutine.
s.send(&dap.StepOutResponse{Response: *newResponse(request.Request)}) s.send(&dap.StepOutResponse{Response: *newResponse(request.Request)})
s.doCommand(api.StepOut) s.doStepCommand(api.StepOut, request.Arguments.ThreadId)
}
func (s *Server) doStepCommand(command string, threadId int) {
// Use SwitchGoroutine to change the current goroutine.
state, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: threadId}, nil)
if err != nil {
s.log.Errorf("Error switching goroutines while stepping: %e", err)
// If we encounter an error, we will have to send a stopped event
// since we already sent the step response.
stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
stopped.Body.AllThreadsStopped = true
if state.SelectedGoroutine != nil {
stopped.Body.ThreadId = state.SelectedGoroutine.ID
} else if state.CurrentThread != nil {
stopped.Body.ThreadId = state.CurrentThread.GoroutineID
}
stopped.Body.Reason = "error"
stopped.Body.Text = err.Error()
s.send(stopped)
return
}
s.doCommand(command)
} }
// onPauseRequest sends a not-yet-implemented error response. // onPauseRequest sends a not-yet-implemented error response.

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math/rand"
"net" "net"
"os" "os"
"os/exec" "os/exec"
@ -2068,6 +2069,181 @@ func TestNextAndStep(t *testing.T) {
}) })
} }
func TestNextParked(t *testing.T) {
if runtime.GOOS == "freebsd" {
t.SkipNow()
}
runTest(t, "parallel_next", 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() {
goroutineId := testStepParkedHelper(t, client, fixture)
client.NextRequest(goroutineId)
client.ExpectNextResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != goroutineId {
t.Fatalf("Next did not continue on the selected goroutine, expected %d got %d", goroutineId, se.Body.ThreadId)
}
},
disconnect: false,
}})
})
}
func TestStepInParked(t *testing.T) {
if runtime.GOOS == "freebsd" {
t.SkipNow()
}
runTest(t, "parallel_next", 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() {
goroutineId := testStepParkedHelper(t, client, fixture)
client.StepInRequest(goroutineId)
client.ExpectStepInResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != goroutineId {
t.Fatalf("StepIn did not continue on the selected goroutine, expected %d got %d", goroutineId, se.Body.ThreadId)
}
},
disconnect: false,
}})
})
}
func testStepParkedHelper(t *testing.T, client *daptest.Client, fixture protest.Fixture) int {
t.Helper()
// Set a breakpoint at main.sayHi
client.SetBreakpointsRequest(fixture.Source, []int{8})
client.ExpectSetBreakpointsResponse(t)
var goroutineId = -1
for goroutineId < 0 {
client.ContinueRequest(1)
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
client.ThreadsRequest()
threads := client.ExpectThreadsResponse(t)
// Search for a parked goroutine that we know for sure will have to be
// resumed before the program can exit. This is a parked goroutine that:
// 1. is executing main.sayhi
// 2. hasn't called wg.Done yet
// 3. is not the currently selected goroutine
for _, g := range threads.Body.Threads {
// We do not need to check the thread that the program
// is currently stopped on.
if g.Id == se.Body.ThreadId {
continue
}
client.StackTraceRequest(g.Id, 0, 5)
frames := client.ExpectStackTraceResponse(t)
for _, frame := range frames.Body.StackFrames {
// line 11 is the line where wg.Done is called
if frame.Name == "main.sayhi" && frame.Line < 11 {
goroutineId = g.Id
break
}
}
if goroutineId >= 0 {
break
}
}
}
// Clear all breakpoints.
client.SetBreakpointsRequest(fixture.Source, []int{})
client.ExpectSetBreakpointsResponse(t)
return goroutineId
}
func TestStepOutPreservesGoroutine(t *testing.T) {
// Checks that StepOut preserves the currently selected goroutine.
if runtime.GOOS == "freebsd" {
t.SkipNow()
}
rand.Seed(time.Now().Unix())
runTest(t, "issue2113", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{25},
[]onBreakpoint{{ // Stop at line 25
execute: func() {
client.ContinueRequest(1)
client.ExpectContinueResponse(t)
// The program contains runtime.Breakpoint()
se := client.ExpectStoppedEvent(t)
client.ThreadsRequest()
gs := client.ExpectThreadsResponse(t)
candg := []int{}
bestg := []int{}
for _, g := range gs.Body.Threads {
// We do not need to check the thread that the program
// is currently stopped on.
if g.Id == se.Body.ThreadId {
continue
}
client.StackTraceRequest(g.Id, 0, 20)
frames := client.ExpectStackTraceResponse(t)
for _, frame := range frames.Body.StackFrames {
if frame.Name == "main.coroutine" {
candg = append(candg, g.Id)
if strings.HasPrefix(frames.Body.StackFrames[0].Name, "runtime.") {
bestg = append(bestg, g.Id)
}
break
}
}
}
var goroutineId int
if len(bestg) > 0 {
goroutineId = bestg[rand.Intn(len(bestg))]
t.Logf("selected goroutine %d (best)\n", goroutineId)
} else {
goroutineId = candg[rand.Intn(len(candg))]
t.Logf("selected goroutine %d\n", goroutineId)
}
client.StepOutRequest(goroutineId)
client.ExpectStepOutResponse(t)
se = client.ExpectStoppedEvent(t)
if se.Body.ThreadId != goroutineId {
t.Fatalf("StepIn did not continue on the selected goroutine, expected %d got %d", goroutineId, se.Body.ThreadId)
}
},
disconnect: false,
}})
})
}
func TestBadAccess(t *testing.T) { func TestBadAccess(t *testing.T) {
if runtime.GOOS != "darwin" || testBackend != "lldb" { if runtime.GOOS != "darwin" || testBackend != "lldb" {
t.Skip("not applicable") t.Skip("not applicable")