未验证 提交 4980fff8 编写于 作者: P polinasok 提交者: GitHub

service/dap: Add support for package globals to scopes/variables requests (#2160)

* Support global variables

* Respond to review comments

* Clarify comment

* Add more details to test error messages

* Remove flaky main..inittask checks

* Rename globals flag to match vscode-go

* Normalize filepath with slash separator

* Improve handling for unknown package

* Tweak error message

* More refactoring, normalization and error details to deal with Win test failures

* Clean up optional launch args processing

* Add CurrentPackage to debugger and use instead of ListPackagesBuildInfo
Co-authored-by: NPolina Sokolova <polinasok@users.noreply.github.com>
上级 37d1e010
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings"
"github.com/go-delve/delve/pkg/gobuild" "github.com/go-delve/delve/pkg/gobuild"
"github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/logflags"
...@@ -72,12 +73,15 @@ type launchAttachArgs struct { ...@@ -72,12 +73,15 @@ type launchAttachArgs struct {
stopOnEntry bool stopOnEntry bool
// stackTraceDepth is the maximum length of the returned list of stack frames. // stackTraceDepth is the maximum length of the returned list of stack frames.
stackTraceDepth int stackTraceDepth int
// showGlobalVariables indicates if global package variables should be loaded.
showGlobalVariables bool
} }
// defaultArgs borrows the defaults for the arguments from the original vscode-go adapter. // defaultArgs borrows the defaults for the arguments from the original vscode-go adapter.
var defaultArgs = launchAttachArgs{ var defaultArgs = launchAttachArgs{
stopOnEntry: false, stopOnEntry: false,
stackTraceDepth: 50, stackTraceDepth: 50,
showGlobalVariables: false,
} }
// NewServer creates a new DAP Server. It takes an opened Listener // NewServer creates a new DAP Server. It takes an opened Listener
...@@ -457,13 +461,19 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) { ...@@ -457,13 +461,19 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
return return
} }
stop, ok := request.Arguments["stopOnEntry"] // If user-specified, overwrite the defaults for optional args.
s.args.stopOnEntry = ok && stop == true stop, ok := request.Arguments["stopOnEntry"].(bool)
if ok {
s.args.stopOnEntry = stop
}
depth, ok := request.Arguments["stackTraceDepth"].(float64) depth, ok := request.Arguments["stackTraceDepth"].(float64)
if ok && depth > 0 { if ok && depth > 0 {
s.args.stackTraceDepth = int(depth) s.args.stackTraceDepth = int(depth)
} }
globals, ok := request.Arguments["showGlobalVariables"].(bool)
if ok {
s.args.showGlobalVariables = globals
}
var targetArgs []string var targetArgs []string
args, ok := request.Arguments["args"] args, ok := request.Arguments["args"]
...@@ -732,18 +742,51 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) { ...@@ -732,18 +742,51 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
// Retrieve local variables // Retrieve local variables
locals, err := s.debugger.LocalVariables(goid, frame, 0, cfg) locals, err := s.debugger.LocalVariables(goid, frame, 0, cfg)
if err != nil { if err != nil {
s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list local vars", err.Error()) s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", err.Error())
return return
} }
locScope := &proc.Variable{Name: "Locals", Children: slicePtrVarToSliceVar(locals)} locScope := &proc.Variable{Name: "Locals", Children: slicePtrVarToSliceVar(locals)}
// TODO(polina): Annotate shadowed variables // TODO(polina): Annotate shadowed variables
// TODO(polina): Retrieve global variables
scopeArgs := dap.Scope{Name: argScope.Name, VariablesReference: s.variableHandles.create(argScope)} scopeArgs := dap.Scope{Name: argScope.Name, VariablesReference: s.variableHandles.create(argScope)}
scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)} scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
scopes := []dap.Scope{scopeArgs, scopeLocals} scopes := []dap.Scope{scopeArgs, scopeLocals}
if s.args.showGlobalVariables {
// Limit what global variables we will return to the current package only.
// TODO(polina): This is how vscode-go currently does it to make
// the amount of the returned data manageable. In fact, this is
// considered so expensive even with the package filter, that
// the default for showGlobalVariables was recently flipped to
// not showing. If we delay loading of the globals until the corresponding
// scope is expanded, generating an explicit variable request,
// should we consider making all globals accessible with a scope per package?
// Or users can just rely on watch variables.
currPkg, err := s.debugger.CurrentPackage()
if err != nil {
s.sendErrorResponse(request.Request, UnableToListGlobals, "Unable to list globals", err.Error())
return
}
currPkgFilter := fmt.Sprintf("^%s\\.", currPkg)
globals, err := s.debugger.PackageVariables(s.debugger.CurrentThread().ThreadID(), currPkgFilter, cfg)
if err != nil {
s.sendErrorResponse(request.Request, UnableToListGlobals, "Unable to list globals", err.Error())
return
}
// Remove package prefix from the fully-qualified variable names.
// We will include the package info once in the name of the scope instead.
for i, g := range globals {
globals[i].Name = strings.TrimPrefix(g.Name, currPkg+".")
}
globScope := &proc.Variable{
Name: fmt.Sprintf("Globals (package %s)", currPkg),
Children: slicePtrVarToSliceVar(globals),
}
scopeGlobals := dap.Scope{Name: globScope.Name, VariablesReference: s.variableHandles.create(globScope)}
scopes = append(scopes, scopeGlobals)
}
response := &dap.ScopesResponse{ response := &dap.ScopesResponse{
Response: *newResponse(request.Request), Response: *newResponse(request.Request),
Body: dap.ScopesResponseBody{Scopes: scopes}, Body: dap.ScopesResponseBody{Scopes: scopes},
......
...@@ -341,6 +341,9 @@ func TestSetBreakpoint(t *testing.T) { ...@@ -341,6 +341,9 @@ func TestSetBreakpoint(t *testing.T) {
client.ScopesRequest(1000) client.ScopesRequest(1000)
scopes := client.ExpectScopesResponse(t) scopes := client.ExpectScopesResponse(t)
if len(scopes.Body.Scopes) > 2 {
t.Errorf("\ngot %#v\nwant len(Scopes)=2 (Arguments & Locals)", scopes)
}
expectScope(t, scopes, 0, "Arguments", 1000) expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001) expectScope(t, scopes, 1, "Locals", 1001)
...@@ -413,12 +416,12 @@ func expectScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varR ...@@ -413,12 +416,12 @@ func expectScope(t *testing.T, got *dap.ScopesResponse, i int, name string, varR
} }
// expectChildren is a helper for verifying the number of variables within a VariablesResponse. // expectChildren is a helper for verifying the number of variables within a VariablesResponse.
// parentName - name of the enclosing variable or scope // parentName - pseudoname of the enclosing variable or scope (used for error message only)
// numChildren - number of variables/fields/elements of this variable // numChildren - number of variables/fields/elements of this variable
func expectChildren(t *testing.T, got *dap.VariablesResponse, parentName string, numChildren int) { func expectChildren(t *testing.T, got *dap.VariablesResponse, parentName string, numChildren int) {
t.Helper() t.Helper()
if len(got.Body.Variables) != numChildren { if len(got.Body.Variables) != numChildren {
t.Errorf("\ngot len(%s)=%d\nwant %d", parentName, len(got.Body.Variables), numChildren) t.Errorf("\ngot len(%s)=%d (children=%#v)\nwant len=%d", parentName, len(got.Body.Variables), got.Body.Variables, numChildren)
} }
} }
...@@ -432,7 +435,7 @@ func expectChildren(t *testing.T, got *dap.VariablesResponse, parentName string, ...@@ -432,7 +435,7 @@ func expectChildren(t *testing.T, got *dap.VariablesResponse, parentName string,
func expectVar(t *testing.T, got *dap.VariablesResponse, i int, name, value string, useExactMatch, hasRef bool) (ref int) { func expectVar(t *testing.T, got *dap.VariablesResponse, i int, name, value string, useExactMatch, hasRef bool) (ref int) {
t.Helper() t.Helper()
if len(got.Body.Variables) <= i { if len(got.Body.Variables) <= i {
t.Errorf("\ngot len=%d\nwant len>%d", len(got.Body.Variables), i) t.Errorf("\ngot len=%d (children=%#v)\nwant len>%d", len(got.Body.Variables), got.Body.Variables, i)
return return
} }
if i < 0 { if i < 0 {
...@@ -556,7 +559,9 @@ func TestScopesAndVariablesRequests(t *testing.T) { ...@@ -556,7 +559,9 @@ func TestScopesAndVariablesRequests(t *testing.T) {
runDebugSessionWithBPs(t, client, runDebugSessionWithBPs(t, client,
// Launch // Launch
func() { func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry) client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
})
}, },
// Breakpoints are set within the program // Breakpoints are set within the program
fixture.Source, []int{}, fixture.Source, []int{},
...@@ -580,6 +585,7 @@ func TestScopesAndVariablesRequests(t *testing.T) { ...@@ -580,6 +585,7 @@ func TestScopesAndVariablesRequests(t *testing.T) {
scopes := client.ExpectScopesResponse(t) scopes := client.ExpectScopesResponse(t)
expectScope(t, scopes, 0, "Arguments", 1000) expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001) expectScope(t, scopes, 1, "Locals", 1001)
expectScope(t, scopes, 2, "Globals (package main)", 1002)
// Arguments // Arguments
...@@ -596,6 +602,12 @@ func TestScopesAndVariablesRequests(t *testing.T) { ...@@ -596,6 +602,12 @@ func TestScopesAndVariablesRequests(t *testing.T) {
expectVarExact(t, bar, 1, "Bur", `"lorem"`, noChildren) expectVarExact(t, bar, 1, "Bur", `"lorem"`, noChildren)
} }
// Globals
client.VariablesRequest(1002)
globals := client.ExpectVariablesResponse(t)
expectVarExact(t, globals, 0, "p1", "10", noChildren)
// Locals // Locals
client.VariablesRequest(1001) client.VariablesRequest(1001)
...@@ -775,6 +787,7 @@ func TestScopesAndVariablesRequests(t *testing.T) { ...@@ -775,6 +787,7 @@ func TestScopesAndVariablesRequests(t *testing.T) {
scopes := client.ExpectScopesResponse(t) scopes := client.ExpectScopesResponse(t)
expectScope(t, scopes, 0, "Arguments", 1000) expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001) expectScope(t, scopes, 1, "Locals", 1001)
expectScope(t, scopes, 2, "Globals (package main)", 1002)
client.ScopesRequest(1111) client.ScopesRequest(1111)
erres := client.ExpectErrorResponse(t) erres := client.ExpectErrorResponse(t)
...@@ -791,6 +804,10 @@ func TestScopesAndVariablesRequests(t *testing.T) { ...@@ -791,6 +804,10 @@ func TestScopesAndVariablesRequests(t *testing.T) {
expectChildren(t, locals, "Locals", 1) expectChildren(t, locals, "Locals", 1)
expectVarExact(t, locals, -1, "a1", `"bur"`, noChildren) expectVarExact(t, locals, -1, "a1", `"bur"`, noChildren)
client.VariablesRequest(1002) // Globals
globals := client.ExpectVariablesResponse(t)
expectVarExact(t, globals, 0, "p1", "10", noChildren)
client.VariablesRequest(7777) client.VariablesRequest(7777)
erres = client.ExpectErrorResponse(t) erres = client.ExpectErrorResponse(t)
if erres.Body.Error.Format != "Unable to lookup variable: unknown reference 7777" { if erres.Body.Error.Format != "Unable to lookup variable: unknown reference 7777" {
...@@ -835,6 +852,9 @@ func TestScopesAndVariablesRequests2(t *testing.T) { ...@@ -835,6 +852,9 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
client.ScopesRequest(1000) client.ScopesRequest(1000)
scopes := client.ExpectScopesResponse(t) scopes := client.ExpectScopesResponse(t)
if len(scopes.Body.Scopes) > 2 {
t.Errorf("\ngot %#v\nwant len(scopes)=2 (Argumes & Locals)", scopes)
}
expectScope(t, scopes, 0, "Arguments", 1000) expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001) expectScope(t, scopes, 1, "Locals", 1001)
...@@ -964,18 +984,18 @@ func TestScopesAndVariablesRequests2(t *testing.T) { ...@@ -964,18 +984,18 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
ref = expectVarExact(t, m4, 2, "[key 1]", "<main.astruct>", hasChildren) ref = expectVarExact(t, m4, 2, "[key 1]", "<main.astruct>", hasChildren)
if ref > 0 { if ref > 0 {
client.VariablesRequest(ref) client.VariablesRequest(ref)
m4_key1 := client.ExpectVariablesResponse(t) m4Key1 := client.ExpectVariablesResponse(t)
expectChildren(t, m4_key1, "m4_key1", 2) expectChildren(t, m4Key1, "m4Key1", 2)
expectVarExact(t, m4_key1, 0, "A", "2", noChildren) expectVarExact(t, m4Key1, 0, "A", "2", noChildren)
expectVarExact(t, m4_key1, 1, "B", "2", noChildren) expectVarExact(t, m4Key1, 1, "B", "2", noChildren)
} }
ref = expectVarExact(t, m4, 3, "[val 1]", "<main.astruct>", hasChildren) ref = expectVarExact(t, m4, 3, "[val 1]", "<main.astruct>", hasChildren)
if ref > 0 { if ref > 0 {
client.VariablesRequest(ref) client.VariablesRequest(ref)
m4_val1 := client.ExpectVariablesResponse(t) m4Val1 := client.ExpectVariablesResponse(t)
expectChildren(t, m4_val1, "m4_val1", 2) expectChildren(t, m4Val1, "m4Val1", 2)
expectVarExact(t, m4_val1, 0, "A", "22", noChildren) expectVarExact(t, m4Val1, 0, "A", "22", noChildren)
expectVarExact(t, m4_val1, 1, "B", "22", noChildren) expectVarExact(t, m4Val1, 1, "B", "22", noChildren)
} }
} }
expectVarExact(t, locals, -1, "emptymap", "<map[string]string> (length: 0)", noChildren) expectVarExact(t, locals, -1, "emptymap", "<map[string]string> (length: 0)", noChildren)
...@@ -1020,6 +1040,73 @@ func TestScopesAndVariablesRequests2(t *testing.T) { ...@@ -1020,6 +1040,73 @@ func TestScopesAndVariablesRequests2(t *testing.T) {
}) })
} }
// TestGlobalScopeAndVariables launches the program with showGlobalVariables
// arg set, executes to a breakpoint in the main package and tests that global
// package main variables got loaded. It then steps into a function
// in another package and tests that globals scope got updated to those vars.
func TestGlobalScopeAndVariables(t *testing.T) {
runTest(t, "consts", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client,
// Launch
func() {
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "exec", "program": fixture.Path, "showGlobalVariables": true,
})
},
// Breakpoints are set within the program
fixture.Source, []int{},
[]onBreakpoint{{
// Stop at line 36
execute: func() {
client.StackTraceRequest(1, 0, 20)
stack := client.ExpectStackTraceResponse(t)
expectStackFrames(t, stack, 36, 1000, 3, 3)
client.ScopesRequest(1000)
scopes := client.ExpectScopesResponse(t)
expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001)
expectScope(t, scopes, 2, "Globals (package main)", 1002)
client.VariablesRequest(1002)
client.ExpectVariablesResponse(t)
// The program has no user-defined globals.
// Depending on the Go version, there might
// be some runtime globals (e.g. main..inittask)
// so testing for the total number is too fragile.
// Step into pkg.AnotherMethod()
client.StepInRequest(1)
client.ExpectStepInResponse(t)
client.ExpectStoppedEvent(t)
client.StackTraceRequest(1, 0, 20)
stack = client.ExpectStackTraceResponse(t)
expectStackFrames(t, stack, 14, 1000, 4, 4)
client.ScopesRequest(1000)
scopes = client.ExpectScopesResponse(t)
expectScope(t, scopes, 0, "Arguments", 1000)
expectScope(t, scopes, 1, "Locals", 1001)
expectScope(t, scopes, 2, "Globals (package github.com/go-delve/delve/_fixtures/internal/dir0/pkg)", 1002)
client.VariablesRequest(1002)
globals := client.ExpectVariablesResponse(t)
expectChildren(t, globals, "Globals", 1)
ref := expectVarExact(t, globals, 0, "SomeVar", "<github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType>", hasChildren)
if ref > 0 {
client.VariablesRequest(ref)
somevar := client.ExpectVariablesResponse(t)
expectChildren(t, somevar, "SomeVar", 1)
expectVarExact(t, somevar, 0, "X", "0", noChildren)
}
},
disconnect: false,
}})
})
}
// Tests that 'stackTraceDepth' from LaunchRequest is parsed and passed to // Tests that 'stackTraceDepth' from LaunchRequest is parsed and passed to
// stacktrace requests handlers. // stacktrace requests handlers.
func TestLaunchRequestWithStackTraceDepth(t *testing.T) { func TestLaunchRequestWithStackTraceDepth(t *testing.T) {
......
...@@ -1415,6 +1415,26 @@ func (d *Debugger) convertDefers(defers []*proc.Defer) []api.Defer { ...@@ -1415,6 +1415,26 @@ func (d *Debugger) convertDefers(defers []*proc.Defer) []api.Defer {
return r return r
} }
// CurrentPackage returns the fully qualified name of the
// package corresponding to the function location of the
// current thread.
func (d *Debugger) CurrentPackage() (string, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
if _, err := d.target.Valid(); err != nil {
return "", err
}
loc, err := d.target.CurrentThread().Location()
if err != nil {
return "", err
}
if loc.Fn == nil {
return "", fmt.Errorf("unable to determine current package due to unspecified function location")
}
return loc.Fn.PackageName(), nil
}
// FindLocation will find the location specified by 'locStr'. // FindLocation will find the location specified by 'locStr'.
func (d *Debugger) FindLocation(goid, frame, deferredCall int, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { func (d *Debugger) FindLocation(goid, frame, deferredCall int, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
d.targetMutex.Lock() d.targetMutex.Lock()
...@@ -1490,6 +1510,13 @@ func (d *Debugger) Recorded() (recorded bool, tracedir string) { ...@@ -1490,6 +1510,13 @@ func (d *Debugger) Recorded() (recorded bool, tracedir string) {
return d.target.Recorded() return d.target.Recorded()
} }
// CurrentThread returns the current thread.
func (d *Debugger) CurrentThread() proc.Thread {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.target.CurrentThread()
}
// Checkpoint will set a checkpoint specified by the locspec. // Checkpoint will set a checkpoint specified by the locspec.
func (d *Debugger) Checkpoint(where string) (int, error) { func (d *Debugger) Checkpoint(where string) (int, error) {
d.targetMutex.Lock() d.targetMutex.Lock()
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册