From 477e46ebbdf8af550807c6fa0218edc023fd47a5 Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Thu, 5 Dec 2024 04:14:47 +0100 Subject: [PATCH] pkg/proc: support swiss table map implementation (#3838) Adds support for the swiss table implementation for Go's maps. --- _fixtures/testvariables2.go | 8 +- _scripts/rtype-out.txt | 2 + _scripts/rtype.go | 77 ++++- cmd/dlv/dlv_test.go | 6 + pkg/proc/eval.go | 6 +- pkg/proc/mapiter.go | 602 ++++++++++++++++++++++++++++++++++++ pkg/proc/variables.go | 265 +--------------- pkg/proc/variables_test.go | 101 ++++-- 8 files changed, 760 insertions(+), 307 deletions(-) create mode 100644 pkg/proc/mapiter.go diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go index 6fb3bccc..a20d9fb7 100644 --- a/_fixtures/testvariables2.go +++ b/_fixtures/testvariables2.go @@ -34,6 +34,11 @@ type astruct struct { B int } +type largestruct struct { + name string + v [256]byte +} + type astructName1 astruct type astructName2 astruct @@ -267,6 +272,7 @@ func main() { m2 := map[int]*astruct{1: &astruct{10, 11}} m3 := map[astruct]int{{1, 1}: 42, {2, 2}: 43} m4 := map[astruct]astruct{{1, 1}: {11, 11}, {2, 2}: {22, 22}} + mlarge := map[largestruct]largestruct{largestruct{name: "one"}: largestruct{name: "oneval"}} upnil := unsafe.Pointer(nil) up1 := unsafe.Pointer(&i1) i4 := 800 @@ -427,5 +433,5 @@ func main() { longslice := make([]int, 100, 100) runtime.Breakpoint() - fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4) + fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge) } diff --git a/_scripts/rtype-out.txt b/_scripts/rtype-out.txt index 68d9da5e..a592eb39 100644 --- a/_scripts/rtype-out.txt +++ b/_scripts/rtype-out.txt @@ -63,6 +63,8 @@ const emptyOne = 1 const emptyRest = 0 +const internal/runtime/maps.ctrlEmpty = 128 + const kindDirectIface|internal/abi.KindDirectIface = 32 const kindGCProg|internal/abi.KindGCProg = 64 diff --git a/_scripts/rtype.go b/_scripts/rtype.go index b34653a5..04dc36d4 100644 --- a/_scripts/rtype.go +++ b/_scripts/rtype.go @@ -56,6 +56,14 @@ // - a type defined in the runtime package, without the 'runtime.' prefix // - anytype to match all possible types // - an expression of the form T1|T2 where both T1 and T2 can be arbitrary type expressions +// +// GO VERSION RESTRICTIONS +// +// A rtype comment of this form: +// +// // +rtype go1.24 ... +// +// Will only be applied to versions of Go following version 1.24.0 package main @@ -70,6 +78,7 @@ import ( "log" "os" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -78,6 +87,7 @@ import ( ) const magicCommentPrefix = "+rtype" +const gover = "go1." var fset = &token.FileSet{} var checkVarTypeRules = []*checkVarType{} @@ -97,6 +107,7 @@ type rtypeCmnt struct { type checkVarType struct { V, T string // V must have type T pos token.Pos + firstMinor } func (c *checkVarType) String() string { @@ -111,6 +122,7 @@ type checkFieldType struct { S, F, T string // S.F must have type T opt bool pos token.Pos + firstMinor } func (c *checkFieldType) String() string { @@ -122,6 +134,13 @@ type checkConstVal struct { C string // const C = V V constant.Value pos token.Pos + firstMinor +} + +type firstMinor int + +func (firstMinor firstMinor) versionOk(curminor int) bool { + return curminor >= int(firstMinor) } func (c *checkConstVal) String() string { @@ -255,31 +274,38 @@ func process(pkg *packages.Package, rtcmnt *rtypeCmnt, cmntmap ast.CommentMap, r tinfo := pkg.TypesInfo fields := strings.Split(rtcmnt.txt, " ") + firstMinor := 0 + + if strings.HasPrefix(fields[1], gover) { + firstMinor, _ = strconv.Atoi(fields[1][len(gover):]) + fields = fields[1:] + } + switch fields[1] { case "-var": // -var V T // requests that variable V is of type T - addCheckVarType(fields[2], fields[3], rtcmnt.slash) + addCheckVarType(fields[2], fields[3], rtcmnt.slash, firstMinor) case "-field": // -field S.F T // requests that field F of type S is of type T v := strings.Split(fields[2], ".") - addCheckFieldType(v[0], v[1], fields[3], false, rtcmnt.slash) + addCheckFieldType(v[0], v[1], fields[3], false, rtcmnt.slash, firstMinor) default: ok := false if ident := isProcVariableDecl(rtcmnt.stmt, tinfo); ident != nil { if len(fields) == 2 { - processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[1]) + processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[1], firstMinor) ok = true } else if len(fields) == 3 && fields[1] == "-opt" { - processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[2]) + processProcVariableUses(rtcmnt.toplevel, tinfo, ident, cmntmap, rtcmnts, fields[2], firstMinor) ok = true } } else if ident := isConstDecl(rtcmnt.toplevel, rtcmnt.node); len(fields) == 2 && ident != nil { - addCheckConstVal(fields[1], constValue(tinfo.Defs[ident]), rtcmnt.slash) + addCheckConstVal(fields[1], constValue(tinfo.Defs[ident]), rtcmnt.slash, firstMinor) ok = true } else if F := isStringCaseClause(rtcmnt.stmt); F != "" && len(fields) == 4 && fields[1] == "-fieldof" { - addCheckFieldType(fields[2], F, fields[3], false, rtcmnt.slash) + addCheckFieldType(fields[2], F, fields[3], false, rtcmnt.slash, firstMinor) ok = true } if !ok { @@ -359,7 +385,7 @@ func isStringCaseClause(stmt ast.Stmt) string { // processProcVariableUses scans the body of the function declaration 'decl' // looking for uses of 'procVarIdent' which is assumed to be an identifier // for a *proc.Variable variable. -func processProcVariableUses(decl ast.Node, tinfo *types.Info, procVarIdent *ast.Ident, cmntmap ast.CommentMap, rtcmnts []*rtypeCmnt, S string) { +func processProcVariableUses(decl ast.Node, tinfo *types.Info, procVarIdent *ast.Ident, cmntmap ast.CommentMap, rtcmnts []*rtypeCmnt, S string, firstMinor int) { if len(S) > 0 && S[0] == '*' { S = S[1:] } @@ -435,7 +461,7 @@ func processProcVariableUses(decl ast.Node, tinfo *types.Info, procVarIdent *ast } } F, _ := strconv.Unquote(arg0.Value) - addCheckFieldType(S, F, typ, opt, fncall.Pos()) + addCheckFieldType(S, F, typ, opt, fncall.Pos(), firstMinor) //printNode(fset, fncall) default: pos := fset.Position(n.Pos()) @@ -454,18 +480,18 @@ func findComment(slash token.Pos, rtcmnts []*rtypeCmnt) int { return -1 } -func addCheckVarType(V, T string, pos token.Pos) { - checkVarTypeRules = append(checkVarTypeRules, &checkVarType{V, T, pos}) +func addCheckVarType(V, T string, pos token.Pos, firstMinor_ int) { + checkVarTypeRules = append(checkVarTypeRules, &checkVarType{V, T, pos, firstMinor(firstMinor_)}) } -func addCheckFieldType(S, F, T string, opt bool, pos token.Pos) { +func addCheckFieldType(S, F, T string, opt bool, pos token.Pos, firstMinor_ int) { if !strings.Contains(S, "|") { - checkFieldTypeRules[S] = append(checkFieldTypeRules[S], &checkFieldType{S, F, T, opt, pos}) + checkFieldTypeRules[S] = append(checkFieldTypeRules[S], &checkFieldType{S, F, T, opt, pos, firstMinor(firstMinor_)}) } } -func addCheckConstVal(C string, V constant.Value, pos token.Pos) { - checkConstValRules[C] = append(checkConstValRules[C], &checkConstVal{C, V, pos}) +func addCheckConstVal(C string, V constant.Value, pos token.Pos, firstMinor_ int) { + checkConstValRules[C] = append(checkConstValRules[C], &checkConstVal{C, V, pos, firstMinor(firstMinor_)}) } // report writes a report of all rules derived from the proc package to stdout. @@ -548,7 +574,7 @@ func check() { pkgmap := map[string]*packages.Package{} allok := true - for _, rule := range checkVarTypeRules { + for _, rule := range versionOkFilter(checkVarTypeRules) { pos := fset.Position(rule.pos) def := lookupPackage(pkgmap, "runtime").Types.Scope().Lookup(rule.V) if def == nil { @@ -569,7 +595,10 @@ func check() { } sort.Strings(Ss) for _, S := range Ss { - rules := checkFieldTypeRules[S] + rules := versionOkFilter(checkFieldTypeRules[S]) + if len(rules) == 0 { + continue + } pos := fset.Position(rules[0].pos) def := lookupTypeDef(pkgmap, S) @@ -617,7 +646,10 @@ func check() { } sort.Strings(Cs) for _, C := range Cs { - rules := checkConstValRules[C] + rules := versionOkFilter(checkConstValRules[C]) + if len(rules) == 0 { + continue + } pos := fset.Position(rules[0].pos) def := findConst(pkgmap, C) if def == nil { @@ -716,3 +748,14 @@ func relative(s string) string { } return r } + +func versionOkFilter[T interface{ versionOk(int) bool }](rules []T) []T { + curminor, _ := strconv.Atoi(strings.Split(runtime.Version()[len(gover):], ".")[0]) + r := []T{} + for _, rule := range rules { + if rule.versionOk(curminor) { + rules = append(rules, rule) + } + } + return r +} diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index a2a953bb..50a506ed 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -467,6 +467,9 @@ func qf(*types.Package) string { } func TestTypecheckRPC(t *testing.T) { + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 24) { + t.Skip("disabled due to export format changes") + } fset := &token.FileSet{} cfg := &packages.Config{ Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedName | packages.NeedCompiledGoFiles | packages.NeedTypes, @@ -1349,6 +1352,9 @@ func TestVersion(t *testing.T) { } func TestStaticcheck(t *testing.T) { + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 24) { + t.Skip("disabled due to export format changes") + } _, err := exec.LookPath("staticcheck") if err != nil { t.Skip("staticcheck not installed") diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index da4679fe..77020475 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -1840,7 +1840,7 @@ func lenBuiltin(args []*Variable, nodeargs []ast.Expr) (*Variable, error) { } return newConstant(arg.Children[0].Value, arg.mem), nil case reflect.Map: - it := arg.mapIterator() + it := arg.mapIterator(0) if arg.Unreadable != nil { return nil, arg.Unreadable } @@ -2152,7 +2152,7 @@ func (scope *EvalScope) evalReslice(op *evalop.Reslice, stack *evalStack) { return } xev.mapSkip += int(low) - xev.mapIterator() // reads map length + xev.mapIterator(0) // reads map length if int64(xev.mapSkip) >= xev.Len { stack.err = errors.New("map index out of bounds") return @@ -2755,7 +2755,7 @@ func (v *Variable) sliceAccess(idx int) (*Variable, error) { } func (v *Variable) mapAccess(idx *Variable) (*Variable, error) { - it := v.mapIterator() + it := v.mapIterator(0) if it == nil { return nil, fmt.Errorf("can not access unreadable map: %v", v.Unreadable) } diff --git a/pkg/proc/mapiter.go b/pkg/proc/mapiter.go new file mode 100644 index 00000000..7d4141ee --- /dev/null +++ b/pkg/proc/mapiter.go @@ -0,0 +1,602 @@ +package proc + +import ( + "errors" + "fmt" + "reflect" + + "github.com/go-delve/delve/pkg/dwarf/godwarf" + "github.com/go-delve/delve/pkg/goversion" +) + +type mapIterator interface { + next() bool + key() *Variable + value() *Variable +} + +func (v *Variable) mapIterator(maxNumBuckets uint64) mapIterator { + mt := v.RealType.(*godwarf.MapType) + sv := v.clone() + sv.RealType = resolveTypedef(&(sv.RealType.(*godwarf.MapType).TypedefType)) + sv = sv.maybeDereference() + v.Base = sv.Addr + + maptype, ok := sv.RealType.(*godwarf.StructType) + if !ok { + v.Unreadable = errors.New("wrong real type for map") + return nil + } + + isptr := func(typ godwarf.Type) bool { + _, isptr := typ.(*godwarf.PtrType) + return isptr + } + + it := &mapIteratorClassic{v: v, bidx: 0, b: nil, idx: 0, maxNumBuckets: maxNumBuckets, keyTypeIsPtr: isptr(mt.KeyType), elemTypeIsPtr: isptr(mt.ElemType)} + itswiss := &mapIteratorSwiss{v: v, maxNumGroups: maxNumBuckets, keyTypeIsPtr: isptr(mt.KeyType), elemTypeIsPtr: isptr(mt.ElemType)} + + if sv.Addr == 0 { + it.numbuckets = 0 + return it + } + + v.mem = cacheMemory(v.mem, v.Base, int(v.RealType.Size())) + + for _, f := range maptype.Field { + var err error + field, _ := sv.toField(f) + switch f.Name { + // Classic map fields + case "count": // +rtype -fieldof hmap int + v.Len, err = field.asInt() + case "B": // +rtype -fieldof hmap uint8 + var b uint64 + b, err = field.asUint() + it.numbuckets = 1 << b + it.oldmask = (1 << (b - 1)) - 1 + case "buckets": // +rtype -fieldof hmap unsafe.Pointer + it.buckets = field.maybeDereference() + case "oldbuckets": // +rtype -fieldof hmap unsafe.Pointer + it.oldbuckets = field.maybeDereference() + + // Swisstable map fields + case "used": + var n uint64 + n, err = field.asUint() + v.Len = int64(n) + case "dirPtr": + itswiss.dirPtr = field + case "dirLen": + itswiss.dirLen, err = field.asInt() + } + if err != nil { + v.Unreadable = err + return nil + } + } + + if it.buckets == nil && itswiss.dirPtr != nil { + itswiss.loadTypes() + return itswiss + } + + if it.buckets == nil || it.oldbuckets == nil || it.buckets.Kind != reflect.Struct || it.oldbuckets.Kind != reflect.Struct { + v.Unreadable = errMapBucketsNotStruct + return nil + } + + it.hashTophashEmptyOne = hashTophashEmptyZero + it.hashMinTopHash = hashMinTopHashGo111 + if producer := v.bi.Producer(); producer != "" && goversion.ProducerAfterOrEqual(producer, 1, 12) { + it.hashTophashEmptyOne = hashTophashEmptyOne + it.hashMinTopHash = hashMinTopHashGo112 + } + + return it +} + +// Classic Maps /////////////////////////////////////////////////////////////// + +type mapIteratorClassic struct { + v *Variable + numbuckets uint64 + oldmask uint64 + buckets *Variable + oldbuckets *Variable + b *Variable + bidx uint64 + + keyTypeIsPtr, elemTypeIsPtr bool + + tophashes *Variable + keys *Variable + values *Variable + overflow *Variable + + maxNumBuckets uint64 // maximum number of buckets to scan + + idx int64 + + hashTophashEmptyOne uint64 // Go 1.12 and later has two sentinel tophash values for an empty cell, this is the second one (the first one hashTophashEmptyZero, the same as Go 1.11 and earlier) + hashMinTopHash uint64 // minimum value of tophash for a cell that isn't either evacuated or empty +} + +var errMapBucketContentsNotArray = errors.New("malformed map type: keys, values or tophash of a bucket is not an array") +var errMapBucketContentsInconsistentLen = errors.New("malformed map type: inconsistent array length in bucket") +var errMapBucketsNotStruct = errors.New("malformed map type: buckets, oldbuckets or overflow field not a struct") + +func (it *mapIteratorClassic) nextBucket() bool { + if it.overflow != nil && it.overflow.Addr > 0 { + it.b = it.overflow + } else { + it.b = nil + + if it.maxNumBuckets > 0 && it.bidx >= it.maxNumBuckets { + return false + } + + for it.bidx < it.numbuckets { + it.b = it.buckets.clone() + it.b.Addr += uint64(it.buckets.DwarfType.Size()) * it.bidx + + if it.oldbuckets.Addr <= 0 { + break + } + + // if oldbuckets is not nil we are iterating through a map that is in + // the middle of a grow. + // if the bucket we are looking at hasn't been filled in we iterate + // instead through its corresponding "oldbucket" (i.e. the bucket the + // elements of this bucket are coming from) but only if this is the first + // of the two buckets being created from the same oldbucket (otherwise we + // would print some keys twice) + + oldbidx := it.bidx & it.oldmask + oldb := it.oldbuckets.clone() + oldb.Addr += uint64(it.oldbuckets.DwarfType.Size()) * oldbidx + + if it.mapEvacuated(oldb) { + break + } + + if oldbidx == it.bidx { + it.b = oldb + break + } + + // oldbucket origin for current bucket has not been evacuated but we have already + // iterated over it so we should just skip it + it.b = nil + it.bidx++ + } + + if it.b == nil { + return false + } + it.bidx++ + } + + if it.b.Addr <= 0 { + return false + } + + it.b.mem = cacheMemory(it.b.mem, it.b.Addr, int(it.b.RealType.Size())) + + it.tophashes = nil + it.keys = nil + it.values = nil + it.overflow = nil + + for _, f := range it.b.DwarfType.(*godwarf.StructType).Field { + field, err := it.b.toField(f) + if err != nil { + it.v.Unreadable = err + return false + } + if field.Unreadable != nil { + it.v.Unreadable = field.Unreadable + return false + } + + switch f.Name { + case "tophash": // +rtype -fieldof bmap [8]uint8 + it.tophashes = field + case "keys": + it.keys = field + case "values": + it.values = field + case "overflow": + it.overflow = field.maybeDereference() + } + } + + // sanity checks + if it.tophashes == nil || it.keys == nil || it.values == nil { + it.v.Unreadable = errors.New("malformed map type") + return false + } + + if it.tophashes.Kind != reflect.Array || it.keys.Kind != reflect.Array || it.values.Kind != reflect.Array { + it.v.Unreadable = errMapBucketContentsNotArray + return false + } + + if it.tophashes.Len != it.keys.Len { + it.v.Unreadable = errMapBucketContentsInconsistentLen + return false + } + + if it.values.fieldType.Size() > 0 && it.tophashes.Len != it.values.Len { + // if the type of the value is zero-sized (i.e. struct{}) then the values + // array's length is zero. + it.v.Unreadable = errMapBucketContentsInconsistentLen + return false + } + + if it.overflow.Kind != reflect.Struct { + it.v.Unreadable = errMapBucketsNotStruct + return false + } + + return true +} + +func (it *mapIteratorClassic) next() bool { + for { + if it.b == nil || it.idx >= it.tophashes.Len { + r := it.nextBucket() + if !r { + return false + } + it.idx = 0 + } + tophash, _ := it.tophashes.sliceAccess(int(it.idx)) + h, err := tophash.asUint() + if err != nil { + it.v.Unreadable = fmt.Errorf("unreadable tophash: %v", err) + return false + } + it.idx++ + if h != hashTophashEmptyZero && h != it.hashTophashEmptyOne { + return true + } + } +} + +func (it *mapIteratorClassic) key() *Variable { + k, _ := it.keys.sliceAccess(int(it.idx - 1)) + if k.Kind == reflect.Ptr && !it.keyTypeIsPtr { + k = k.maybeDereference() + } + return k +} + +func (it *mapIteratorClassic) value() *Variable { + if it.values.fieldType.Size() <= 0 { + return it.v.newVariable("", it.values.Addr, it.values.fieldType, DereferenceMemory(it.v.mem)) + } + + v, _ := it.values.sliceAccess(int(it.idx - 1)) + if v.Kind == reflect.Ptr && !it.elemTypeIsPtr { + v = v.maybeDereference() + } + + return v +} + +func (it *mapIteratorClassic) mapEvacuated(b *Variable) bool { + if b.Addr == 0 { + return true + } + for _, f := range b.DwarfType.(*godwarf.StructType).Field { + if f.Name != "tophash" { + continue + } + tophashes, _ := b.toField(f) + tophash0var, _ := tophashes.sliceAccess(0) + tophash0, err := tophash0var.asUint() + if err != nil { + return true + } + //TODO: this needs to be > hashTophashEmptyOne for go >= 1.12 + return tophash0 > it.hashTophashEmptyOne && tophash0 < it.hashMinTopHash + } + return true +} + +// Swisstable Maps /////////////////////////////////////////////////////////////// + +const ( + swissTableCtrlEmpty = 0b10000000 // +rtype go1.24 internal/runtime/maps.ctrlEmpty +) + +type mapIteratorSwiss struct { + v *Variable + dirPtr *Variable + dirLen int64 + maxNumGroups uint64 // Maximum number of groups we will visit + + keyTypeIsPtr, elemTypeIsPtr bool + tableType, groupType *godwarf.StructType + + tableFieldIndex, tableFieldGroups, groupsFieldLengthMask, groupsFieldData, groupFieldCtrl, groupFieldSlots, slotFieldKey, slotFieldElem *godwarf.StructField + + dirIdx int64 + tab *swissTable + + groupIdx uint64 + group *swissGroup + + slotIdx uint32 + + groupCount uint64 // Total count of visited groups except for current table + + curKey, curValue *Variable +} + +type swissTable struct { + index int64 + groups *Variable +} + +type swissGroup struct { + slots *Variable + ctrls []byte +} + +var errSwissTableCouldNotLoad = errors.New("could not load one of the tables") +var errSwissMapBadType = errors.New("swiss table type does not have some required fields") +var errSwissMapBadTableField = errors.New("swiss table bad table field") +var errSwissMapBadGroupTypeErr = errors.New("bad swiss map type, group type lacks some required fields") + +// loadTypes determines the correct type for it.dirPtr: the linker records +// this type as **table but in reality it is either *[dirLen]*table for +// large maps or *group for small maps, when it.dirLen == 0. +func (it *mapIteratorSwiss) loadTypes() { + tableptrptrtyp, ok := it.dirPtr.DwarfType.(*godwarf.PtrType) + if !ok { + it.v.Unreadable = errSwissMapBadTableField + return + } + tableptrtyp, ok := tableptrptrtyp.Type.(*godwarf.PtrType) + if !ok { + it.v.Unreadable = errSwissMapBadTableField + return + } + it.tableType, ok = tableptrtyp.Type.(*godwarf.StructType) + if !ok { + it.v.Unreadable = errSwissMapBadTableField + return + } + for _, field := range it.tableType.Field { + switch field.Name { + case "index": + it.tableFieldIndex = field + case "groups": + it.tableFieldGroups = field + groupstyp, ok := field.Type.(*godwarf.StructType) + if ok { + for _, field := range groupstyp.Field { + switch field.Name { + case "data": + it.groupsFieldData = field + typ, ok := field.Type.(*godwarf.PtrType) + if ok { + it.groupType, _ = resolveTypedef(typ.Type).(*godwarf.StructType) + } + case "lengthMask": + it.groupsFieldLengthMask = field + } + } + } + } + } + if it.groupType == nil || it.tableFieldIndex == nil || it.tableFieldGroups == nil || it.groupsFieldLengthMask == nil { + it.v.Unreadable = errSwissMapBadType + return + } + for _, field := range it.groupType.Field { + switch field.Name { + case "ctrl": + it.groupFieldCtrl = field + case "slots": + it.groupFieldSlots = field + } + } + if it.groupFieldCtrl == nil || it.groupFieldSlots == nil { + it.v.Unreadable = errSwissMapBadGroupTypeErr + return + } + + slotsType, ok := resolveTypedef(it.groupFieldSlots.Type).(*godwarf.ArrayType) + if !ok { + it.v.Unreadable = errSwissMapBadGroupTypeErr + return + } + slotType, ok := slotsType.Type.(*godwarf.StructType) + if !ok { + it.v.Unreadable = errSwissMapBadGroupTypeErr + return + } + for _, field := range slotType.Field { + switch field.Name { + case "key": + it.slotFieldKey = field + case "elem": + it.slotFieldElem = field + } + } + if it.slotFieldKey == nil || it.slotFieldElem == nil { + it.v.Unreadable = errSwissMapBadGroupTypeErr + return + } + + if it.dirLen <= 0 { + // small maps, convert it.dirPtr to be of type *group, then dereference it + it.dirPtr.DwarfType = pointerTo(fakeArrayType(1, it.groupType), it.v.bi.Arch) + it.dirPtr.RealType = it.dirPtr.DwarfType + it.dirPtr = it.dirPtr.maybeDereference() + it.dirLen = 1 + it.tab = &swissTable{groups: it.dirPtr} // so that we don't try to load this later on + return + } + + // normal map, convert it.dirPtr to be of type *[dirLen]*table, then dereference it + + it.dirPtr.DwarfType = pointerTo(fakeArrayType(uint64(it.dirLen), tableptrtyp), it.v.bi.Arch) + it.dirPtr.RealType = it.dirPtr.DwarfType + it.dirPtr = it.dirPtr.maybeDereference() +} + +// derived from $GOROOT/src/internal/runtime/maps/table.go and $GOROOT/src/runtime/runtime-gdb.py +func (it *mapIteratorSwiss) next() bool { + if it.v.Unreadable != nil { + return false + } + for it.dirIdx < it.dirLen { + if it.tab == nil { + it.loadCurrentTable() + if it.tab == nil { + return false + } + if it.tab.index != it.dirIdx { + it.nextTable() + continue + } + } + + for ; it.groupIdx < uint64(it.tab.groups.Len); it.nextGroup() { + if it.maxNumGroups > 0 && it.groupIdx+it.groupCount >= it.maxNumGroups { + return false + } + if it.group == nil { + it.loadCurrentGroup() + if it.group == nil { + return false + } + } + + for ; it.slotIdx < uint32(it.group.slots.Len); it.slotIdx++ { + if it.slotIsEmptyOrDeleted(it.slotIdx) { + continue + } + + cur, err := it.group.slots.sliceAccess(int(it.slotIdx)) + if err != nil { + it.v.Unreadable = fmt.Errorf("error accessing swiss map in table %d, group %d, slot %d", it.dirIdx, it.groupIdx, it.slotIdx) + return false + } + + var err1, err2 error + it.curKey, err1 = cur.toField(it.slotFieldKey) + it.curValue, err2 = cur.toField(it.slotFieldElem) + if err1 != nil || err2 != nil { + it.v.Unreadable = fmt.Errorf("error accessing swiss map slot: %v %v", err1, err2) + return false + } + + // If the type we expect is non-pointer but we read a pointer type it + // means that the key (or the value) is stored indirectly into the map + // because it is too big. We dereference it here so that the type of the + // key (or value) matches the type on the map definition. + if it.curKey.Kind == reflect.Ptr && !it.keyTypeIsPtr { + it.curKey = it.curKey.maybeDereference() + } + if it.curValue.Kind == reflect.Ptr && !it.elemTypeIsPtr { + it.curValue = it.curValue.maybeDereference() + } + + it.slotIdx++ + return true + } + + it.slotIdx = 0 + } + + it.groupCount += it.groupIdx + it.groupIdx = 0 + it.group = nil + it.nextTable() + } + return false +} + +func (it *mapIteratorSwiss) nextTable() { + it.dirIdx++ + it.tab = nil +} + +func (it *mapIteratorSwiss) nextGroup() { + it.groupIdx++ + it.group = nil +} + +// loadCurrentTable loads the table at index it.dirIdx into it.tab +func (it *mapIteratorSwiss) loadCurrentTable() { + tab, err := it.dirPtr.sliceAccess(int(it.dirIdx)) + if err != nil || tab == nil || tab.Unreadable != nil { + it.v.Unreadable = errSwissTableCouldNotLoad + return + } + + tab = tab.maybeDereference() + + r := &swissTable{} + + field, _ := tab.toField(it.tableFieldIndex) + r.index, err = field.asInt() + if err != nil { + it.v.Unreadable = fmt.Errorf("could not load swiss table index: %v", err) + return + } + + groups, _ := tab.toField(it.tableFieldGroups) + r.groups, _ = groups.toField(it.groupsFieldData) + + field, _ = groups.toField(it.groupsFieldLengthMask) + groupsLengthMask, err := field.asUint() + if err != nil { + it.v.Unreadable = fmt.Errorf("could not load swiss table group lengthMask: %v", err) + return + } + + // convert the type of groups from *group to *[len]group so that it's easier to use + r.groups.DwarfType = pointerTo(fakeArrayType(groupsLengthMask+1, it.groupType), it.v.bi.Arch) + r.groups.RealType = r.groups.DwarfType + r.groups = r.groups.maybeDereference() + + it.tab = r +} + +// loadCurrentGroup loads the group at index it.groupIdx of it.tab into it.group +func (it *mapIteratorSwiss) loadCurrentGroup() { + group, err := it.tab.groups.sliceAccess(int(it.groupIdx)) + if err != nil { + it.v.Unreadable = fmt.Errorf("could not load swiss map group: %v", err) + return + } + g := &swissGroup{} + g.slots, _ = group.toField(it.groupFieldSlots) + ctrl, _ := group.toField(it.groupFieldCtrl) + g.ctrls = make([]byte, ctrl.DwarfType.Size()) + _, err = ctrl.mem.ReadMemory(g.ctrls, ctrl.Addr) + if err != nil { + it.v.Unreadable = err + return + } + it.group = g +} + +func (it *mapIteratorSwiss) key() *Variable { + return it.curKey +} + +func (it *mapIteratorSwiss) value() *Variable { + return it.curValue +} + +func (it *mapIteratorSwiss) slotIsEmptyOrDeleted(k uint32) bool { + //TODO: check that this hasn't changed after it's merged and the TODO is deleted + return it.group.ctrls[k]&swissTableCtrlEmpty == swissTableCtrlEmpty +} diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index b9923cb0..bab10d5a 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -1365,7 +1365,7 @@ func (v *Variable) loadValueInternal(recurseLevel int, cfg LoadConfig) { v.loadMap(recurseLevel, cfg) } else { // loads length so that the client knows that the map isn't empty - v.mapIterator() + v.mapIterator(0) } case reflect.String: @@ -1992,11 +1992,10 @@ func (v *Variable) funcvalAddr() uint64 { } func (v *Variable) loadMap(recurseLevel int, cfg LoadConfig) { - it := v.mapIterator() + it := v.mapIterator(uint64(cfg.MaxMapBuckets)) if it == nil { return } - it.maxNumBuckets = uint64(cfg.MaxMapBuckets) if v.Len == 0 || int64(v.mapSkip) >= v.Len || cfg.MaxArrayValues == 0 { return @@ -2013,12 +2012,7 @@ func (v *Variable) loadMap(recurseLevel int, cfg LoadConfig) { errcount := 0 for it.next() { key := it.key() - var val *Variable - if it.values.fieldType.Size() > 0 { - val = it.value() - } else { - val = v.newVariable("", it.values.Addr, it.values.fieldType, DereferenceMemory(v.mem)) - } + val := it.value() key.loadValueInternal(recurseLevel+1, cfg) val.loadValueInternal(recurseLevel+1, cfg) if key.Unreadable != nil || val.Unreadable != nil { @@ -2035,259 +2029,6 @@ func (v *Variable) loadMap(recurseLevel int, cfg LoadConfig) { } } -type mapIterator struct { - v *Variable - numbuckets uint64 - oldmask uint64 - buckets *Variable - oldbuckets *Variable - b *Variable - bidx uint64 - - tophashes *Variable - keys *Variable - values *Variable - overflow *Variable - - maxNumBuckets uint64 // maximum number of buckets to scan - - idx int64 - - hashTophashEmptyOne uint64 // Go 1.12 and later has two sentinel tophash values for an empty cell, this is the second one (the first one hashTophashEmptyZero, the same as Go 1.11 and earlier) - hashMinTopHash uint64 // minimum value of tophash for a cell that isn't either evacuated or empty -} - -// Code derived from go/src/runtime/hashmap.go -func (v *Variable) mapIterator() *mapIterator { - sv := v.clone() - sv.RealType = resolveTypedef(&(sv.RealType.(*godwarf.MapType).TypedefType)) - sv = sv.maybeDereference() - v.Base = sv.Addr - - maptype, ok := sv.RealType.(*godwarf.StructType) - if !ok { - v.Unreadable = errors.New("wrong real type for map") - return nil - } - - it := &mapIterator{v: v, bidx: 0, b: nil, idx: 0} - - if sv.Addr == 0 { - it.numbuckets = 0 - return it - } - - v.mem = cacheMemory(v.mem, v.Base, int(v.RealType.Size())) - - for _, f := range maptype.Field { - var err error - field, _ := sv.toField(f) - switch f.Name { - case "count": // +rtype -fieldof hmap int - v.Len, err = field.asInt() - case "B": // +rtype -fieldof hmap uint8 - var b uint64 - b, err = field.asUint() - it.numbuckets = 1 << b - it.oldmask = (1 << (b - 1)) - 1 - case "buckets": // +rtype -fieldof hmap unsafe.Pointer - it.buckets = field.maybeDereference() - case "oldbuckets": // +rtype -fieldof hmap unsafe.Pointer - it.oldbuckets = field.maybeDereference() - } - if err != nil { - v.Unreadable = err - return nil - } - } - - if it.buckets.Kind != reflect.Struct || it.oldbuckets.Kind != reflect.Struct { - v.Unreadable = errMapBucketsNotStruct - return nil - } - - it.hashTophashEmptyOne = hashTophashEmptyZero - it.hashMinTopHash = hashMinTopHashGo111 - if producer := v.bi.Producer(); producer != "" && goversion.ProducerAfterOrEqual(producer, 1, 12) { - it.hashTophashEmptyOne = hashTophashEmptyOne - it.hashMinTopHash = hashMinTopHashGo112 - } - - return it -} - -var errMapBucketContentsNotArray = errors.New("malformed map type: keys, values or tophash of a bucket is not an array") -var errMapBucketContentsInconsistentLen = errors.New("malformed map type: inconsistent array length in bucket") -var errMapBucketsNotStruct = errors.New("malformed map type: buckets, oldbuckets or overflow field not a struct") - -func (it *mapIterator) nextBucket() bool { - if it.overflow != nil && it.overflow.Addr > 0 { - it.b = it.overflow - } else { - it.b = nil - - if it.maxNumBuckets > 0 && it.bidx >= it.maxNumBuckets { - return false - } - - for it.bidx < it.numbuckets { - it.b = it.buckets.clone() - it.b.Addr += uint64(it.buckets.DwarfType.Size()) * it.bidx - - if it.oldbuckets.Addr <= 0 { - break - } - - // if oldbuckets is not nil we are iterating through a map that is in - // the middle of a grow. - // if the bucket we are looking at hasn't been filled in we iterate - // instead through its corresponding "oldbucket" (i.e. the bucket the - // elements of this bucket are coming from) but only if this is the first - // of the two buckets being created from the same oldbucket (otherwise we - // would print some keys twice) - - oldbidx := it.bidx & it.oldmask - oldb := it.oldbuckets.clone() - oldb.Addr += uint64(it.oldbuckets.DwarfType.Size()) * oldbidx - - if it.mapEvacuated(oldb) { - break - } - - if oldbidx == it.bidx { - it.b = oldb - break - } - - // oldbucket origin for current bucket has not been evacuated but we have already - // iterated over it so we should just skip it - it.b = nil - it.bidx++ - } - - if it.b == nil { - return false - } - it.bidx++ - } - - if it.b.Addr <= 0 { - return false - } - - it.b.mem = cacheMemory(it.b.mem, it.b.Addr, int(it.b.RealType.Size())) - - it.tophashes = nil - it.keys = nil - it.values = nil - it.overflow = nil - - for _, f := range it.b.DwarfType.(*godwarf.StructType).Field { - field, err := it.b.toField(f) - if err != nil { - it.v.Unreadable = err - return false - } - if field.Unreadable != nil { - it.v.Unreadable = field.Unreadable - return false - } - - switch f.Name { - case "tophash": // +rtype -fieldof bmap [8]uint8 - it.tophashes = field - case "keys": - it.keys = field - case "values": - it.values = field - case "overflow": - it.overflow = field.maybeDereference() - } - } - - // sanity checks - if it.tophashes == nil || it.keys == nil || it.values == nil { - it.v.Unreadable = errors.New("malformed map type") - return false - } - - if it.tophashes.Kind != reflect.Array || it.keys.Kind != reflect.Array || it.values.Kind != reflect.Array { - it.v.Unreadable = errMapBucketContentsNotArray - return false - } - - if it.tophashes.Len != it.keys.Len { - it.v.Unreadable = errMapBucketContentsInconsistentLen - return false - } - - if it.values.fieldType.Size() > 0 && it.tophashes.Len != it.values.Len { - // if the type of the value is zero-sized (i.e. struct{}) then the values - // array's length is zero. - it.v.Unreadable = errMapBucketContentsInconsistentLen - return false - } - - if it.overflow.Kind != reflect.Struct { - it.v.Unreadable = errMapBucketsNotStruct - return false - } - - return true -} - -func (it *mapIterator) next() bool { - for { - if it.b == nil || it.idx >= it.tophashes.Len { - r := it.nextBucket() - if !r { - return false - } - it.idx = 0 - } - tophash, _ := it.tophashes.sliceAccess(int(it.idx)) - h, err := tophash.asUint() - if err != nil { - it.v.Unreadable = fmt.Errorf("unreadable tophash: %v", err) - return false - } - it.idx++ - if h != hashTophashEmptyZero && h != it.hashTophashEmptyOne { - return true - } - } -} - -func (it *mapIterator) key() *Variable { - k, _ := it.keys.sliceAccess(int(it.idx - 1)) - return k -} - -func (it *mapIterator) value() *Variable { - v, _ := it.values.sliceAccess(int(it.idx - 1)) - return v -} - -func (it *mapIterator) mapEvacuated(b *Variable) bool { - if b.Addr == 0 { - return true - } - for _, f := range b.DwarfType.(*godwarf.StructType).Field { - if f.Name != "tophash" { - continue - } - tophashes, _ := b.toField(f) - tophash0var, _ := tophashes.sliceAccess(0) - tophash0, err := tophash0var.asUint() - if err != nil { - return true - } - //TODO: this needs to be > hashTophashEmptyOne for go >= 1.12 - return tophash0 > it.hashTophashEmptyOne && tophash0 < it.hashMinTopHash - } - return true -} - func (v *Variable) readInterface() (_type, data *Variable, isnil bool) { // An interface variable is implemented either by a runtime.iface // struct or a runtime.eface struct. The difference being that empty diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go index 2e2ad1c2..8361afd6 100644 --- a/pkg/proc/variables_test.go +++ b/pkg/proc/variables_test.go @@ -683,6 +683,7 @@ func getEvalExpressionTestCases() []varTest { {"m3[as1]", false, "42", "42", "int", nil}, {"mnil[\"Malone\"]", false, "", "", "", errors.New("key not found")}, {"m1[80:]", false, "", "", "", errors.New("map index out of bounds")}, + {"mlarge", false, "map[main.largestruct]main.largestruct [{name: \"one\", v: [256]uint8 [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,...+192 more]}: {name: \"oneval\", v: [256]uint8 [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,...+192 more]}, ]", "map[main.largestruct]main.largestruct [...]", "map[main.largestruct]main.largestruct", nil}, // interfaces {"err1", true, "error(*main.astruct) *{A: 1, B: 2}", "error(*main.astruct) 0x…", "error", nil}, @@ -957,21 +958,15 @@ func getEvalExpressionTestCases() []varTest { {`*(*uint)(unsafe.Pointer(p1))`, false, `1`, `1`, "uint", nil}, {`*(*uint)(unsafe.Pointer(&i1))`, false, `1`, `1`, "uint", nil}, - // Conversions to ptr-to-ptr types - {`**(**runtime.hmap)(uintptr(&m1))`, false, `…`, `…`, "runtime.hmap", nil}, - // Malformed values {`badslice`, false, `(unreadable non-zero length array with nil base)`, `(unreadable non-zero length array with nil base)`, "[]int", nil}, } - ver, _ := goversion.Parse(runtime.Version()) - if ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 7, Rev: -1}) { - for i := range testcases { - if testcases[i].name == "iface3" { - testcases[i].value = "interface {}(*map[string]go/constant.Value) *[]" - testcases[i].alternate = "interface {}(*map[string]go/constant.Value) 0x…" - } - } + // Conversions to ptr-to-ptr types + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 24) { + testcases = append(testcases, varTest{`**(**maps.Map)(uintptr(&m1))`, false, `…`, `…`, "internal/runtime/maps.Map", nil}) + } else { + testcases = append(testcases, varTest{`**(**runtime.hmap)(uintptr(&m1))`, false, `…`, `…`, "runtime.hmap", nil}) } return testcases @@ -1204,18 +1199,11 @@ func TestPackageRenames(t *testing.T) { {`"dir0/pkg".A`, false, "0", "", "int", nil}, {`"dir1/pkg".A`, false, "1", "", "int", nil}, - } - testcases_i386 := []varTest{ {"amap", true, "interface {}(map[go/ast.BadExpr]net/http.Request) [{From: 2, To: 3}: {Method: \"othermethod\", …", "", "interface {}", nil}, {"amap2", true, "interface {}(*map[go/ast.BadExpr]net/http.Request) *[{From: 2, To: 3}: {Method: \"othermethod\", …", "", "interface {}", nil}, } - testcases_64bit := []varTest{ - {"amap", true, "interface {}(map[go/ast.BadExpr]net/http.Request) [{From: 2, To: 3}: *{Method: \"othermethod\", …", "", "interface {}", nil}, - {"amap2", true, "interface {}(*map[go/ast.BadExpr]net/http.Request) *[{From: 2, To: 3}: *{Method: \"othermethod\", …", "", "interface {}", nil}, - } - testcases1_9 := []varTest{ {"astruct2", true, `interface {}(*struct { github.com/go-delve/delve/_fixtures/internal/dir1/pkg.SomeType; X int }) *{SomeType: github.com/go-delve/delve/_fixtures/internal/dir1/pkg.SomeType {X: 1, Y: 2}, X: 10}`, "", "interface {}", nil}, } @@ -1239,12 +1227,6 @@ func TestPackageRenames(t *testing.T) { if goversion.VersionAfterOrEqual(runtime.Version(), 1, 13) { testPackageRenamesHelper(t, p, testcases1_13) } - - if runtime.GOARCH == "386" && !goversion.VersionAfterOrEqual(runtime.Version(), 1, 22) { - testPackageRenamesHelper(t, p, testcases_i386) - } else { - testPackageRenamesHelper(t, p, testcases_64bit) - } }) } @@ -1908,3 +1890,74 @@ func TestSetupRangeFramesCrash(t *testing.T) { }) } } + +func TestClassicMap(t *testing.T) { + // This test replicates some of the tests in TestEvalExpression to check + // that we still support non-swiss maps on versions of Go where the default + // map backend is swisstables. + protest.AllowRecording(t) + + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 24) { + t.Skip("N/A") + } + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 27) { + panic("test expired, please remove") + } + t.Setenv("GOEXPERIMENT", "noswissmap") + + testcases := []varTest{ + {"m1[\"Malone\"]", false, "main.astruct {A: 2, B: 3}", "main.astruct {A: 2, B: 3}", "main.astruct", nil}, + {"m2[1].B", false, "11", "11", "int", nil}, + {"m2[c1.sa[2].B-4].A", false, "10", "10", "int", nil}, + {"m2[*p1].B", false, "11", "11", "int", nil}, + {"m3[as1]", false, "42", "42", "int", nil}, + {"mlarge", false, "map[main.largestruct]main.largestruct [{name: \"one\", v: [256]uint8 [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,...+192 more]}: {name: \"oneval\", v: [256]uint8 [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,...+192 more]}, ]", "map[main.largestruct]main.largestruct [...]", "map[main.largestruct]main.largestruct", nil}, + {"mnil[\"Malone\"]", false, "", "", "", errors.New("key not found")}, + {"m1[80:]", false, "", "", "", errors.New("map index out of bounds")}, + {"mnil", true, "map[string]main.astruct nil", "map[string]main.astruct nil", "map[string]main.astruct", nil}, + {"m1 == nil", false, "false", "false", "", nil}, + {"mnil == m1", false, "", "", "", errors.New("can not compare map variables")}, + {"mnil == nil", false, "true", "true", "", nil}, + {"m2", true, "map[int]*main.astruct [1: *{A: 10, B: 11}, ]", "map[int]*main.astruct [...]", "map[int]*main.astruct", nil}, + } + + withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + assertNoError(grp.Continue(), t, "Continue() returned an error") + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + t.Logf("%q", tc.name) + variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig) + if tc.err == nil { + assertNoError(err, t, fmt.Sprintf("EvalExpression(%s) returned an error", tc.name)) + assertVariable(t, variable, tc) + variable, err := evalVariableWithCfg(p, tc.name, pshortLoadConfig) + assertNoError(err, t, fmt.Sprintf("EvalExpression(%s, pshortLoadConfig) returned an error", tc.name)) + assertVariable(t, variable, tc.alternateVarTest()) + } else { + + if err == nil { + t.Fatalf("Expected error %s, got no error (%s)", tc.err.Error(), tc.name) + } + switch e := tc.err.(type) { + case *altError: + ok := false + for _, tgtErr := range e.errs { + if tgtErr == err.Error() { + ok = true + break + } + } + if !ok { + t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + } + default: + if tc.err.Error() != "*" && tc.err.Error() != err.Error() { + t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + } + } + + } + }) + } + }) +}