service/dap: add "panic" and "fatal error" as stopped reasons (#2186)

* service/dap: add "panic" and "fatal error" as stopped reasons

The unrecovered panic and fatal throw breakpoints are not set by the
user. We now check for these special breakpoints and send appropriate
stopped reasons to the client.

* Add getter for StopReason

* Set threadID and stop reason correctly

If there is no selected goroutine, no goroutine ID should be set in
the stopped event.

The stopped reason can be better determined using the process
StopReason.

* Update panic breakpoint on next test to work with Go 1.13 runtime

When running panic.go with Go1.13, the next line that is stepped to
after panic('boom') is the defer function in the runtime package. The
unrecovered panic breakpoint is not hit until after several steps.

The test now steps until the breakpoint is hit, or the program terminates
without hitting the unrecovered panic breakpoint, in which case it fails.

* Skip breakpoint on next test in < Go 1.14
This commit is contained in:
Suzy Mueller 2020-10-07 11:24:40 -04:00 committed by GitHub
parent 80d0c8e717
commit 5632cf92be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 108 additions and 3 deletions

@ -1195,13 +1195,24 @@ func (s *Server) doCommand(command string) {
stopped.Body.AllThreadsStopped = true
if err == nil {
stopped.Body.ThreadId = state.SelectedGoroutine.ID
switch command {
case api.Next, api.Step, api.StepOut:
if state.SelectedGoroutine != nil {
stopped.Body.ThreadId = state.SelectedGoroutine.ID
}
switch s.debugger.StopReason() {
case proc.StopNextFinished:
stopped.Body.Reason = "step"
default:
stopped.Body.Reason = "breakpoint"
}
if state.CurrentThread.Breakpoint != nil {
switch state.CurrentThread.Breakpoint.Name {
case proc.FatalThrow:
stopped.Body.Reason = "fatal error"
case proc.UnrecoveredPanic:
stopped.Body.Reason = "panic"
}
}
s.send(stopped)
} else {
s.log.Error("runtime error: ", err)

@ -1327,6 +1327,91 @@ func TestBadAccess(t *testing.T) {
})
}
func TestPanicBreakpointOnContinue(t *testing.T) {
runTest(t, "panic", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client,
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{5},
[]onBreakpoint{{
execute: func() {
handleStop(t, client, 1, 5)
client.ContinueRequest(1)
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"panic\"", se)
}
},
disconnect: true,
}})
})
}
func TestPanicBreakpointOnNext(t *testing.T) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) {
// In Go 1.13, 'next' will step into the defer in the runtime
// main function, instead of the next line in the main program.
t.SkipNow()
}
runTest(t, "panic", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client,
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{5},
[]onBreakpoint{{
execute: func() {
handleStop(t, client, 1, 5)
client.NextRequest(1)
client.ExpectNextResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
t.Errorf("\ngot %#v\nexpected ThreadId=1 Reason=\"panic\"", se)
}
},
disconnect: true,
}})
})
}
func TestFatalThrowBreakpoint(t *testing.T) {
runTest(t, "testdeadlock", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client,
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{3},
[]onBreakpoint{{
execute: func() {
handleStop(t, client, 1, 3)
client.ContinueRequest(1)
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.Reason != "fatal error" {
t.Errorf("\ngot %#v\nwant Reason=\"fatal error\"", se)
}
},
disconnect: true,
}})
})
}
// handleStop covers the standard sequence of reqeusts issued by
// a client at a breakpoint or another non-terminal stop event.
// The details have been tested by other tests,

@ -1602,6 +1602,15 @@ func (d *Debugger) StopRecording() error {
return d.stopRecording()
}
// StopReason returns the reason the reason why the target process is stopped.
// A process could be stopped for multiple simultaneous reasons, in which
// case only one will be reported.
func (d *Debugger) StopReason() proc.StopReason {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.target.StopReason
}
// LockTarget acquires the target mutex.
func (d *Debugger) LockTarget() {
d.targetMutex.Lock()