mirror of
https://github.com/go-delve/delve.git
synced 2025-10-29 09:46:56 +08:00
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 <polinasok@users.noreply.github.com>
This commit is contained in:
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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: <interface{}(main.MyStruct)>
|
||||
// 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
|
||||
|
||||
|
||||
@ -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, "", "<int>", 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, "<interface {}(int)>", 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, "<main.Nest>", 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, "<interface {}(string)>", hasChildren)
|
||||
if ref > 0 {
|
||||
client.VariablesRequest(ref)
|
||||
rv := client.ExpectVariablesResponse(t)
|
||||
expectChildren(t, rv, "rv", 1)
|
||||
ref = expectVarExact(t, rv, 0, "~panic", "<interface {}(string)>", 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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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},
|
||||
|
||||
Reference in New Issue
Block a user