mirror of
				https://github.com/go-delve/delve.git
				synced 2025-11-01 03:42:59 +08:00 
			
		
		
		
	terminal: Mechanism to handle command prefixes
Implements extensible mechanism to specify which commands accept prefixes (goroutine, frame, on) instead of hardcoding them in a switch. Implements #240
This commit is contained in:
		| @ -6,6 +6,7 @@ import ( | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| @ -17,12 +18,13 @@ import ( | ||||
|  | ||||
| type FakeTerminal struct { | ||||
| 	*Term | ||||
| 	t testing.TB | ||||
| 	t      testing.TB | ||||
| 	client service.Client | ||||
| 	cmds   *Commands | ||||
| } | ||||
|  | ||||
| func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) { | ||||
| 	cmdstr, args := parseCommand(cmdstr) | ||||
| 	cmd := ft.cmds.Find(cmdstr) | ||||
|  | ||||
| 	outfh, err := ioutil.TempFile("", "cmdtestout") | ||||
| 	if err != nil { | ||||
| @ -41,7 +43,7 @@ func (ft *FakeTerminal) Exec(cmdstr string) (outstr string, err error) { | ||||
| 		outstr = string(outbs) | ||||
| 		os.Remove(outfh.Name()) | ||||
| 	}() | ||||
| 	err = cmd(ft.Term, args) | ||||
| 	err = ft.cmds.Call(cmdstr, args, ft.Term) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| @ -53,6 +55,23 @@ func (ft *FakeTerminal) MustExec(cmdstr string) string { | ||||
| 	return outstr | ||||
| } | ||||
|  | ||||
| func (ft *FakeTerminal) AssertExec(cmdstr, tgt string) { | ||||
| 	out := ft.MustExec(cmdstr) | ||||
| 	if out != tgt { | ||||
| 		ft.t.Fatalf("Error executing %q, expected %q got %q", cmdstr, tgt, out) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ft *FakeTerminal) AssertExecError(cmdstr, tgterr string) { | ||||
| 	_, err := ft.Exec(cmdstr) | ||||
| 	if err == nil { | ||||
| 		ft.t.Fatalf("Expected error executing %q") | ||||
| 	} | ||||
| 	if err.Error() != tgterr { | ||||
| 		ft.t.Fatalf("Expected error %q executing %q, got error %q", tgterr, cmdstr, err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) { | ||||
| 	listener, err := net.Listen("tcp", "localhost:0") | ||||
| 	if err != nil { | ||||
| @ -70,9 +89,12 @@ func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) { | ||||
| 	defer func() { | ||||
| 		client.Detach(true) | ||||
| 	}() | ||||
|  | ||||
| 	ft := &FakeTerminal{ | ||||
| 		t:    t, | ||||
| 		Term: New(client, nil), | ||||
| 		t:      t, | ||||
| 		client: client, | ||||
| 		cmds:   DebugCommands(client), | ||||
| 		Term:   New(client, nil), | ||||
| 	} | ||||
| 	fn(ft) | ||||
| } | ||||
| @ -80,10 +102,10 @@ func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) { | ||||
| func TestCommandDefault(t *testing.T) { | ||||
| 	var ( | ||||
| 		cmds = Commands{} | ||||
| 		cmd  = cmds.Find("non-existant-command") | ||||
| 		cmd  = cmds.Find("non-existant-command", noPrefix) | ||||
| 	) | ||||
|  | ||||
| 	err := cmd(nil, "") | ||||
| 	err := cmd(nil, callContext{}, "") | ||||
| 	if err == nil { | ||||
| 		t.Fatal("cmd() did not default") | ||||
| 	} | ||||
| @ -95,16 +117,16 @@ func TestCommandDefault(t *testing.T) { | ||||
|  | ||||
| func TestCommandReplay(t *testing.T) { | ||||
| 	cmds := DebugCommands(nil) | ||||
| 	cmds.Register("foo", func(t *Term, args string) error { return fmt.Errorf("registered command") }, "foo command") | ||||
| 	cmd := cmds.Find("foo") | ||||
| 	cmds.Register("foo", func(t *Term, ctx callContext, args string) error { return fmt.Errorf("registered command") }, "foo command") | ||||
| 	cmd := cmds.Find("foo", noPrefix) | ||||
|  | ||||
| 	err := cmd(nil, "") | ||||
| 	err := cmd(nil, callContext{}, "") | ||||
| 	if err.Error() != "registered command" { | ||||
| 		t.Fatal("wrong command output") | ||||
| 	} | ||||
|  | ||||
| 	cmd = cmds.Find("") | ||||
| 	err = cmd(nil, "") | ||||
| 	cmd = cmds.Find("", noPrefix) | ||||
| 	err = cmd(nil, callContext{}, "") | ||||
| 	if err.Error() != "registered command" { | ||||
| 		t.Fatal("wrong command output") | ||||
| 	} | ||||
| @ -113,8 +135,8 @@ func TestCommandReplay(t *testing.T) { | ||||
| func TestCommandReplayWithoutPreviousCommand(t *testing.T) { | ||||
| 	var ( | ||||
| 		cmds = DebugCommands(nil) | ||||
| 		cmd  = cmds.Find("") | ||||
| 		err  = cmd(nil, "") | ||||
| 		cmd  = cmds.Find("", noPrefix) | ||||
| 		err  = cmd(nil, callContext{}, "") | ||||
| 	) | ||||
|  | ||||
| 	if err != nil { | ||||
| @ -125,10 +147,10 @@ func TestCommandReplayWithoutPreviousCommand(t *testing.T) { | ||||
| func TestCommandThread(t *testing.T) { | ||||
| 	var ( | ||||
| 		cmds = DebugCommands(nil) | ||||
| 		cmd  = cmds.Find("thread") | ||||
| 		cmd  = cmds.Find("thread", noPrefix) | ||||
| 	) | ||||
|  | ||||
| 	err := cmd(nil, "") | ||||
| 	err := cmd(nil, callContext{}, "") | ||||
| 	if err == nil { | ||||
| 		t.Fatal("thread terminal command did not default") | ||||
| 	} | ||||
| @ -144,11 +166,11 @@ func TestExecuteFile(t *testing.T) { | ||||
| 	c := &Commands{ | ||||
| 		client: nil, | ||||
| 		cmds: []command{ | ||||
| 			{aliases: []string{"trace"}, cmdFn: func(t *Term, args string) error { | ||||
| 			{aliases: []string{"trace"}, cmdFn: func(t *Term, ctx callContext, args string) error { | ||||
| 				traceCount++ | ||||
| 				return nil | ||||
| 			}}, | ||||
| 			{aliases: []string{"break"}, cmdFn: func(t *Term, args string) error { | ||||
| 			{aliases: []string{"break"}, cmdFn: func(t *Term, ctx callContext, args string) error { | ||||
| 				breakCount++ | ||||
| 				return nil | ||||
| 			}}, | ||||
| @ -184,3 +206,153 @@ func TestIssue411(t *testing.T) { | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestScopePrefix(t *testing.T) { | ||||
| 	const goroutinesLinePrefix = "  Goroutine " | ||||
| 	const goroutinesCurLinePrefix = "* Goroutine " | ||||
| 	withTestTerminal("goroutinestackprog", t, func(term *FakeTerminal) { | ||||
| 		term.MustExec("b stacktraceme") | ||||
| 		term.MustExec("continue") | ||||
|  | ||||
| 		goroutinesOut := strings.Split(term.MustExec("goroutines"), "\n") | ||||
|  | ||||
| 		agoroutines := []int{} | ||||
| 		curgid := -1 | ||||
|  | ||||
| 		for _, line := range goroutinesOut { | ||||
| 			iscur := strings.HasPrefix(line, goroutinesCurLinePrefix) | ||||
| 			if !iscur && !strings.HasPrefix(line, goroutinesLinePrefix) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			dash := strings.Index(line, " - ") | ||||
| 			if dash < 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			gid, err := strconv.Atoi(line[len(goroutinesLinePrefix):dash]) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if iscur { | ||||
| 				curgid = gid | ||||
| 			} | ||||
|  | ||||
| 			if idx := strings.Index(line, " main.agoroutine "); idx < 0 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			agoroutines = append(agoroutines, gid) | ||||
| 		} | ||||
|  | ||||
| 		if len(agoroutines) != 10 { | ||||
| 			t.Fatalf("Output of goroutines did not have 10 goroutines stopped on main.agoroutine: %q", goroutinesOut) | ||||
| 		} | ||||
|  | ||||
| 		if curgid < 0 { | ||||
| 			t.Fatalf("Could not find current goroutine in output of goroutines: %q", goroutinesOut) | ||||
| 		} | ||||
|  | ||||
| 		seen := make([]bool, 10) | ||||
| 		_ = seen | ||||
|  | ||||
| 		for _, gid := range agoroutines { | ||||
| 			stackOut := strings.Split(term.MustExec(fmt.Sprintf("goroutine %d stack", gid)), "\n") | ||||
| 			fid := -1 | ||||
| 			for _, line := range stackOut { | ||||
| 				space := strings.Index(line, " ") | ||||
| 				if space < 0 { | ||||
| 					continue | ||||
| 				} | ||||
| 				curfid, err := strconv.Atoi(line[:space]) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				if idx := strings.Index(line, " main.agoroutine"); idx >= 0 { | ||||
| 					fid = curfid | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			if fid < 0 { | ||||
| 				t.Fatalf("Could not find frame for goroutine %d: %v", gid, stackOut) | ||||
| 			} | ||||
| 			term.AssertExec(fmt.Sprintf("goroutine %d frame %d locals", gid, fid), "") | ||||
| 			argsOut := strings.Split(term.MustExec(fmt.Sprintf("goroutine %d frame %d args", gid, fid)), "\n") | ||||
| 			if len(argsOut) != 4 || argsOut[3] != "" { | ||||
| 				t.Fatalf("Wrong number of arguments in goroutine %d frame %d: %v", gid, fid, argsOut) | ||||
| 			} | ||||
| 			out := term.MustExec(fmt.Sprintf("goroutine %d frame %d p i", gid, fid)) | ||||
| 			ival, err := strconv.Atoi(out[:len(out)-1]) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("could not parse value %q of i for goroutine %d frame %d: %v", out, gid, fid, err) | ||||
| 			} | ||||
| 			seen[ival] = true | ||||
| 		} | ||||
|  | ||||
| 		for i := range seen { | ||||
| 			if !seen[i] { | ||||
| 				t.Fatalf("goroutine %d not found", i) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		term.MustExec("c") | ||||
|  | ||||
| 		term.AssertExecError("frame", "not enough arguments") | ||||
| 		term.AssertExecError("frame 1", "not enough arguments") | ||||
| 		term.AssertExecError("frame 1 goroutines", "command not available") | ||||
| 		term.AssertExecError("frame 1 goroutine", "no command passed to goroutine") | ||||
| 		term.AssertExecError(fmt.Sprintf("frame 1 goroutine %d", curgid), "no command passed to goroutine") | ||||
| 		term.AssertExecError(fmt.Sprintf("goroutine %d frame 10 locals", curgid), fmt.Sprintf("Frame 10 does not exist in goroutine %d", curgid)) | ||||
| 		term.AssertExecError("goroutine 9000 locals", "Unknown goroutine 9000") | ||||
|  | ||||
| 		term.AssertExecError("print n", "could not find symbol value for n") | ||||
| 		term.AssertExec("frame 1 print n", "3\n") | ||||
| 		term.AssertExec("frame 2 print n", "2\n") | ||||
| 		term.AssertExec("frame 3 print n", "1\n") | ||||
| 		term.AssertExec("frame 4 print n", "0\n") | ||||
| 		term.AssertExecError("frame 5 print n", "could not find symbol value for n") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestOnPrefix(t *testing.T) { | ||||
| 	const prefix = "\ti: " | ||||
| 	withTestTerminal("goroutinestackprog", t, func(term *FakeTerminal) { | ||||
| 		term.MustExec("b agobp main.agoroutine") | ||||
| 		term.MustExec("on agobp print i") | ||||
|  | ||||
| 		seen := make([]bool, 10) | ||||
|  | ||||
| 		for { | ||||
| 			outstr, err := term.Exec("continue") | ||||
| 			if err != nil { | ||||
| 				if strings.Index(err.Error(), "exited") < 0 { | ||||
| 					t.Fatalf("Unexpected error executing 'continue': %v", err) | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 			out := strings.Split(outstr, "\n") | ||||
|  | ||||
| 			for i := range out { | ||||
| 				if !strings.HasPrefix(out[i], "\ti: ") { | ||||
| 					continue | ||||
| 				} | ||||
| 				id, err := strconv.Atoi(out[i][len(prefix):]) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
| 				if seen[id] { | ||||
| 					t.Fatalf("Goroutine %d seen twice\n", id) | ||||
| 				} | ||||
| 				seen[id] = true | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for i := range seen { | ||||
| 			if !seen[i] { | ||||
| 				t.Fatalf("Goroutine %d not seen\n", i) | ||||
| 			} | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 aarzilli
					aarzilli