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:
aarzilli
2016-02-23 15:12:04 +01:00
parent b3c1ca5384
commit 5ba9bcd740
4 changed files with 365 additions and 227 deletions

View File

@ -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)
}
}
})
}