From c94db60d2728e2b11e5c8c9eb9ba59745b52f357 Mon Sep 17 00:00:00 2001 From: polinasok <51177946+polinasok@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:24:31 -0800 Subject: [PATCH] service/dap: support evaluate requests with expressions and calls (#2185) * Support evaluate request * Fix failing tests * Call support * Remove debugger.CurrentThread() that got accidentally reintroduced during merge * Address review comments * Function to stringify stop reason * Add resetHandlesForStop * Handle stop inside call * More tests * Address review comments * Check all threads to determine if call completed * Fix test * Fix test * Fix test * Address review comments Co-authored-by: Polina Sokolova --- _fixtures/fncall.go | 24 +- _fixtures/testvariables.go | 9 +- pkg/proc/target.go | 26 +++ pkg/terminal/command_test.go | 9 +- service/dap/daptest/client.go | 28 ++- service/dap/error_ids.go | 19 +- service/dap/server.go | 197 +++++++++++++--- service/dap/server_test.go | 404 +++++++++++++++++++++++++++++---- service/debugger/debugger.go | 18 ++ service/test/variables_test.go | 1 + 10 files changed, 647 insertions(+), 88 deletions(-) diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go index 6824fcf8..9937e530 100644 --- a/_fixtures/fncall.go +++ b/_fixtures/fncall.go @@ -2,10 +2,13 @@ package main import ( "fmt" + "os" "runtime" "strings" ) +var call = "this is a variable named `call`" + func callstacktrace() (stacktrace string) { for skip := 0; ; skip++ { pc, file, line, ok := runtime.Caller(skip) @@ -18,16 +21,35 @@ func callstacktrace() (stacktrace string) { return stacktrace } +func call0(a, b int) { + fmt.Printf("call0: first: %d second: %d\n", a, b) +} + func call1(a, b int) int { fmt.Printf("first: %d second: %d\n", a, b) return a + b } +func call2(a, b int) (int, int) { + fmt.Printf("call2: first: %d second: %d\n", a, b) + return a, b +} + +func callexit() { + fmt.Printf("about to exit\n") + os.Exit(0) +} + func callpanic() { fmt.Printf("about to panic\n") panic("callpanic panicked") } +func callbreak() { + fmt.Printf("about to break") + runtime.Breakpoint() +} + func stringsJoin(v []string, sep string) string { // This is needed because strings.Join is in an optimized package and // because of a bug in the compiler arguments of optimized functions don't @@ -178,5 +200,5 @@ func main() { d.Method() d.Base.Method() x.CallMe() - fmt.Println(one, two, zero, callpanic, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree, curriedAdd, getAStruct, getAStructPtr, getVRcvrableFromAStruct, getPRcvrableFromAStructPtr, getVRcvrableFromAStructPtr, pa2, noreturncall, str, d, x, x2.CallMe(5)) + fmt.Println(one, two, zero, call, call0, call2, callexit, callpanic, callbreak, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree, curriedAdd, getAStruct, getAStructPtr, getVRcvrableFromAStruct, getPRcvrableFromAStructPtr, getVRcvrableFromAStructPtr, pa2, noreturncall, str, d, x, x2.CallMe(5)) } diff --git a/_fixtures/testvariables.go b/_fixtures/testvariables.go index c086e981..d957d29e 100644 --- a/_fixtures/testvariables.go +++ b/_fixtures/testvariables.go @@ -1,7 +1,9 @@ package main -import "fmt" -import "runtime" +import ( + "fmt" + "runtime" +) type FooBar struct { Baz int @@ -56,11 +58,12 @@ func foobar(baz string, bar FooBar) { f = barfoo ms = Nest{0, &Nest{1, &Nest{2, &Nest{3, &Nest{4, nil}}}}} // Test recursion capping ba = make([]int, 200, 200) // Test array size capping + mp = map[int]interface{}{1: 42, 2: 43} ) runtime.Breakpoint() barfoo() - fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, b1, b2, baz, neg, i8, u8, u16, u32, u64, up, f32, c64, c128, i32, bar, f, ms, ba, p1) + fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, b1, b2, baz, neg, i8, u8, u16, u32, u64, up, f32, c64, c128, i32, bar, f, ms, ba, p1, mp) } var p1 = 10 diff --git a/pkg/proc/target.go b/pkg/proc/target.go index 59b91fe7..bef040e1 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -76,6 +76,32 @@ func (pe ErrProcessExited) Error() string { // case only one will be reported. type StopReason uint8 +// String maps StopReason to string representation. +func (sr StopReason) String() string { + switch sr { + case StopUnknown: + return "unkown" + case StopLaunched: + return "launched" + case StopAttached: + return "attached" + case StopExited: + return "exited" + case StopBreakpoint: + return "breakpoint" + case StopHardcodedBreakpoint: + return "hardcoded breakpoint" + case StopManual: + return "manual" + case StopNextFinished: + return "next finished" + case StopCallReturned: + return "call returned" + default: + return "" + } +} + const ( StopUnknown StopReason = iota StopLaunched // The process was just launched diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index e4adcb8b..43ae875b 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -644,6 +644,7 @@ func TestIssue387(t *testing.T) { } func listIsAt(t *testing.T, term *FakeTerminal, listcmd string, cur, start, end int) { + t.Helper() outstr := term.MustExec(listcmd) lines := strings.Split(outstr, "\n") @@ -688,10 +689,10 @@ func TestListCmd(t *testing.T) { withTestTerminal("testvariables", t, func(term *FakeTerminal) { term.MustExec("continue") term.MustExec("continue") - listIsAt(t, term, "list", 25, 20, 30) - listIsAt(t, term, "list 69", 69, 64, 70) - listIsAt(t, term, "frame 1 list", 62, 57, 67) - listIsAt(t, term, "frame 1 list 69", 69, 64, 70) + listIsAt(t, term, "list", 27, 22, 32) + listIsAt(t, term, "list 69", 69, 64, 73) + listIsAt(t, term, "frame 1 list", 65, 60, 70) + listIsAt(t, term, "frame 1 list 69", 69, 64, 73) _, err := term.Exec("frame 50 list") if err == nil { t.Fatalf("Expected error requesting 50th frame") diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index b35f888f..0f3f8a55 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -61,7 +61,20 @@ func (c *Client) expectReadProtocolMessage(t *testing.T) dap.Message { func (c *Client) ExpectErrorResponse(t *testing.T) *dap.ErrorResponse { t.Helper() - return c.expectReadProtocolMessage(t).(*dap.ErrorResponse) + er := c.expectReadProtocolMessage(t).(*dap.ErrorResponse) + if er.Body.Error.ShowUser { + t.Errorf("\ngot %#v\nwant ShowUser=false", er) + } + return er +} + +func (c *Client) ExpectVisibleErrorResponse(t *testing.T) *dap.ErrorResponse { + t.Helper() + er := c.expectReadProtocolMessage(t).(*dap.ErrorResponse) + if !er.Body.Error.ShowUser { + t.Errorf("\ngot %#v\nwant ShowUser=true", er) + } + return er } func (c *Client) expectErrorResponse(t *testing.T, id int, message string) *dap.ErrorResponse { @@ -177,6 +190,11 @@ func (c *Client) ExpectVariablesResponse(t *testing.T) *dap.VariablesResponse { return c.expectReadProtocolMessage(t).(*dap.VariablesResponse) } +func (c *Client) ExpectEvaluateResponse(t *testing.T) *dap.EvaluateResponse { + t.Helper() + return c.expectReadProtocolMessage(t).(*dap.EvaluateResponse) +} + func (c *Client) ExpectTerminateResponse(t *testing.T) *dap.TerminateResponse { t.Helper() return c.expectReadProtocolMessage(t).(*dap.TerminateResponse) @@ -485,8 +503,12 @@ func (c *Client) TerminateThreadsRequest() { } // EvaluateRequest sends a 'evaluate' request. -func (c *Client) EvaluateRequest() { - c.send(&dap.EvaluateRequest{Request: *c.newRequest("evaluate")}) +func (c *Client) EvaluateRequest(expr string, fid int, context string) { + request := &dap.EvaluateRequest{Request: *c.newRequest("evaluate")} + request.Arguments.Expression = expr + request.Arguments.FrameId = fid + request.Arguments.Context = context + c.send(request) } // StepInTargetsRequest sends a 'stepInTargets' request. diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go index c75b4d42..ddb88c83 100644 --- a/service/dap/error_ids.go +++ b/service/dap/error_ids.go @@ -10,14 +10,15 @@ const ( // Where applicable and for consistency only, // values below are inspired the original vscode-go debug adaptor. - FailedToLaunch = 3000 - FailedtoAttach = 3001 - UnableToSetBreakpoints = 2002 - UnableToDisplayThreads = 2003 - UnableToProduceStackTrace = 2004 - UnableToListLocals = 2005 - UnableToListArgs = 2006 - UnableToListGlobals = 2007 - UnableToLookupVariable = 2008 + FailedToLaunch = 3000 + FailedtoAttach = 3001 + UnableToSetBreakpoints = 2002 + UnableToDisplayThreads = 2003 + UnableToProduceStackTrace = 2004 + UnableToListLocals = 2005 + UnableToListArgs = 2006 + UnableToListGlobals = 2007 + UnableToLookupVariable = 2008 + UnableToEvaluateExpression = 2009 // Add more codes as we support more requests ) diff --git a/service/dap/server.go b/service/dap/server.go index 6557a6c1..b7f1ba44 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -18,6 +18,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "strings" "github.com/go-delve/delve/pkg/gobuild" @@ -58,8 +59,10 @@ type Server struct { // binaryToRemove is the compiled binary to be removed on disconnect. binaryToRemove string // stackFrameHandles maps frames of each goroutine to unique ids across all goroutines. + // Reset at every stop. stackFrameHandles *handlesMap // variableHandles maps compound variables to unique references within their stack frame. + // Reset at every stop. // See also comment for convertVariable. variableHandles *variablesHandlesMap // args tracks special settings for handling debug session requests. @@ -317,8 +320,7 @@ func (s *Server) handleRequest(request dap.Message) { // Optional (capability ‘supportsTerminateThreadsRequest’) s.sendUnsupportedErrorResponse(request.Request) case *dap.EvaluateRequest: - // Required - TODO - // TODO: implement this request in V0 + // Required s.onEvaluateRequest(request) case *dap.StepInTargetsRequest: // Optional (capability ‘supportsStepInTargetsRequest’) @@ -673,7 +675,7 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) { // TODO V0 // onNextRequest handles 'next' request. // This is a mandatory request to support. func (s *Server) onNextRequest(request *dap.NextRequest) { - // This ingores threadId argument to match the original vscode-go implementation. + // 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.doCommand(api.Next) @@ -682,7 +684,7 @@ func (s *Server) onNextRequest(request *dap.NextRequest) { // onStepInRequest handles 'stepIn' request // This is a mandatory request to support. func (s *Server) onStepInRequest(request *dap.StepInRequest) { - // This ingores threadId argument to match the original vscode-go implementation. + // 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.doCommand(api.Step) @@ -691,7 +693,7 @@ func (s *Server) onStepInRequest(request *dap.StepInRequest) { // onStepOutRequest handles 'stepOut' request // This is a mandatory request to support. func (s *Server) onStepOutRequest(request *dap.StepOutRequest) { - // This ingores threadId argument to match the original vscode-go implementation. + // 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.doCommand(api.StepOut) @@ -913,15 +915,32 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) { s.send(response) } -// convertVariable converts api.Variable to dap.Variable value and reference. +// convertVariable converts proc.Variable to dap.Variable value and reference. // Variable reference is used to keep track of the children associated with each -// variable. It is shared with the host via a scopes response and is an index to -// the s.variableHandles map, so it can be referenced from a subsequent variables -// request. A positive reference signals the host that another variables request -// can be issued to get the elements of the compound variable. As a custom, a zero -// reference, reminiscent of a zero pointer, is used to indicate that a scalar -// variable cannot be "dereferenced" to get its elements (as there are none). +// variable. It is shared with the host via scopes or evaluate response and is an index +// into the s.variableHandles map, used to look up variables and their children on +// subsequent variables requests. A positive reference signals the host that another +// variables request can be issued to get the elements of the compound variable. As a +// custom, a zero reference, reminiscent of a zero pointer, is used to indicate that +// a scalar variable cannot be "dereferenced" to get its elements (as there are none). func (s *Server) convertVariable(v *proc.Variable) (value string, variablesReference int) { + return s.convertVariableWithOpts(v, false) +} + +func (s *Server) convertVariableToString(v *proc.Variable) string { + val, _ := s.convertVariableWithOpts(v, true) + return val +} + +// convertVarialbeWithOpts allows to skip reference generation in case all we need is +// a string representation of the variable. +func (s *Server) convertVariableWithOpts(v *proc.Variable, skipRef bool) (value string, variablesReference int) { + maybeCreateVariableHandle := func(v *proc.Variable) int { + if skipRef { + return 0 + } + return s.variableHandles.create(v) + } if v.Unreadable != nil { value = fmt.Sprintf("unreadable <%v>", v.Unreadable) return @@ -943,12 +962,12 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer value = "void" } else { value = fmt.Sprintf("<%s>(%#x)", typeName, v.Children[0].Addr) - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } case reflect.Array: value = "<" + typeName + ">" if len(v.Children) > 0 { - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } case reflect.Slice: if v.Base == 0 { @@ -956,7 +975,7 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer } else { value = fmt.Sprintf("<%s> (length: %d, cap: %d)", typeName, v.Len, v.Cap) if len(v.Children) > 0 { - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } } case reflect.Map: @@ -965,7 +984,7 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer } else { value = fmt.Sprintf("<%s> (length: %d)", typeName, v.Len) if len(v.Children) > 0 { - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } } case reflect.String: @@ -980,11 +999,11 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer value = "nil <" + typeName + ">" } else { value = "<" + typeName + ">" - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } case reflect.Interface: if v.Addr == 0 { - // An escaped interface variable that points to nil, this shouldn't + // An escaped interface variable that points to nil: this shouldn't // happen in normal code but can happen if the variable is out of scope, // such as if an interface variable has been captured by a // closure and replaced by a pointer to interface, and the pointer @@ -1008,7 +1027,7 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer // After: // i: // field1: ... - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } case reflect.Complex64, reflect.Complex128: v.Children = make([]proc.Variable, 2) @@ -1032,16 +1051,129 @@ func (s *Server) convertVariable(v *proc.Variable) (value string, variablesRefer value = "<" + typeName + ">" } if len(v.Children) > 0 { - variablesReference = s.variableHandles.create(v) + variablesReference = maybeCreateVariableHandle(v) } } return } -// onEvaluateRequest sends a not-yet-implemented error response. +// onEvaluateRequest handles 'evalute' requests. // This is a mandatory request to support. -func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) { // TODO V0 - s.sendNotYetImplementedErrorResponse(request.Request) +// Support the following expressions: +// -- {expression} - evaluates the expression and returns the result as a variable +// -- call {function} - injects a function call and returns the result as a variable +// TODO(polina): users have complained about having to click to expand multi-level +// variables, so consider also adding the following: +// -- print {expression} - return the result as a string like from dlv cli +func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) { + showErrorToUser := request.Arguments.Context != "watch" + if s.debugger == nil { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "debugger is nil", showErrorToUser) + return + } + // Default to the topmost stack frame of the current goroutine in case + // no frame is specified (e.g. when stopped on entry or no call stack frame is expanded) + goid, frame := -1, 0 + if sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId); ok { + goid = sf.(stackFrame).goroutineID + frame = sf.(stackFrame).frameIndex + } + // TODO(polina): Support config settings via launch/attach args vs auto-loading? + apiCfg := &api.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1} + prcCfg := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1} + + response := &dap.EvaluateResponse{Response: *newResponse(request.Request)} + isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression) + if err == nil && isCall { // call {expression} + // This call might be evaluated in the context of the frame that is not topmost + // if the editor is set to view the variables for one of the parent frames. + // If the call expression refers to any of these variables, unlike regular + // expressions, it will evaluate them in the context of the topmost frame, + // and the user will get an unexpected result or an unexpected symbol error. + // We prevent this but disallowing any frames other than topmost. + if frame > 0 { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "call is only supported with topmost stack frame", showErrorToUser) + return + } + stateBeforeCall, err := s.debugger.State( /*nowait*/ true) + if err != nil { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) + return + } + state, err := s.debugger.Command(&api.DebuggerCommand{ + Name: api.Call, + ReturnInfoLoadConfig: apiCfg, + Expr: strings.Replace(request.Arguments.Expression, "call ", "", 1), + UnsafeCall: false, + GoroutineID: goid, + }) + if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited { + e := &dap.TerminatedEvent{Event: *newEvent("terminated")} + s.send(e) + return + } + if err != nil { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) + return + } + // After the call is done, the goroutine where we injected the call should + // return to the original stopped line with return values. However, + // it is not guaranteed to be selected due to the possibility of the + // of simultaenous breakpoints. Therefore, we check all threads. + var retVars []*proc.Variable + for _, t := range state.Threads { + if t.GoroutineID == stateBeforeCall.SelectedGoroutine.ID && + t.Line == stateBeforeCall.SelectedGoroutine.CurrentLoc.Line && t.ReturnValues != nil { + // The call completed. Get the return values. + retVars, err = s.debugger.FindThreadReturnValues(t.ID, prcCfg) + if err != nil { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) + return + } + break + } + } + if retVars == nil { + // The call got interrupted by a stop (e.g. breakpoint in injected + // function call or in another goroutine) + s.resetHandlesForStop() + stopped := &dap.StoppedEvent{Event: *newEvent("stopped")} + stopped.Body.AllThreadsStopped = true + if state.SelectedGoroutine != nil { + stopped.Body.ThreadId = state.SelectedGoroutine.ID + } + stopped.Body.Reason = s.debugger.StopReason().String() + s.send(stopped) + // TODO(polina): once this is asynchronous, we could wait to reply until the user + // continues, call ends, original stop point is hit and return values are available. + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "call stopped", showErrorToUser) + return + } + // The call completed and we can reply with its return values (if any) + if len(retVars) > 0 { + // Package one or more return values in a single scope-like nameless variable + // that preserves their names. + retVarsAsVar := &proc.Variable{Children: slicePtrVarToSliceVar(retVars)} + // As a shortcut also express the return values as a single string. + retVarsAsStr := "" + for _, v := range retVars { + retVarsAsStr += s.convertVariableToString(v) + ", " + } + response.Body = dap.EvaluateResponseBody{ + Result: strings.TrimRight(retVarsAsStr, ", "), + VariablesReference: s.variableHandles.create(retVarsAsVar), + } + } + } else { // {expression} + exprVar, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, prcCfg) + if err != nil { + s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser) + return + } + exprVal, exprRef := s.convertVariable(exprVar) + response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef} + } + s.send(response) } // onTerminateRequest sends a not-yet-implemented error response. @@ -1110,7 +1242,9 @@ func (s *Server) onCancelRequest(request *dap.CancelRequest) { s.sendNotYetImplementedErrorResponse(request.Request) } -func (s *Server) sendErrorResponse(request dap.Request, id int, summary, details string) { +// sendERrorResponseWithOpts offers configuration options. +// showUser - if true, the error will be shown to the user (e.g. via a visible pop-up) +func (s *Server) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) { er := &dap.ErrorResponse{} er.Type = "response" er.Command = request.Command @@ -1119,10 +1253,16 @@ func (s *Server) sendErrorResponse(request dap.Request, id int, summary, details er.Message = summary er.Body.Error.Id = id er.Body.Error.Format = fmt.Sprintf("%s: %s", summary, details) + er.Body.Error.ShowUser = showUser s.log.Error(er.Body.Error.Format) s.send(er) } +// sendErrorResponse sends an error response with default visibility settings. +func (s *Server) sendErrorResponse(request dap.Request, id int, summary, details string) { + s.sendErrorResponseWithOpts(request, id, summary, details, false /*showUser*/) +} + // sendInternalErrorResponse sends an "internal error" response back to the client. // We only take a seq here because we don't want to make assumptions about the // kind of message received by the server that this error is a reply to. @@ -1173,6 +1313,11 @@ func newEvent(event string) *dap.Event { const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation] Unable to propogate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)` +func (s *Server) resetHandlesForStop() { + s.stackFrameHandles.reset() + s.variableHandles.reset() +} + // doCommand runs a debugger command until it stops on // termination, error, breakpoint, etc, when an appropriate // event needs to be sent to the client. @@ -1188,9 +1333,7 @@ func (s *Server) doCommand(command string) { return } - s.stackFrameHandles.reset() - s.variableHandles.reset() - + s.resetHandlesForStop() stopped := &dap.StoppedEvent{Event: *newEvent("stopped")} stopped.Body.AllThreadsStopped = true diff --git a/service/dap/server_test.go b/service/dap/server_test.go index f89fff45..aa939d45 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -98,14 +98,18 @@ func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.F // : 7 >> threads // : 7 << threads (Dummy) // : 8 >> stackTrace -// : 8 << stackTrace (Unable to produce stack trace) +// : 8 << error (Unable to produce stack trace) // : 9 >> stackTrace -// : 9 << stackTrace (Unable to produce stack trace) -// - User selects "Continue" : 10 >> continue -// : 10 << continue +// : 9 << error (Unable to produce stack trace) +// - User evaluates bad expression : 10 >> evaluate +// : 10 << error (unable to find function context) +// - User evaluates good expression: 11 >> evaluate +// : 11 << evaluate +// - User selects "Continue" : 12 >> continue +// : 12 << continue // - Program runs to completion : << terminated event -// : 11 >> disconnect -// : 11 << disconnect +// : 13 >> disconnect +// : 13 << disconnect // This test exhaustively tests Seq and RequestSeq on all messages from the // server. Other tests do not necessarily need to repeat all these checks. func TestStopOnEntry(t *testing.T) { @@ -173,36 +177,50 @@ func TestStopOnEntry(t *testing.T) { t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7 len(Threads)=1", tResp) } - // 8 >> stackTrace, << stackTrace + // 8 >> stackTrace, << error client.StackTraceRequest(1, 0, 20) stResp := client.ExpectErrorResponse(t) if stResp.Seq != 0 || stResp.RequestSeq != 8 || stResp.Body.Error.Format != "Unable to produce stack trace: unknown goroutine 1" { t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=8 Format=\"Unable to produce stack trace: unknown goroutine 1\"", stResp) } - // 9 >> stackTrace, << stackTrace + // 9 >> stackTrace, << error client.StackTraceRequest(1, 0, 20) stResp = client.ExpectErrorResponse(t) if stResp.Seq != 0 || stResp.RequestSeq != 9 || stResp.Body.Error.Id != 2004 { t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=9 Id=2004", stResp) } - // 10 >> continue, << continue, << terminated + // 10 >> evaluate, << error + client.EvaluateRequest("foo", 0 /*no frame specified*/, "repl") + erResp := client.ExpectVisibleErrorResponse(t) + if erResp.Seq != 0 || erResp.RequestSeq != 10 || erResp.Body.Error.Id != 2009 { + t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=10 Id=2009", erResp) + } + + // 11 >> evaluate, << evaluate + client.EvaluateRequest("1+1", 0 /*no frame specified*/, "repl") + evResp := client.ExpectEvaluateResponse(t) + if evResp.Seq != 0 || evResp.RequestSeq != 11 || evResp.Body.Result != "2" { + t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=10 Result=2", evResp) + } + + // 12 >> continue, << continue, << terminated client.ContinueRequest(1) contResp := client.ExpectContinueResponse(t) - if contResp.Seq != 0 || contResp.RequestSeq != 10 || !contResp.Body.AllThreadsContinued { - t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=10 Body.AllThreadsContinued=true", contResp) + if contResp.Seq != 0 || contResp.RequestSeq != 12 || !contResp.Body.AllThreadsContinued { + t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=12 Body.AllThreadsContinued=true", contResp) } termEvent := client.ExpectTerminatedEvent(t) if termEvent.Seq != 0 { t.Errorf("\ngot %#v\nwant Seq=0", termEvent) } - // 11 >> disconnect, << disconnect + // 13 >> disconnect, << disconnect client.DisconnectRequest() dResp := client.ExpectDisconnectResponse(t) - if dResp.Seq != 0 || dResp.RequestSeq != 11 { - t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=11", dResp) + if dResp.Seq != 0 || dResp.RequestSeq != 13 { + t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=13", dResp) } }) } @@ -565,12 +583,12 @@ func TestScopesAndVariablesRequests(t *testing.T) { // Breakpoints are set within the program fixture.Source, []int{}, []onBreakpoint{{ - // Stop at line 62 + // Stop at first breakpoint execute: func() { client.StackTraceRequest(1, 0, 20) stack := client.ExpectStackTraceResponse(t) - startLineno := 62 + startLineno := 65 if runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { // Go1.15 on windows inserts a NOP after the call to // runtime.Breakpoint and marks it same line as the @@ -611,7 +629,7 @@ func TestScopesAndVariablesRequests(t *testing.T) { client.VariablesRequest(1001) locals := client.ExpectVariablesResponse(t) - expectChildren(t, locals, "Locals", 29) + expectChildren(t, locals, "Locals", 30) // reflect.Kind == Bool expectVarExact(t, locals, -1, "b1", "true", noChildren) @@ -775,12 +793,12 @@ func TestScopesAndVariablesRequests(t *testing.T) { }, disconnect: false, }, { - // Stop at line 25 + // Stop at second breakpoint execute: func() { // Frame ids get reset at each breakpoint. client.StackTraceRequest(1, 0, 20) stack := client.ExpectStackTraceResponse(t) - expectStackFrames(t, stack, 25, 1000, 5, 5) + expectStackFrames(t, stack, 27, 1000, 5, 5) client.ScopesRequest(1000) scopes := client.ExpectScopesResponse(t) @@ -1146,7 +1164,7 @@ func TestSetBreakpoint(t *testing.T) { fixture.Source, []int{16}, // b main.main []onBreakpoint{{ execute: func() { - handleStop(t, client, 1, 16) + handleStop(t, client, 1, "main.main", 16) type Breakpoint struct { line int @@ -1180,7 +1198,7 @@ func TestSetBreakpoint(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) client.ExpectStoppedEvent(t) - handleStop(t, client, 1, 18) + handleStop(t, client, 1, "main.main", 18) // Set another breakpoint inside the loop in loop(), twice to trigger error client.SetBreakpointsRequest(fixture.Source, []int{8, 8}) @@ -1190,7 +1208,7 @@ func TestSetBreakpoint(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) client.ExpectStoppedEvent(t) - handleStop(t, client, 1, 8) + handleStop(t, client, 1, "main.loop", 8) client.VariablesRequest(1001) // Locals locals := client.ExpectVariablesResponse(t) expectVarExact(t, locals, 0, "i", "0", noChildren) // i == 0 @@ -1203,7 +1221,7 @@ func TestSetBreakpoint(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) client.ExpectStoppedEvent(t) - handleStop(t, client, 1, 8) + handleStop(t, client, 1, "main.loop", 8) client.VariablesRequest(1001) // Locals locals = client.ExpectVariablesResponse(t) expectVarExact(t, locals, 0, "i", "3", noChildren) // i == 3 @@ -1216,7 +1234,7 @@ func TestSetBreakpoint(t *testing.T) { client.ContinueRequest(1) client.ExpectContinueResponse(t) client.ExpectStoppedEvent(t) - handleStop(t, client, 1, 8) + handleStop(t, client, 1, "main.loop", 8) client.VariablesRequest(1001) // Locals locals = client.ExpectVariablesResponse(t) expectVarExact(t, locals, 0, "i", "4", noChildren) // i == 4 @@ -1231,6 +1249,305 @@ func TestSetBreakpoint(t *testing.T) { }) } +// expectEval is a helper for verifying the values within an EvaluateResponse. +// value - the value of the evaluated expression +// hasRef - true if the evaluated expression should have children and therefore a non-0 variable reference +// ref - reference to retrieve children of this evaluated expression (0 if none) +func expectEval(t *testing.T, got *dap.EvaluateResponse, value string, hasRef bool) (ref int) { + t.Helper() + if got.Body.Result != value || (got.Body.VariablesReference > 0) != hasRef { + t.Errorf("\ngot %#v\nwant Result=%q hasRef=%t", got, value, hasRef) + } + return got.Body.VariablesReference +} + +func TestEvaluateRequest(t *testing.T) { + runTest(t, "testvariables", func(client *daptest.Client, fixture protest.Fixture) { + runDebugSessionWithBPs(t, client, + // Launch + func() { + client.LaunchRequest("exec", fixture.Path, !stopOnEntry) + }, + fixture.Source, []int{}, // Breakpoint set in the program + []onBreakpoint{{ // Stop at first breakpoint + execute: func() { + handleStop(t, client, 1, "main.foobar", 65) + + // Variable lookup + client.EvaluateRequest("a2", 1000, "this context will be ignored") + got := client.ExpectEvaluateResponse(t) + expectEval(t, got, "6", noChildren) + + client.EvaluateRequest("a5", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref := expectEval(t, got, "<[]int> (length: 5, cap: 5)", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + a5 := client.ExpectVariablesResponse(t) + expectChildren(t, a5, "a5", 5) + expectVarExact(t, a5, 0, "[0]", "1", noChildren) + expectVarExact(t, a5, 4, "[4]", "5", noChildren) + } + + // All (binary and unary) on basic types except <-, ++ and -- + client.EvaluateRequest("1+1", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "2", noChildren) + + // Comparison operators on any type + client.EvaluateRequest("1<2", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "true", noChildren) + + // Type casts between numeric types + client.EvaluateRequest("int(2.3)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "2", noChildren) + + // Type casts of integer constants into any pointer type and vice versa + client.EvaluateRequest("(*int)(2)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref = expectEval(t, got, "<*int>(0x2)", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + expr := client.ExpectVariablesResponse(t) + expectChildren(t, expr, "(*int)(2)", 1) + // TODO(polina): should this be printed as (unknown int) instead? + expectVarExact(t, expr, 0, "", "", noChildren) + } + // Type casts between string, []byte and []rune + client.EvaluateRequest("[]byte(\"ABC€\")", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + // TODO(polina): this is a bug (in vscode-go too). dlv cli prints + // []uint8 len: 6, cap: 6, [65,66,67,226,130,172] + expectEval(t, got, "nil <[]uint8>", noChildren) + + // Struct member access (i.e. somevar.memberfield) + client.EvaluateRequest("ms.Nest.Level", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "1", noChildren) + + // Slicing and indexing operators on arrays, slices and strings + client.EvaluateRequest("a5[4]", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "5", noChildren) + + // Map access + client.EvaluateRequest("mp[1]", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref = expectEval(t, got, "", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + expr := client.ExpectVariablesResponse(t) + expectChildren(t, expr, "mp[1]", 1) + expectVarExact(t, expr, 0, "data", "42", noChildren) + } + + // Pointer dereference + client.EvaluateRequest("*ms.Nest", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref = expectEval(t, got, "", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + expr := client.ExpectVariablesResponse(t) + expectChildren(t, expr, "*ms.Nest", 2) + expectVarExact(t, expr, 0, "Level", "1", noChildren) + } + + // Calls to builtin functions: cap, len, complex, imag and real + client.EvaluateRequest("len(a5)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "5", noChildren) + + // Type assertion on interface variables (i.e. somevar.(concretetype)) + client.EvaluateRequest("mp[1].(int)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "42", noChildren) + }, + disconnect: false, + }, { // Stop at second breakpoint + execute: func() { + handleStop(t, client, 1, "main.barfoo", 27) + + // Top-most frame + client.EvaluateRequest("a1", 1000, "this context will be ignored") + got := client.ExpectEvaluateResponse(t) + expectEval(t, got, "\"bur\"", noChildren) + // No frame defaults to top-most frame + client.EvaluateRequest("a1", 0, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "\"bur\"", noChildren) + // Next frame + client.EvaluateRequest("a1", 1001, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "\"foofoofoofoofoofoo\"", noChildren) + // Next frame + client.EvaluateRequest("a1", 1002, "any context but watch") + erres := client.ExpectVisibleErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: could not find symbol value for a1" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: could not find symbol value for a1\"", erres) + } + client.EvaluateRequest("a1", 1002, "watch") + erres = client.ExpectErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: could not find symbol value for a1" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: could not find symbol value for a1\"", erres) + } + }, + disconnect: false, + }}) + }) +} + +func TestEvaluateCallRequest(t *testing.T) { + protest.MustSupportFunctionCalls(t, testBackend) + runTest(t, "fncall", func(client *daptest.Client, fixture protest.Fixture) { + runDebugSessionWithBPs(t, client, + // Launch + func() { + client.LaunchRequest("exec", fixture.Path, !stopOnEntry) + }, + fixture.Source, []int{88}, + []onBreakpoint{{ // Stop in makeclos() + execute: func() { + handleStop(t, client, 1, "main.makeclos", 88) + + // Topmost frame: both types of expressions should work + client.EvaluateRequest("callstacktrace", 1000, "this context will be ignored") + client.ExpectEvaluateResponse(t) + client.EvaluateRequest("call callstacktrace()", 1000, "this context will be ignored") + client.ExpectEvaluateResponse(t) + + // Next frame: only non-call expressions will work + client.EvaluateRequest("callstacktrace", 1001, "this context will be ignored") + client.ExpectEvaluateResponse(t) + client.EvaluateRequest("call callstacktrace()", 1001, "not watch") + erres := client.ExpectVisibleErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: call is only supported with topmost stack frame" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: call is only supported with topmost stack frame\"", erres) + } + + // A call can stop on a breakpoint + client.EvaluateRequest("call callbreak()", 1000, "not watch") + s := client.ExpectStoppedEvent(t) + if s.Body.Reason != "hardcoded breakpoint" { + t.Errorf("\ngot %#v\nwant Reason=\"hardcoded breakpoint\"", s) + } + erres = client.ExpectVisibleErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: call stopped" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: call stopped\"", erres) + } + + // A call during a call causes an error + client.EvaluateRequest("call callstacktrace()", 1000, "not watch") + erres = client.ExpectVisibleErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: cannot call function while another function call is already in progress" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: cannot call function while another function call is already in progress\"", erres) + } + + // Complete the call and get back to original breakpoint in makeclos() + client.ContinueRequest(1) + client.ExpectContinueResponse(t) + client.ExpectStoppedEvent(t) + handleStop(t, client, 1, "main.makeclos", 88) + + // Inject a call for the same function that is stopped at breakpoint: + // it might stop at the exact same breakpoint on the same goroutine, + // but we should still detect that its an injected call that stopped + // and not the return to the original point of injection after it + // completed. + client.EvaluateRequest("call makeclos(nil)", 1000, "not watch") + client.ExpectStoppedEvent(t) + erres = client.ExpectVisibleErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: call stopped" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: call stopped\"", erres) + } + if (goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) && (runtime.GOOS == "linux" || runtime.GOOS == "windows")) || + runtime.GOOS == "freebsd" { + handleStop(t, client, 1, "runtime.debugCallWrap", -1) + } else { + handleStop(t, client, 1, "main.makeclos", 88) + } + + // Complete the call and get back to original breakpoint in makeclos() + client.ContinueRequest(1) + client.ExpectContinueResponse(t) + client.ExpectStoppedEvent(t) + handleStop(t, client, 1, "main.makeclos", 88) + }, + disconnect: false, + }, { // Stop at runtime breakpoint + execute: func() { + handleStop(t, client, 1, "main.main", 197) + + // No return values + client.EvaluateRequest("call call0(1, 2)", 1000, "this context will be ignored") + got := client.ExpectEvaluateResponse(t) + expectEval(t, got, "", noChildren) + // One unnamed return value + client.EvaluateRequest("call call1(one, two)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref := expectEval(t, got, "3", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + rv := client.ExpectVariablesResponse(t) + expectChildren(t, rv, "rv", 1) + expectVarExact(t, rv, 0, "~r2", "3", noChildren) + } + // One named return value + // Panic doesn't panic, but instead returns the error as a named return variable + client.EvaluateRequest("call callpanic()", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref = expectEval(t, got, "", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + rv := client.ExpectVariablesResponse(t) + expectChildren(t, rv, "rv", 1) + ref = expectVarExact(t, rv, 0, "~panic", "", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + p := client.ExpectVariablesResponse(t) + expectChildren(t, p, "~panic", 1) + expectVarExact(t, p, 0, "data", "\"callpanic panicked\"", noChildren) + } + } + // Multiple return values + client.EvaluateRequest("call call2(one, two)", 1000, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + ref = expectEval(t, got, "1, 2", hasChildren) + if ref > 0 { + client.VariablesRequest(ref) + rvs := client.ExpectVariablesResponse(t) + expectChildren(t, rvs, "rvs", 2) + expectVarExact(t, rvs, 0, "~r2", "1", noChildren) + expectVarExact(t, rvs, 1, "~r3", "2", noChildren) + } + // No frame defaults to top-most frame + client.EvaluateRequest("call call1(one, two)", 0, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "3", hasChildren) + // Extra spaces don't matter + client.EvaluateRequest(" call call1(one, one) ", 0, "this context will be ignored") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "2", hasChildren) + // Just 'call', even with extra space, is treated as {expression} + client.EvaluateRequest("call ", 1000, "watch") + got = client.ExpectEvaluateResponse(t) + expectEval(t, got, "\"this is a variable named `call`\"", noChildren) + // Call error + client.EvaluateRequest("call call1(one)", 1000, "watch") + erres := client.ExpectErrorResponse(t) + if erres.Body.Error.Format != "Unable to evaluate expression: not enough arguments" { + t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: not enough arguments\"", erres) + } + // Call can exit + client.EvaluateRequest("call callexit()", 1000, "this context will be ignored") + client.ExpectTerminatedEvent(t) + }, + disconnect: true, + }}) + }) +} + func TestNextAndStep(t *testing.T) { runTest(t, "testinline", func(client *daptest.Client, fixture protest.Fixture) { runDebugSessionWithBPs(t, client, @@ -1242,32 +1559,32 @@ func TestNextAndStep(t *testing.T) { fixture.Source, []int{11}, []onBreakpoint{{ // Stop at line 11 execute: func() { - handleStop(t, client, 1, 11) + handleStop(t, client, 1, "main.initialize", 11) - expectStop := func(line int) { + expectStop := func(fun string, line int) { t.Helper() se := client.ExpectStoppedEvent(t) if se.Body.Reason != "step" || se.Body.ThreadId != 1 || !se.Body.AllThreadsStopped { t.Errorf("got %#v, want Reason=\"step\", ThreadId=1, AllThreadsStopped=true", se) } - handleStop(t, client, 1, line) + handleStop(t, client, 1, fun, line) } client.StepOutRequest(1) client.ExpectStepOutResponse(t) - expectStop(18) + expectStop("main.main", 18) client.NextRequest(1) client.ExpectNextResponse(t) - expectStop(19) + expectStop("main.main", 19) client.StepInRequest(1) client.ExpectStepInResponse(t) - expectStop(5) + expectStop("main.inlineThis", 5) client.NextRequest(-10000 /*this is ignored*/) client.ExpectNextResponse(t) - expectStop(6) + expectStop("main.inlineThis", 6) }, disconnect: false, }}) @@ -1288,7 +1605,7 @@ func TestBadAccess(t *testing.T) { fixture.Source, []int{4}, []onBreakpoint{{ // Stop at line 4 execute: func() { - handleStop(t, client, 1, 4) + handleStop(t, client, 1, "main.main", 4) expectStoppedOnError := func(errorPrefix string) { t.Helper() @@ -1338,7 +1655,7 @@ func TestPanicBreakpointOnContinue(t *testing.T) { fixture.Source, []int{5}, []onBreakpoint{{ execute: func() { - handleStop(t, client, 1, 5) + handleStop(t, client, 1, "main.main", 5) client.ContinueRequest(1) client.ExpectContinueResponse(t) @@ -1370,7 +1687,7 @@ func TestPanicBreakpointOnNext(t *testing.T) { fixture.Source, []int{5}, []onBreakpoint{{ execute: func() { - handleStop(t, client, 1, 5) + handleStop(t, client, 1, "main.main", 5) client.NextRequest(1) client.ExpectNextResponse(t) @@ -1397,7 +1714,7 @@ func TestFatalThrowBreakpoint(t *testing.T) { fixture.Source, []int{3}, []onBreakpoint{{ execute: func() { - handleStop(t, client, 1, 3) + handleStop(t, client, 1, "main.main", 3) client.ContinueRequest(1) client.ExpectContinueResponse(t) @@ -1416,15 +1733,23 @@ func TestFatalThrowBreakpoint(t *testing.T) { // a client at a breakpoint or another non-terminal stop event. // The details have been tested by other tests, // so this is just a sanity check. -func handleStop(t *testing.T, client *daptest.Client, thread, line int) { +// Skips line check if line is -1. +func handleStop(t *testing.T, client *daptest.Client, thread int, name string, line int) { t.Helper() client.ThreadsRequest() client.ExpectThreadsResponse(t) client.StackTraceRequest(thread, 0, 20) st := client.ExpectStackTraceResponse(t) - if len(st.Body.StackFrames) < 1 || st.Body.StackFrames[0].Line != line { - t.Errorf("\ngot %#v\nwant Line=%d", st, line) + if len(st.Body.StackFrames) < 1 { + t.Errorf("\ngot %#v\nwant len(stackframes) => 1", st) + } else { + if line != -1 && st.Body.StackFrames[0].Line != line { + t.Errorf("\ngot %#v\nwant Line=%d", st, line) + } + if st.Body.StackFrames[0].Name != name { + t.Errorf("\ngot %#v\nwant Name=%q", st, name) + } } client.ScopesRequest(1000) @@ -1626,9 +1951,6 @@ func TestRequiredNotYetImplementedResponses(t *testing.T) { client.PauseRequest() expectNotYetImplemented("pause") - - client.EvaluateRequest() - expectNotYetImplemented("evaluate") }) } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index a94a20f5..6e4ced70 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1517,6 +1517,24 @@ func (d *Debugger) Recorded() (recorded bool, tracedir string) { return d.target.Recorded() } +// FindThreadReturnValues returns the return values of the function that +// the thread of the given 'id' just stepped out of. +func (d *Debugger) FindThreadReturnValues(id int, cfg proc.LoadConfig) ([]*proc.Variable, error) { + d.targetMutex.Lock() + defer d.targetMutex.Unlock() + + if _, err := d.target.Valid(); err != nil { + return nil, err + } + + thread, found := d.target.FindThread(id) + if !found { + return nil, fmt.Errorf("could not find thread %d", id) + } + + return thread.Common().ReturnValues(cfg), nil +} + // Checkpoint will set a checkpoint specified by the locspec. func (d *Debugger) Checkpoint(where string) (int, error) { d.targetMutex.Lock() diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 1c892754..3860b700 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -442,6 +442,7 @@ func TestLocalVariables(t *testing.T) { {"f32", true, "1.2", "", "float32", nil}, {"i32", true, "[2]int32 [1,2]", "", "[2]int32", nil}, {"i8", true, "1", "", "int8", nil}, + {"mp", true, "map[int]interface {} [1: 42, 2: 43, ]", "", "map[int]interface {}", nil}, {"ms", true, "main.Nest {Level: 0, Nest: *main.Nest {Level: 1, Nest: *(*main.Nest)…", "", "main.Nest", nil}, {"neg", true, "-1", "", "int", nil}, {"u16", true, "65535", "", "uint16", nil},