github.com/undoio/delve@v1.9.0/pkg/proc/scope_test.go (about)

     1  package proc_test
     2  
     3  import (
     4  	"fmt"
     5  	"go/constant"
     6  	"go/parser"
     7  	"go/token"
     8  	"math"
     9  	"path/filepath"
    10  	"reflect"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/undoio/delve/pkg/goversion"
    17  	"github.com/undoio/delve/pkg/proc"
    18  	protest "github.com/undoio/delve/pkg/proc/test"
    19  )
    20  
    21  func TestScopeWithEscapedVariable(t *testing.T) {
    22  	if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1, Beta: 3}) {
    23  		return
    24  	}
    25  
    26  	protest.AllowRecording(t)
    27  	withTestProcess("scopeescapevareval", t, func(p *proc.Target, fixture protest.Fixture) {
    28  		assertNoError(p.Continue(), t, "Continue")
    29  
    30  		// On the breakpoint there are two 'a' variables in scope, the one that
    31  		// isn't shadowed is a variable that escapes to the heap and figures in
    32  		// debug_info as '&a'. Evaluating 'a' should yield the escaped variable.
    33  
    34  		avar := evalVariable(p, t, "a")
    35  		if aval, _ := constant.Int64Val(avar.Value); aval != 3 {
    36  			t.Errorf("wrong value for variable a: %d", aval)
    37  		}
    38  
    39  		if avar.Flags&proc.VariableEscaped == 0 {
    40  			t.Errorf("variable a isn't escaped to the heap")
    41  		}
    42  	})
    43  }
    44  
    45  // TestScope will:
    46  //   - run _fixtures/scopetest.go
    47  //   - set a breakpoint on all lines containing a comment
    48  //   - continue until the program ends
    49  //   - every time a breakpoint is hit it will check that
    50  //     scope.FunctionArguments+scope.LocalVariables and scope.EvalExpression
    51  //     return what the corresponding comment describes they should return and
    52  //     removes the breakpoint.
    53  //
    54  // Each comment is a comma separated list of variable declarations, with
    55  // each variable declaration having the following format:
    56  //
    57  //	name type = initialvalue
    58  //
    59  // the = and the initial value are optional and can only be specified if the
    60  // type is an integer type, float32, float64 or bool.
    61  //
    62  // If multiple variables with the same name are specified:
    63  //  1. LocalVariables+FunctionArguments should return them in the same order and
    64  //     every variable except the last one should be marked as shadowed
    65  //  2. EvalExpression should return the last one.
    66  func TestScope(t *testing.T) {
    67  	if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) {
    68  		return
    69  	}
    70  
    71  	fixturesDir := protest.FindFixturesDir()
    72  	scopetestPath := filepath.Join(fixturesDir, "scopetest.go")
    73  
    74  	scopeChecks := getScopeChecks(scopetestPath, t)
    75  
    76  	protest.AllowRecording(t)
    77  	withTestProcess("scopetest", t, func(p *proc.Target, fixture protest.Fixture) {
    78  		for i := range scopeChecks {
    79  			setFileBreakpoint(p, t, fixture.Source, scopeChecks[i].line)
    80  		}
    81  
    82  		t.Logf("%d breakpoints set", len(scopeChecks))
    83  
    84  		for {
    85  			if err := p.Continue(); err != nil {
    86  				if _, exited := err.(proc.ErrProcessExited); exited {
    87  					break
    88  				}
    89  				assertNoError(err, t, "Continue()")
    90  			}
    91  			bp := p.CurrentThread().Breakpoint()
    92  
    93  			scopeCheck := findScopeCheck(scopeChecks, bp.Line)
    94  			if scopeCheck == nil {
    95  				t.Errorf("unknown stop position %s:%d %#x", bp.File, bp.Line, bp.Addr)
    96  			}
    97  
    98  			scope, _ := scopeCheck.checkLocalsAndArgs(p, t)
    99  
   100  			for i := range scopeCheck.varChecks {
   101  				vc := &scopeCheck.varChecks[i]
   102  				if vc.shdw {
   103  					continue
   104  				}
   105  				vc.checkInScope(scopeCheck.line, scope, t)
   106  			}
   107  
   108  			scopeCheck.ok = true
   109  			err := p.ClearBreakpoint(bp.Addr)
   110  			assertNoError(err, t, "ClearBreakpoint")
   111  		}
   112  	})
   113  
   114  	for i := range scopeChecks {
   115  		if !scopeChecks[i].ok {
   116  			t.Errorf("breakpoint at line %d not hit", scopeChecks[i].line)
   117  		}
   118  	}
   119  
   120  }
   121  
   122  type scopeCheck struct {
   123  	line      int
   124  	varChecks []varCheck
   125  	ok        bool // this scope check was passed
   126  }
   127  
   128  type varCheck struct {
   129  	name     string
   130  	typ      string
   131  	kind     reflect.Kind
   132  	shdw     bool // this variable should be shadowed
   133  	hasVal   bool
   134  	intVal   int64
   135  	uintVal  uint64
   136  	floatVal float64
   137  	boolVal  bool
   138  
   139  	ok bool // this variable check was passed
   140  }
   141  
   142  func getScopeChecks(path string, t *testing.T) []scopeCheck {
   143  	var fset token.FileSet
   144  	root, err := parser.ParseFile(&fset, path, nil, parser.ParseComments)
   145  	if err != nil {
   146  		t.Fatalf("could not parse %s: %v", path, err)
   147  	}
   148  
   149  	scopeChecks := []scopeCheck{}
   150  
   151  	for _, cmtg := range root.Comments {
   152  		for _, cmt := range cmtg.List {
   153  			pos := fset.Position(cmt.Slash)
   154  
   155  			scopeChecks = append(scopeChecks, scopeCheck{line: pos.Line})
   156  			scopeChecks[len(scopeChecks)-1].Parse(cmt.Text[2:], t)
   157  		}
   158  	}
   159  
   160  	return scopeChecks
   161  }
   162  
   163  func findScopeCheck(scopeChecks []scopeCheck, line int) *scopeCheck {
   164  	for i := range scopeChecks {
   165  		if scopeChecks[i].line == line {
   166  			return &scopeChecks[i]
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  func (check *scopeCheck) Parse(descr string, t *testing.T) {
   173  	decls := strings.Split(descr, ",")
   174  	check.varChecks = make([]varCheck, len(decls))
   175  	for i, decl := range decls {
   176  		varcheck := &check.varChecks[i]
   177  		value := ""
   178  		if equal := strings.Index(decl, "="); equal >= 0 {
   179  			value = strings.TrimSpace(decl[equal+1:])
   180  			decl = strings.TrimSpace(decl[:equal])
   181  			varcheck.hasVal = true
   182  		} else {
   183  			decl = strings.TrimSpace(decl)
   184  		}
   185  
   186  		space := strings.Index(decl, " ")
   187  		if space < 0 {
   188  			t.Fatalf("could not parse scope comment %q (%q)", descr, decl)
   189  		}
   190  		varcheck.name = strings.TrimSpace(decl[:space])
   191  		varcheck.typ = strings.TrimSpace(decl[space+1:])
   192  		if strings.Index(varcheck.typ, " ") >= 0 {
   193  			t.Fatalf("could not parse scope comment %q (%q)", descr, decl)
   194  		}
   195  
   196  		if !varcheck.hasVal {
   197  			continue
   198  		}
   199  
   200  		switch varcheck.typ {
   201  		case "int", "int8", "int16", "int32", "int64":
   202  			var err error
   203  			varcheck.kind = reflect.Int
   204  			varcheck.intVal, err = strconv.ParseInt(value, 10, 64)
   205  			if err != nil {
   206  				t.Fatalf("could not parse scope comment %q: %v", descr, err)
   207  			}
   208  
   209  		case "uint", "uint8", "uint16", "uint32", "uint64", "uintptr":
   210  			var err error
   211  			varcheck.kind = reflect.Uint
   212  			varcheck.uintVal, err = strconv.ParseUint(value, 10, 64)
   213  			if err != nil {
   214  				t.Fatalf("could not parse scope comment %q: %v", descr, err)
   215  			}
   216  
   217  		case "float32", "float64":
   218  			var err error
   219  			varcheck.kind = reflect.Float64
   220  			varcheck.floatVal, err = strconv.ParseFloat(value, 64)
   221  			if err != nil {
   222  				t.Fatalf("could not parse scope comment %q: %v", descr, err)
   223  			}
   224  
   225  		case "bool":
   226  			var err error
   227  			varcheck.kind = reflect.Bool
   228  			varcheck.boolVal, err = strconv.ParseBool(value)
   229  			if err != nil {
   230  				t.Fatalf("could not parse scope comment %q: %v", descr, err)
   231  			}
   232  		}
   233  	}
   234  
   235  	for i := 1; i < len(check.varChecks); i++ {
   236  		if check.varChecks[i-1].name == check.varChecks[i].name {
   237  			check.varChecks[i-1].shdw = true
   238  		}
   239  	}
   240  }
   241  
   242  func (scopeCheck *scopeCheck) checkLocalsAndArgs(p *proc.Target, t *testing.T) (*proc.EvalScope, bool) {
   243  	scope, err := proc.GoroutineScope(p, p.CurrentThread())
   244  	assertNoError(err, t, "GoroutineScope()")
   245  
   246  	ok := true
   247  
   248  	args, err := scope.FunctionArguments(normalLoadConfig)
   249  	assertNoError(err, t, "FunctionArguments()")
   250  	locals, err := scope.LocalVariables(normalLoadConfig)
   251  	assertNoError(err, t, "LocalVariables()")
   252  
   253  	for _, arg := range args {
   254  		scopeCheck.checkVar(arg, t)
   255  	}
   256  
   257  	for _, local := range locals {
   258  		scopeCheck.checkVar(local, t)
   259  	}
   260  
   261  	for i := range scopeCheck.varChecks {
   262  		if !scopeCheck.varChecks[i].ok {
   263  			t.Errorf("%d: variable %s not found", scopeCheck.line, scopeCheck.varChecks[i].name)
   264  			ok = false
   265  		}
   266  	}
   267  
   268  	return scope, ok
   269  }
   270  
   271  func (check *scopeCheck) checkVar(v *proc.Variable, t *testing.T) {
   272  	var varCheck *varCheck
   273  	for i := range check.varChecks {
   274  		if !check.varChecks[i].ok && (check.varChecks[i].name == v.Name) {
   275  			varCheck = &check.varChecks[i]
   276  			break
   277  		}
   278  	}
   279  
   280  	if varCheck == nil {
   281  		t.Errorf("%d: unexpected variable %s", check.line, v.Name)
   282  		return
   283  	}
   284  
   285  	varCheck.check(check.line, v, t, "FunctionArguments+LocalVariables")
   286  	varCheck.ok = true
   287  }
   288  
   289  func (varCheck *varCheck) checkInScope(line int, scope *proc.EvalScope, t *testing.T) {
   290  	v, err := scope.EvalExpression(varCheck.name, normalLoadConfig)
   291  	assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", varCheck.name))
   292  	varCheck.check(line, v, t, "EvalExpression")
   293  
   294  }
   295  
   296  func (varCheck *varCheck) check(line int, v *proc.Variable, t *testing.T, ctxt string) {
   297  	typ := v.DwarfType.String()
   298  	typ = strings.Replace(typ, " ", "", -1)
   299  	if typ != varCheck.typ {
   300  		t.Errorf("%d: wrong type for %s (%s), got %s, expected %s", line, v.Name, ctxt, typ, varCheck.typ)
   301  	}
   302  
   303  	if varCheck.shdw && v.Flags&proc.VariableShadowed == 0 {
   304  		t.Errorf("%d: expected shadowed %s variable", line, v.Name)
   305  	}
   306  
   307  	if !varCheck.hasVal {
   308  		return
   309  	}
   310  
   311  	switch varCheck.kind {
   312  	case reflect.Int:
   313  		if vv, _ := constant.Int64Val(v.Value); vv != varCheck.intVal {
   314  			t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.intVal)
   315  		}
   316  	case reflect.Uint:
   317  		if vv, _ := constant.Uint64Val(v.Value); vv != varCheck.uintVal {
   318  			t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.uintVal)
   319  		}
   320  	case reflect.Float64:
   321  		if vv, _ := constant.Float64Val(v.Value); math.Abs(vv-varCheck.floatVal) > 0.001 {
   322  			t.Errorf("%d: wrong value for %s (%s), got %g expected %g", line, v.Name, ctxt, vv, varCheck.floatVal)
   323  		}
   324  	case reflect.Bool:
   325  		if vv := constant.BoolVal(v.Value); vv != varCheck.boolVal {
   326  			t.Errorf("%d: wrong value for %s (%s), got %v expected %v", line, v.Name, ctxt, vv, varCheck.boolVal)
   327  		}
   328  	}
   329  }