service/dap: implement array, slice, and map paging (#2512)
If the client supports paging, we allow them to fetch array and slice items in chunks that we assume will be of a reasonable size. For example, VS Code requests indexed variables in chunks of 100. Fixes golang/vscode-go#1518
This commit is contained in:
parent
fb4d6f1144
commit
30b3cc2c6f
@ -1950,6 +1950,31 @@ func (v *Variable) mapAccess(idx *Variable) (*Variable, error) {
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
// LoadResliced returns a new array, slice or map that starts at index start and contains
|
||||
// up to cfg.MaxArrayValues children.
|
||||
func (v *Variable) LoadResliced(start int, cfg LoadConfig) (newV *Variable, err error) {
|
||||
switch v.Kind {
|
||||
case reflect.Array, reflect.Slice:
|
||||
low, high := int64(start), int64(start+cfg.MaxArrayValues)
|
||||
if high > v.Len {
|
||||
high = v.Len
|
||||
}
|
||||
newV, err = v.reslice(low, high)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case reflect.Map:
|
||||
newV = v.clone()
|
||||
newV.Children = nil
|
||||
newV.loaded = false
|
||||
newV.mapSkip = start
|
||||
default:
|
||||
return nil, fmt.Errorf("variable to reslice is not an array, slice, or map")
|
||||
}
|
||||
newV.loadValue(cfg)
|
||||
return newV, nil
|
||||
}
|
||||
|
||||
func (v *Variable) reslice(low int64, high int64) (*Variable, error) {
|
||||
wrong := false
|
||||
cptrNeedsFakeSlice := false
|
||||
|
@ -343,6 +343,16 @@ func (c *Client) VariablesRequest(variablesReference int) {
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// IndexedVariablesRequest sends a 'variables' request.
|
||||
func (c *Client) IndexedVariablesRequest(variablesReference, start, count int) {
|
||||
request := &dap.VariablesRequest{Request: *c.newRequest("variables")}
|
||||
request.Arguments.VariablesReference = variablesReference
|
||||
request.Arguments.Filter = "indexed"
|
||||
request.Arguments.Start = start
|
||||
request.Arguments.Count = count
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// TeriminateRequest sends a 'terminate' request.
|
||||
func (c *Client) TerminateRequest() {
|
||||
c.send(&dap.TerminateRequest{Request: *c.newRequest("terminate")})
|
||||
|
@ -24,6 +24,9 @@ type fullyQualifiedVariable struct {
|
||||
fullyQualifiedNameOrExpr string
|
||||
// True if this represents variable scope
|
||||
isScope bool
|
||||
// startIndex is the index of the first child for an array or slice.
|
||||
// This variable represents a chunk of the array, slice or map.
|
||||
startIndex int
|
||||
}
|
||||
|
||||
func newHandlesMap() *handlesMap {
|
||||
|
@ -1588,7 +1588,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
|
||||
s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to list args", err.Error())
|
||||
return
|
||||
}
|
||||
argScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Arguments%s", suffix), Children: slicePtrVarToSliceVar(args)}, "", true}
|
||||
argScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Arguments%s", suffix), Children: slicePtrVarToSliceVar(args)}, "", true, 0}
|
||||
|
||||
// Retrieve local variables
|
||||
locals, err := s.debugger.LocalVariables(goid, frame, 0, DefaultLoadConfig)
|
||||
@ -1596,7 +1596,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
|
||||
s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", err.Error())
|
||||
return
|
||||
}
|
||||
locScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Locals%s", suffix), Children: slicePtrVarToSliceVar(locals)}, "", true}
|
||||
locScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Locals%s", suffix), Children: slicePtrVarToSliceVar(locals)}, "", true, 0}
|
||||
|
||||
scopeArgs := dap.Scope{Name: argScope.Name, VariablesReference: s.variableHandles.create(argScope)}
|
||||
scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
|
||||
@ -1632,7 +1632,7 @@ func (s *Server) onScopesRequest(request *dap.ScopesRequest) {
|
||||
globScope := &fullyQualifiedVariable{&proc.Variable{
|
||||
Name: fmt.Sprintf("Globals (package %s)", currPkg),
|
||||
Children: slicePtrVarToSliceVar(globals),
|
||||
}, currPkg, true}
|
||||
}, currPkg, true, 0}
|
||||
scopeGlobals := dap.Scope{Name: globScope.Name, VariablesReference: s.variableHandles.create(globScope)}
|
||||
scopes = append(scopes, scopeGlobals)
|
||||
}
|
||||
@ -1661,6 +1661,18 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// If there is a filter applied, we will need to create a new variable that includes
|
||||
// the values actually needed to load. This cannot be done when loading the parent
|
||||
// node, since it is unknown at that point which children will need to be loaded.
|
||||
if request.Arguments.Filter == "indexed" {
|
||||
var err error
|
||||
v, err = s.maybeLoadResliced(v, request.Arguments.Start, request.Arguments.Count)
|
||||
if err != nil {
|
||||
s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
children, err := s.childrenToDAPVariables(v)
|
||||
if err != nil {
|
||||
s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
|
||||
@ -1673,6 +1685,30 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
|
||||
s.send(response)
|
||||
}
|
||||
|
||||
func (s *Server) maybeLoadResliced(v *fullyQualifiedVariable, start, count int) (*fullyQualifiedVariable, error) {
|
||||
if start == 0 && count == len(v.Children) {
|
||||
// If we have already loaded the correct children,
|
||||
// just return the variable.
|
||||
return v, nil
|
||||
}
|
||||
indexedLoadConfig := DefaultLoadConfig
|
||||
indexedLoadConfig.MaxArrayValues = count
|
||||
newV, err := s.debugger.LoadResliced(v.Variable, start, indexedLoadConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fullyQualifiedVariable{newV, v.fullyQualifiedNameOrExpr, false, start}, nil
|
||||
}
|
||||
|
||||
func getIndexedVariableCount(c *proc.Variable) int {
|
||||
indexedVars := 0
|
||||
switch c.Kind {
|
||||
case reflect.Array, reflect.Slice, reflect.Map:
|
||||
indexedVars = int(c.Len)
|
||||
}
|
||||
return indexedVars
|
||||
}
|
||||
|
||||
// childrenToDAPVariables returns the DAP presentation of the referenced variable's children.
|
||||
func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
|
||||
// TODO(polina): consider convertVariableToString instead of convertVariable
|
||||
@ -1709,18 +1745,20 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
|
||||
// Otherwise, we must return separate variables for both.
|
||||
if keyref > 0 && valref > 0 { // Both are not scalars
|
||||
keyvar := dap.Variable{
|
||||
Name: fmt.Sprintf("[key %d]", kvIndex),
|
||||
Name: fmt.Sprintf("[key %d]", v.startIndex+kvIndex),
|
||||
EvaluateName: keyexpr,
|
||||
Type: keyType,
|
||||
Value: key,
|
||||
VariablesReference: keyref,
|
||||
IndexedVariables: getIndexedVariableCount(keyv),
|
||||
}
|
||||
valvar := dap.Variable{
|
||||
Name: fmt.Sprintf("[val %d]", kvIndex),
|
||||
Name: fmt.Sprintf("[val %d]", v.startIndex+kvIndex),
|
||||
EvaluateName: valexpr,
|
||||
Type: valType,
|
||||
Value: val,
|
||||
VariablesReference: valref,
|
||||
IndexedVariables: getIndexedVariableCount(valv),
|
||||
}
|
||||
children = append(children, keyvar, valvar)
|
||||
} else { // At least one is a scalar
|
||||
@ -1740,8 +1778,10 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
|
||||
kvvar.Name = fmt.Sprintf("%s... @ %#x", key[0:DefaultLoadConfig.MaxStringLen], keyv.Addr)
|
||||
}
|
||||
kvvar.VariablesReference = keyref
|
||||
kvvar.IndexedVariables = getIndexedVariableCount(keyv)
|
||||
} else if valref != 0 { // val is a type to be expanded
|
||||
kvvar.VariablesReference = valref
|
||||
kvvar.IndexedVariables = getIndexedVariableCount(valv)
|
||||
}
|
||||
children = append(children, kvvar)
|
||||
}
|
||||
@ -1749,14 +1789,16 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
|
||||
case reflect.Slice, reflect.Array:
|
||||
children = make([]dap.Variable, len(v.Children))
|
||||
for i := range v.Children {
|
||||
cfqname := fmt.Sprintf("%s[%d]", v.fullyQualifiedNameOrExpr, i)
|
||||
idx := v.startIndex + i
|
||||
cfqname := fmt.Sprintf("%s[%d]", v.fullyQualifiedNameOrExpr, idx)
|
||||
cvalue, cvarref := s.convertVariable(&v.Children[i], cfqname)
|
||||
children[i] = dap.Variable{
|
||||
Name: fmt.Sprintf("[%d]", i),
|
||||
Name: fmt.Sprintf("[%d]", idx),
|
||||
EvaluateName: cfqname,
|
||||
Type: s.getTypeIfSupported(&v.Children[i]),
|
||||
Value: cvalue,
|
||||
VariablesReference: cvarref,
|
||||
IndexedVariables: getIndexedVariableCount(&v.Children[i]),
|
||||
}
|
||||
}
|
||||
default:
|
||||
@ -1795,6 +1837,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
|
||||
Type: s.getTypeIfSupported(c),
|
||||
Value: cvalue,
|
||||
VariablesReference: cvarref,
|
||||
IndexedVariables: getIndexedVariableCount(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1849,7 +1892,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
|
||||
if opts&skipRef != 0 {
|
||||
return 0
|
||||
}
|
||||
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/})
|
||||
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/, 0})
|
||||
}
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
if v.Unreadable != nil {
|
||||
@ -1858,15 +1901,13 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
|
||||
|
||||
// Some of the types might be fully or partially not loaded based on LoadConfig.
|
||||
// Those that are fully missing (e.g. due to hitting MaxVariableRecurse), can be reloaded in place.
|
||||
// Those that are partially missing (e.g. MaxArrayValues from a large array), need a more creative solution
|
||||
// that is still to be determined. For now, clearly communicate when that happens with additional value labels.
|
||||
// TODO(polina): look into layered/paged loading for truncated strings, arrays, maps and structs.
|
||||
var reloadVariable = func(v *proc.Variable, qualifiedNameOrExpr string) (value string) {
|
||||
// We might be loading variables from the frame that's not topmost, so use
|
||||
// frame-independent address-based expression, not fully-qualified name as per
|
||||
// https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables.
|
||||
// TODO(polina): Get *proc.Variable object from debugger instead. Export and set v.loaded to false
|
||||
// and call v.loadValue gain. It's more efficient, and it's guaranteed to keep working with generics.
|
||||
// TODO(polina): Get *proc.Variable object from debugger instead. Export a function to set v.loaded to false
|
||||
// and call v.loadValue gain with a different load config. It's more efficient, and it's guaranteed to keep
|
||||
// working with generics.
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
typeName := api.PrettyTypeName(v.DwarfType)
|
||||
loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr)
|
||||
@ -1923,7 +1964,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
|
||||
if v.Len > int64(len(v.Children)) { // Not fully loaded
|
||||
if v.Base != 0 && len(v.Children) == 0 { // Fully missing
|
||||
value = reloadVariable(v, qualifiedNameOrExpr)
|
||||
} else { // Partially missing (TODO)
|
||||
} else if !s.clientCapabilities.supportsVariablePaging {
|
||||
value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children), v.Len) + value
|
||||
}
|
||||
}
|
||||
@ -1934,7 +1975,7 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
|
||||
if v.Len > int64(len(v.Children)/2) { // Not fully loaded
|
||||
if len(v.Children) == 0 { // Fully missing
|
||||
value = reloadVariable(v, qualifiedNameOrExpr)
|
||||
} else { // Partially missing (TODO)
|
||||
} else if !s.clientCapabilities.supportsVariablePaging {
|
||||
value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children)/2, v.Len) + value
|
||||
}
|
||||
}
|
||||
@ -2033,7 +2074,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
|
||||
}
|
||||
response.Body = dap.EvaluateResponseBody{
|
||||
Result: strings.TrimRight(retVarsAsStr, ", "),
|
||||
VariablesReference: s.variableHandles.create(&fullyQualifiedVariable{retVarsAsVar, "", false /*not a scope*/}),
|
||||
VariablesReference: s.variableHandles.create(&fullyQualifiedVariable{retVarsAsVar, "", false /*not a scope*/, 0}),
|
||||
}
|
||||
}
|
||||
} else { // {expression}
|
||||
@ -2064,7 +2105,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
|
||||
opts |= showFullValue
|
||||
}
|
||||
exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts)
|
||||
response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef}
|
||||
response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar)}
|
||||
}
|
||||
s.send(response)
|
||||
}
|
||||
|
@ -667,7 +667,7 @@ func checkChildren(t *testing.T, got *dap.VariablesResponse, parentName string,
|
||||
// useExactMatch - true if name, evalName and value are to be compared to exactly, false if to be used as regex
|
||||
// hasRef - true if the variable should have children and therefore a non-0 variable reference
|
||||
// ref - reference to retrieve children of this variable (0 if none)
|
||||
func checkVar(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, useExactMatch, hasRef bool) (ref int) {
|
||||
func checkVar(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, useExactMatch, hasRef bool, indexed, named int) (ref int) {
|
||||
t.Helper()
|
||||
if len(got.Body.Variables) <= i {
|
||||
t.Errorf("\ngot len=%d (children=%#v)\nwant len>%d", len(got.Body.Variables), got.Body.Variables, i)
|
||||
@ -723,19 +723,37 @@ func checkVar(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, v
|
||||
if !matchedType {
|
||||
t.Errorf("\ngot %s=%q\nwant %q", name, goti.Type, typ)
|
||||
}
|
||||
if indexed >= 0 && goti.IndexedVariables != indexed {
|
||||
t.Errorf("\ngot %s=%d indexed\nwant %d indexed", name, goti.IndexedVariables, indexed)
|
||||
}
|
||||
if named >= 0 && goti.NamedVariables != named {
|
||||
t.Errorf("\ngot %s=%d named\nwant %d named", name, goti.NamedVariables, named)
|
||||
}
|
||||
return goti.VariablesReference
|
||||
}
|
||||
|
||||
// checkVarExact is a helper like checkVar that matches value exactly.
|
||||
func checkVarExact(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, hasRef bool) (ref int) {
|
||||
t.Helper()
|
||||
return checkVar(t, got, i, name, evalName, value, typ, true, hasRef)
|
||||
return checkVarExactIndexed(t, got, i, name, evalName, value, typ, hasRef, -1, -1)
|
||||
}
|
||||
|
||||
// checkVarExact is a helper like checkVar that matches value exactly.
|
||||
func checkVarExactIndexed(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, hasRef bool, indexed, named int) (ref int) {
|
||||
t.Helper()
|
||||
return checkVar(t, got, i, name, evalName, value, typ, true, hasRef, indexed, named)
|
||||
}
|
||||
|
||||
// checkVarRegex is a helper like checkVar that treats value, evalName or name as a regex.
|
||||
func checkVarRegex(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, hasRef bool) (ref int) {
|
||||
t.Helper()
|
||||
return checkVar(t, got, i, name, evalName, value, typ, false, hasRef)
|
||||
return checkVarRegexIndexed(t, got, i, name, evalName, value, typ, hasRef, -1, -1)
|
||||
}
|
||||
|
||||
// checkVarRegex is a helper like checkVar that treats value, evalName or name as a regex.
|
||||
func checkVarRegexIndexed(t *testing.T, got *dap.VariablesResponse, i int, name, evalName, value, typ string, hasRef bool, indexed, named int) (ref int) {
|
||||
t.Helper()
|
||||
return checkVar(t, got, i, name, evalName, value, typ, false, hasRef, indexed, named)
|
||||
}
|
||||
|
||||
func expectMessageFilterStopped(t *testing.T, client *daptest.Client) dap.Message {
|
||||
@ -1481,28 +1499,91 @@ func TestVariablesLoading(t *testing.T) {
|
||||
// String partially missing based on LoadConfig.MaxStringLen
|
||||
checkVarExact(t, locals, -1, "longstr", "longstr", "\"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more\"", "string", noChildren)
|
||||
|
||||
// Array partially missing based on LoadConfig.MaxArrayValues
|
||||
ref := checkVarExact(t, locals, -1, "longarr", "longarr", "(loaded 64/100) [100]int [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+36 more]", "[100]int", hasChildren)
|
||||
checkArrayChildren := func(t *testing.T, longarr *dap.VariablesResponse, parentName string, start int) {
|
||||
t.Helper()
|
||||
for i, child := range longarr.Body.Variables {
|
||||
idx := start + i
|
||||
if child.Name != fmt.Sprintf("[%d]", idx) || child.EvaluateName != fmt.Sprintf("%s[%d]", parentName, idx) {
|
||||
t.Errorf("Expected %s[%d] to have Name=\"[%d]\" EvaluateName=\"%s[%d]\", got %#v", parentName, idx, idx, parentName, idx, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Array not fully loaded based on LoadConfig.MaxArrayValues.
|
||||
// Expect to be able to load array by paging.
|
||||
ref := checkVarExactIndexed(t, locals, -1, "longarr", "longarr", "[100]int [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+36 more]", "[100]int", hasChildren, 100, 0)
|
||||
if ref > 0 {
|
||||
client.VariablesRequest(ref)
|
||||
longarr := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longarr", 64)
|
||||
checkArrayChildren(t, longarr, "longarr", 0)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 0, 100)
|
||||
longarr = client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longarr", 100)
|
||||
checkArrayChildren(t, longarr, "longarr", 0)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 50, 50)
|
||||
longarr = client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longarr", 50)
|
||||
checkArrayChildren(t, longarr, "longarr", 50)
|
||||
|
||||
}
|
||||
|
||||
// Slice partially missing based on LoadConfig.MaxArrayValues
|
||||
ref = checkVarExact(t, locals, -1, "longslice", "longslice", "(loaded 64/100) []int len: 100, cap: 100, [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+36 more]", "[]int", hasChildren)
|
||||
// Slice not fully loaded based on LoadConfig.MaxArrayValues.
|
||||
// Expect to be able to load slice by paging.
|
||||
ref = checkVarExactIndexed(t, locals, -1, "longslice", "longslice", "[]int len: 100, cap: 100, [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+36 more]", "[]int", hasChildren, 100, 0)
|
||||
if ref > 0 {
|
||||
client.VariablesRequest(ref)
|
||||
longarr := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longslice", 64)
|
||||
checkArrayChildren(t, longarr, "longslice", 0)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 0, 100)
|
||||
longarr = client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longslice", 100)
|
||||
checkArrayChildren(t, longarr, "longslice", 0)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 50, 50)
|
||||
longarr = client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, longarr, "longslice", 50)
|
||||
checkArrayChildren(t, longarr, "longslice", 50)
|
||||
}
|
||||
|
||||
// Map partially missing based on LoadConfig.MaxArrayValues
|
||||
ref = checkVarRegex(t, locals, -1, "m1", "m1", `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.`, `map\[string\]main\.astruct`, hasChildren)
|
||||
// Map not fully loaded based on LoadConfig.MaxArrayValues
|
||||
// Expect to be able to load map by paging.
|
||||
ref = checkVarRegexIndexed(t, locals, -1, "m1", "m1", `map\[string\]main\.astruct \[.+\.\.\.`, `map\[string\]main\.astruct`, hasChildren, 66, 0)
|
||||
if ref > 0 {
|
||||
client.VariablesRequest(ref)
|
||||
m1 := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, m1, "m1", 64)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 0, 66)
|
||||
m1 = client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, m1, "m1", 66)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 0, 33)
|
||||
m1part1 := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, m1part1, "m1", 33)
|
||||
|
||||
client.IndexedVariablesRequest(ref, 33, 33)
|
||||
m1part2 := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, m1part2, "m1", 33)
|
||||
|
||||
if len(m1part1.Body.Variables)+len(m1part2.Body.Variables) == len(m1.Body.Variables) {
|
||||
for i, got := range m1part1.Body.Variables {
|
||||
want := m1.Body.Variables[i]
|
||||
if got.Name != want.Name || got.Value != want.Value {
|
||||
t.Errorf("got %#v, want Name=%q Value=%q", got, want.Name, want.Value)
|
||||
}
|
||||
}
|
||||
for i, got := range m1part2.Body.Variables {
|
||||
want := m1.Body.Variables[i+len(m1part1.Body.Variables)]
|
||||
if got.Name != want.Name || got.Value != want.Value {
|
||||
t.Errorf("got %#v, want Name=%q Value=%q", got, want.Name, want.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Struct partially missing based on LoadConfig.MaxStructFields
|
||||
@ -2570,6 +2651,18 @@ func checkEval(t *testing.T, got *dap.EvaluateResponse, value string, hasRef boo
|
||||
return got.Body.VariablesReference
|
||||
}
|
||||
|
||||
// checkEvalIndexed 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 checkEvalIndexed(t *testing.T, got *dap.EvaluateResponse, value string, hasRef bool, indexed, named int) (ref int) {
|
||||
t.Helper()
|
||||
if got.Body.Result != value || (got.Body.VariablesReference > 0) != hasRef || got.Body.IndexedVariables != indexed || got.Body.NamedVariables != named {
|
||||
t.Errorf("\ngot %#v\nwant Result=%q hasRef=%t IndexedVariables=%d NamedVariables=%d", got, value, hasRef, indexed, named)
|
||||
}
|
||||
return got.Body.VariablesReference
|
||||
}
|
||||
|
||||
func checkEvalRegex(t *testing.T, got *dap.EvaluateResponse, valueRegex string, hasRef bool) (ref int) {
|
||||
t.Helper()
|
||||
matched, _ := regexp.MatchString(valueRegex, got.Body.Result)
|
||||
@ -2612,7 +2705,7 @@ func TestEvaluateRequest(t *testing.T) {
|
||||
// Variable lookup that's not fully loaded
|
||||
client.EvaluateRequest("ba", 1000, "this context will be ignored")
|
||||
got = client.ExpectEvaluateResponse(t)
|
||||
checkEval(t, got, "(loaded 64/200) []int len: 200, cap: 200, [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+136 more]", hasChildren)
|
||||
checkEvalIndexed(t, got, "[]int len: 200, cap: 200, [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+136 more]", hasChildren, 200, 0)
|
||||
|
||||
// All (binary and unary) on basic types except <-, ++ and --
|
||||
client.EvaluateRequest("1+1", 1000, "this context will be ignored")
|
||||
@ -2644,7 +2737,7 @@ func TestEvaluateRequest(t *testing.T) {
|
||||
// Type casts between string, []byte and []rune
|
||||
client.EvaluateRequest("[]byte(\"ABC€\")", 1000, "this context will be ignored")
|
||||
got = client.ExpectEvaluateResponse(t)
|
||||
checkEval(t, got, "[]uint8 len: 6, cap: 6, [65,66,67,226,130,172]", noChildren)
|
||||
checkEvalIndexed(t, got, "[]uint8 len: 6, cap: 6, [65,66,67,226,130,172]", noChildren, 6, 0)
|
||||
|
||||
// Struct member access (i.e. somevar.memberfield)
|
||||
client.EvaluateRequest("ms.Nest.Level", 1000, "this context will be ignored")
|
||||
@ -2783,8 +2876,8 @@ func TestEvaluateRequestLongStrLargeValue(t *testing.T) {
|
||||
checkVarExact(t, locals, -1, "m6", "m6", `main.C {s: `+longstrTruncated+`}`, "main.C", hasChildren)
|
||||
|
||||
// large array
|
||||
m1 := `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`
|
||||
m1Truncated := `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.`
|
||||
m1 := `map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`
|
||||
m1Truncated := `map\[string\]main\.astruct \[.+\.\.\.`
|
||||
|
||||
client.EvaluateRequest("m1", 0, evalContext)
|
||||
got3 := client.ExpectEvaluateResponse(t)
|
||||
|
@ -1464,6 +1464,14 @@ func (d *Debugger) EvalVariableInScope(goid, frame, deferredCall int, symbol str
|
||||
return s.EvalVariable(symbol, cfg)
|
||||
}
|
||||
|
||||
// LoadResliced will attempt to 'reslice' a map, array or slice so that the values
|
||||
// up to cfg.MaxArrayValues children are loaded starting from index start.
|
||||
func (d *Debugger) LoadResliced(v *proc.Variable, start int, cfg proc.LoadConfig) (*proc.Variable, error) {
|
||||
d.targetMutex.Lock()
|
||||
defer d.targetMutex.Unlock()
|
||||
return v.LoadResliced(start, cfg)
|
||||
}
|
||||
|
||||
// SetVariableInScope will set the value of the variable represented by
|
||||
// 'symbol' to the value given, in the given scope.
|
||||
func (d *Debugger) SetVariableInScope(goid, frame, deferredCall int, symbol, value string) error {
|
||||
|
Loading…
Reference in New Issue
Block a user