mirror of
				https://github.com/go-delve/delve.git
				synced 2025-10-30 18:27:37 +08:00 
			
		
		
		
	Added info locals and info args commands
This commit is contained in:
		| @ -69,6 +69,8 @@ Once inside a debugging session, the following commands may be used: | |||||||
| * `info $type [regex]` - Outputs information about the symbol table. An optional regex filters the list. Example `info funcs unicode`. Valid types are: | * `info $type [regex]` - Outputs information about the symbol table. An optional regex filters the list. Example `info funcs unicode`. Valid types are: | ||||||
|   * `sources` - Prings the path of all source files |   * `sources` - Prings the path of all source files | ||||||
|   * `funcs` - Prings the name of all defined functions |   * `funcs` - Prings the name of all defined functions | ||||||
|  |   * `locals` - Prints the name and value of all local variables in the current context | ||||||
|  |   * `args` - Prints the name and value of all arguments to the current function | ||||||
|  |  | ||||||
| * `exit` - Exit the debugger. | * `exit` - Exit the debugger. | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ func barfoo() { | |||||||
| 	fmt.Println(a1) | 	fmt.Println(a1) | ||||||
| } | } | ||||||
|  |  | ||||||
| func foobar(baz string) { | func foobar(baz string, bar FooBar) { | ||||||
| 	var ( | 	var ( | ||||||
| 		a1  = "foo" | 		a1  = "foo" | ||||||
| 		a2  = 6 | 		a2  = 6 | ||||||
| @ -28,9 +28,9 @@ func foobar(baz string) { | |||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	barfoo() | 	barfoo() | ||||||
| 	fmt.Println(a1, a2, a3, a4, a5, a6, a7, baz, neg, i8, f32, i32) | 	fmt.Println(a1, a2, a3, a4, a5, a6, a7, baz, neg, i8, f32, i32, bar) | ||||||
| } | } | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	foobar("bazburzum") | 	foobar("bazburzum", FooBar{Baz: 10, Bur: "lorem"}) | ||||||
| } | } | ||||||
|  | |||||||
| @ -54,7 +54,7 @@ func DebugCommands() *Commands { | |||||||
| 		command{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."}, | 		command{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."}, | ||||||
| 		command{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."}, | 		command{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."}, | ||||||
| 		command{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."}, | 		command{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."}, | ||||||
| 		command{aliases: []string{"info"}, cmdFn: info, helpMsg: "Provides list of source files with symbols."}, | 		command{aliases: []string{"info"}, cmdFn: info, helpMsg: "Provides info about source, locals, args, or funcs."}, | ||||||
| 		command{aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."}, | 		command{aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -261,6 +261,19 @@ func printVar(p *proctl.DebuggedProcess, args ...string) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func filterVariables(vars []*proctl.Variable, filter *regexp.Regexp) []string { | ||||||
|  | 	data := make([]string, 0, len(vars)) | ||||||
|  | 	for _, v := range vars { | ||||||
|  | 		if v == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if filter == nil || filter.Match([]byte(v.Name)) { | ||||||
|  | 			data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return data | ||||||
|  | } | ||||||
|  |  | ||||||
| func info(p *proctl.DebuggedProcess, args ...string) error { | func info(p *proctl.DebuggedProcess, args ...string) error { | ||||||
| 	if len(args) == 0 { | 	if len(args) == 0 { | ||||||
| 		return fmt.Errorf("not enough arguments. expected info type [regex].") | 		return fmt.Errorf("not enough arguments. expected info type [regex].") | ||||||
| @ -294,8 +307,22 @@ func info(p *proctl.DebuggedProcess, args ...string) error { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 	case "args": | ||||||
|  | 		vars, err := p.CurrentThread.FunctionArguments() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		data = filterVariables(vars, filter) | ||||||
|  |  | ||||||
|  | 	case "locals": | ||||||
|  | 		vars, err := p.CurrentThread.LocalVariables() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		data = filterVariables(vars, filter) | ||||||
|  |  | ||||||
| 	default: | 	default: | ||||||
| 		return fmt.Errorf("unsupported info type, must be sources or functions") | 		return fmt.Errorf("unsupported info type, must be sources, funcs, locals, or args") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// sort and output data | 	// sort and output data | ||||||
|  | |||||||
| @ -311,7 +311,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) { | |||||||
|  |  | ||||||
| 	fn := thread.Process.GoSymTable.PCToFunc(pc) | 	fn := thread.Process.GoSymTable.PCToFunc(pc) | ||||||
| 	if fn == nil { | 	if fn == nil { | ||||||
| 		return nil, fmt.Errorf("could not func function scope") | 		return nil, fmt.Errorf("could not find function scope") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	reader := data.Reader() | 	reader := data.Reader() | ||||||
| @ -329,27 +329,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) { | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	offset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset) | 	return thread.extractVariableFromEntry(entry) | ||||||
| 	if !ok { |  | ||||||
| 		return nil, fmt.Errorf("type assertion failed") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	t, err := data.Type(offset) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instructions, ok := entry.Val(dwarf.AttrLocation).([]byte) |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, fmt.Errorf("type assertion failed") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	val, err := thread.extractValue(instructions, 0, t) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &Variable{Name: name, Type: t.String(), Value: val}, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // seekToFunctionEntry is basically used to seek the dwarf.Reader to | // seekToFunctionEntry is basically used to seek the dwarf.Reader to | ||||||
| @ -380,11 +360,23 @@ func seekToFunctionEntry(name string, reader *dwarf.Reader) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func findDwarfEntry(name string, reader *dwarf.Reader, member bool) (*dwarf.Entry, error) { | func findDwarfEntry(name string, reader *dwarf.Reader, member bool) (*dwarf.Entry, error) { | ||||||
|  | 	depth := 1 | ||||||
| 	for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { | 	for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if entry.Children { | ||||||
|  | 			depth++ | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if entry.Tag == 0 { | ||||||
|  | 			depth-- | ||||||
|  | 			if depth <= 0 { | ||||||
|  | 				return nil, fmt.Errorf("could not find symbol value for %s", name) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if member { | 		if member { | ||||||
| 			if entry.Tag != dwarf.TagMember { | 			if entry.Tag != dwarf.TagMember { | ||||||
| 				continue | 				continue | ||||||
| @ -430,6 +422,45 @@ func evaluateStructMember(thread *ThreadContext, data *dwarf.Data, reader *dwarf | |||||||
| 	return &Variable{Name: strings.Join([]string{parent, member}, "."), Type: t.String(), Value: val}, nil | 	return &Variable{Name: strings.Join([]string{parent, member}, "."), Type: t.String(), Value: val}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Extracts the name, type, and value of a variable from a dwarf entry | ||||||
|  | func (thread *ThreadContext) extractVariableFromEntry(entry *dwarf.Entry) (*Variable, error) { | ||||||
|  | 	if entry == nil { | ||||||
|  | 		return nil, fmt.Errorf("invalid entry") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if entry.Tag != dwarf.TagFormalParameter && entry.Tag != dwarf.TagVariable { | ||||||
|  | 		return nil, fmt.Errorf("invalid entry tag, only supports FormalParameter and Variable, got %s", entry.Tag.String()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	n, ok := entry.Val(dwarf.AttrName).(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("type assertion failed") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	offset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("type assertion failed") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data := thread.Process.Dwarf | ||||||
|  | 	t, err := data.Type(offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	instructions, ok := entry.Val(dwarf.AttrLocation).([]byte) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("type assertion failed") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	val, err := thread.extractValue(instructions, 0, t) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &Variable{Name: n, Type: t.String(), Value: val}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // Extracts the value from the instructions given in the DW_AT_location entry. | // Extracts the value from the instructions given in the DW_AT_location entry. | ||||||
| // We execute the stack program described in the DW_OP_* instruction stream, and | // We execute the stack program described in the DW_OP_* instruction stream, and | ||||||
| // then grab the value from the other processes memory. | // then grab the value from the other processes memory. | ||||||
| @ -624,6 +655,63 @@ func (thread *ThreadContext) readMemory(addr uintptr, size uintptr) ([]byte, err | |||||||
| 	return buf, nil | 	return buf, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Fetches all variables of a specific type in the current function scope | ||||||
|  | func (thread *ThreadContext) variablesByTag(tag dwarf.Tag) ([]*Variable, error) { | ||||||
|  | 	data := thread.Process.Dwarf | ||||||
|  |  | ||||||
|  | 	pc, err := thread.CurrentPC() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fn := thread.Process.GoSymTable.PCToFunc(pc) | ||||||
|  | 	if fn == nil { | ||||||
|  | 		return nil, fmt.Errorf("could not find function scope") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reader := data.Reader() | ||||||
|  | 	if err = seekToFunctionEntry(fn.Name, reader); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	vars := make([]*Variable, 0) | ||||||
|  |  | ||||||
|  | 	for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// End of function | ||||||
|  | 		if entry.Tag == 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if entry.Tag == tag { | ||||||
|  | 			val, err := thread.extractVariableFromEntry(entry) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			vars = append(vars, val) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Only care about top level | ||||||
|  | 		reader.SkipChildren() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return vars, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LocalVariables returns all local variables from the current function scope | ||||||
|  | func (thread *ThreadContext) LocalVariables() ([]*Variable, error) { | ||||||
|  | 	return thread.variablesByTag(dwarf.TagVariable) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // FunctionArguments returns the name, value, and type of all current function arguments | ||||||
|  | func (thread *ThreadContext) FunctionArguments() ([]*Variable, error) { | ||||||
|  | 	return thread.variablesByTag(dwarf.TagFormalParameter) | ||||||
|  | } | ||||||
|  |  | ||||||
| // Sets the length of a slice. | // Sets the length of a slice. | ||||||
| func setSliceLength(ptr unsafe.Pointer, l int) { | func setSliceLength(ptr unsafe.Pointer, l int) { | ||||||
| 	lptr := (*int)(unsafe.Pointer(uintptr(ptr) + ptrsize)) | 	lptr := (*int)(unsafe.Pointer(uintptr(ptr) + ptrsize)) | ||||||
|  | |||||||
| @ -2,9 +2,30 @@ package proctl | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"sort" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type varTest struct { | ||||||
|  | 	name    string | ||||||
|  | 	value   string | ||||||
|  | 	varType string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func assertVariable(t *testing.T, variable *Variable, expected varTest) { | ||||||
|  | 	if variable.Name != expected.name { | ||||||
|  | 		t.Fatalf("Expected %s got %s\n", expected.name, variable.Name) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if variable.Type != expected.varType { | ||||||
|  | 		t.Fatalf("Expected %s got %s\n", expected.varType, variable.Type) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if variable.Value != expected.value { | ||||||
|  | 		t.Fatalf("Expected %#v got %#v\n", expected.value, variable.Value) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestVariableEvaluation(t *testing.T) { | func TestVariableEvaluation(t *testing.T) { | ||||||
| 	executablePath := "../_fixtures/testvariables" | 	executablePath := "../_fixtures/testvariables" | ||||||
|  |  | ||||||
| @ -13,11 +34,7 @@ func TestVariableEvaluation(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	testcases := []struct { | 	testcases := []varTest{ | ||||||
| 		name    string |  | ||||||
| 		value   string |  | ||||||
| 		varType string |  | ||||||
| 	}{ |  | ||||||
| 		{"a1", "foo", "struct string"}, | 		{"a1", "foo", "struct string"}, | ||||||
| 		{"a2", "6", "int"}, | 		{"a2", "6", "int"}, | ||||||
| 		{"a3", "7.23", "float64"}, | 		{"a3", "7.23", "float64"}, | ||||||
| @ -44,18 +61,123 @@ func TestVariableEvaluation(t *testing.T) { | |||||||
|  |  | ||||||
| 		for _, tc := range testcases { | 		for _, tc := range testcases { | ||||||
| 			variable, err := p.EvalSymbol(tc.name) | 			variable, err := p.EvalSymbol(tc.name) | ||||||
| 			assertNoError(err, t, "Variable() returned an error") | 			assertNoError(err, t, "EvalSymbol() returned an error") | ||||||
|  | 			assertVariable(t, variable, tc) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
| 			if variable.Name != tc.name { | func TestVariableFunctionScoping(t *testing.T) { | ||||||
| 				t.Fatalf("Expected %s got %s\n", tc.name, variable.Name) | 	executablePath := "../_fixtures/testvariables" | ||||||
|  |  | ||||||
|  | 	fp, err := filepath.Abs(executablePath + ".go") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 			if variable.Type != tc.varType { | 	withTestProcess(executablePath, t, func(p *DebuggedProcess) { | ||||||
| 				t.Fatalf("Expected %s got %s\n", tc.varType, variable.Type) | 		pc, _, _ := p.GoSymTable.LineToPC(fp, 30) | ||||||
|  |  | ||||||
|  | 		_, err := p.Break(uintptr(pc)) | ||||||
|  | 		assertNoError(err, t, "Break() returned an error") | ||||||
|  |  | ||||||
|  | 		err = p.Continue() | ||||||
|  | 		assertNoError(err, t, "Continue() returned an error") | ||||||
|  |  | ||||||
|  | 		_, err = p.EvalSymbol("a1") | ||||||
|  | 		assertNoError(err, t, "Unable to find variable a1") | ||||||
|  |  | ||||||
|  | 		_, err = p.EvalSymbol("a2") | ||||||
|  | 		assertNoError(err, t, "Unable to find variable a1") | ||||||
|  |  | ||||||
|  | 		// Move scopes, a1 exists here by a2 does not | ||||||
|  | 		pc, _, _ = p.GoSymTable.LineToPC(fp, 12) | ||||||
|  |  | ||||||
|  | 		_, err = p.Break(uintptr(pc)) | ||||||
|  | 		assertNoError(err, t, "Break() returned an error") | ||||||
|  |  | ||||||
|  | 		err = p.Continue() | ||||||
|  | 		assertNoError(err, t, "Continue() returned an error") | ||||||
|  |  | ||||||
|  | 		_, err = p.EvalSymbol("a1") | ||||||
|  | 		assertNoError(err, t, "Unable to find variable a1") | ||||||
|  |  | ||||||
|  | 		_, err = p.EvalSymbol("a2") | ||||||
|  | 		if err == nil { | ||||||
|  | 			t.Fatalf("Can eval out of scope variable a2") | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type varArray []*Variable | ||||||
|  |  | ||||||
|  | // Len is part of sort.Interface. | ||||||
|  | func (s varArray) Len() int { | ||||||
|  | 	return len(s) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Swap is part of sort.Interface. | ||||||
|  | func (s varArray) Swap(i, j int) { | ||||||
|  | 	s[i], s[j] = s[j], s[i] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. | ||||||
|  | func (s varArray) Less(i, j int) bool { | ||||||
|  | 	return s[i].Name < s[j].Name | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestLocalVariables(t *testing.T) { | ||||||
|  | 	executablePath := "../_fixtures/testvariables" | ||||||
|  |  | ||||||
|  | 	fp, err := filepath.Abs(executablePath + ".go") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 			if variable.Value != tc.value { | 	testcases := []struct { | ||||||
| 				t.Fatalf("Expected %#v got %#v\n", tc.value, variable.Value) | 		fn     func(*ThreadContext) ([]*Variable, error) | ||||||
|  | 		output []varTest | ||||||
|  | 	}{ | ||||||
|  | 		{(*ThreadContext).LocalVariables, | ||||||
|  | 			[]varTest{ | ||||||
|  | 				{"a1", "foo", "struct string"}, | ||||||
|  | 				{"a2", "6", "int"}, | ||||||
|  | 				{"a3", "7.23", "float64"}, | ||||||
|  | 				{"a4", "[2]int [1 2]", "[2]int"}, | ||||||
|  | 				{"a5", "len: 5 cap: 5 [1 2 3 4 5]", "struct []int"}, | ||||||
|  | 				{"a6", "main.FooBar {Baz: 8, Bur: word}", "main.FooBar"}, | ||||||
|  | 				{"a7", "*main.FooBar {Baz: 5, Bur: strum}", "*main.FooBar"}, | ||||||
|  | 				{"f32", "1.2", "float32"}, | ||||||
|  | 				{"i32", "[2]int32 [1 2]", "[2]int32"}, | ||||||
|  | 				{"i8", "1", "int8"}, | ||||||
|  | 				{"neg", "-1", "int"}}}, | ||||||
|  | 		{(*ThreadContext).FunctionArguments, | ||||||
|  | 			[]varTest{ | ||||||
|  | 				{"bar", "main.FooBar {Baz: 10, Bur: lorem}", "main.FooBar"}, | ||||||
|  | 				{"baz", "bazburzum", "struct string"}}}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	withTestProcess(executablePath, t, func(p *DebuggedProcess) { | ||||||
|  | 		pc, _, _ := p.GoSymTable.LineToPC(fp, 30) | ||||||
|  |  | ||||||
|  | 		_, err := p.Break(uintptr(pc)) | ||||||
|  | 		assertNoError(err, t, "Break() returned an error") | ||||||
|  |  | ||||||
|  | 		err = p.Continue() | ||||||
|  | 		assertNoError(err, t, "Continue() returned an error") | ||||||
|  |  | ||||||
|  | 		for _, tc := range testcases { | ||||||
|  | 			vars, err := tc.fn(p.CurrentThread) | ||||||
|  | 			assertNoError(err, t, "LocalVariables() returned an error") | ||||||
|  |  | ||||||
|  | 			sort.Sort(varArray(vars)) | ||||||
|  |  | ||||||
|  | 			if len(tc.output) != len(vars) { | ||||||
|  | 				t.Fatalf("Invalid variable count. Expected %d got %d.", len(tc.output), len(vars)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for i, variable := range vars { | ||||||
|  | 				assertVariable(t, variable, tc.output[i]) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 epipho
					epipho