Extend the "frame" command to set the current frame. (#1110)

* Extend the "frame" command to set the current frame.

Command

  frame 3

sets up so that subsequent "print", "set", "whatis" command
will operate on frame 3.

  frame 3 print foo

continues to work.

Added "up", "down". They move the current frame up or down.

Implementation note:

This changes removes "scopePrefix" mode from the terminal/command.go and instead
have the command examine the goroutine/frame value to see if it is invoked in a
scoped context.

* Rename Command.Frame -> Command.frame.
This commit is contained in:
Yasushi Saito 2018-03-22 10:02:15 -07:00 committed by Derek Parker
parent ec8dc3a10d
commit 82aff3f18a
5 changed files with 150 additions and 63 deletions

@ -9,7 +9,10 @@ func agoroutine(started chan<- struct{}, done chan<- struct{}, i int) {
done <- struct{}{} done <- struct{}{}
} }
var dummy int
func stacktraceme() { func stacktraceme() {
dummy++
return return
} }

@ -896,7 +896,7 @@ func stackMatch(stack []loc, locations []proc.Stackframe, skipRuntime bool) bool
} }
func TestStacktraceGoroutine(t *testing.T) { func TestStacktraceGoroutine(t *testing.T) {
mainStack := []loc{{13, "main.stacktraceme"}, {26, "main.main"}} mainStack := []loc{{14, "main.stacktraceme"}, {29, "main.main"}}
agoroutineStacks := [][]loc{ agoroutineStacks := [][]loc{
{{8, "main.agoroutine"}}, {{8, "main.agoroutine"}},
{{9, "main.agoroutine"}}, {{9, "main.agoroutine"}},

@ -32,7 +32,7 @@ func TestGoroutineCreationLocation(t *testing.T) {
if filepath.Base(createdLocation.File) != "goroutinestackprog.go" { if filepath.Base(createdLocation.File) != "goroutinestackprog.go" {
t.Fatalf("goroutine creation file incorrect: %s", filepath.Base(createdLocation.File)) t.Fatalf("goroutine creation file incorrect: %s", filepath.Base(createdLocation.File))
} }
if createdLocation.Line != 20 { if createdLocation.Line != 23 {
t.Fatalf("goroutine creation line incorrect: %v", createdLocation.Line) t.Fatalf("goroutine creation line incorrect: %v", createdLocation.Line)
} }
} }

@ -29,9 +29,8 @@ const optimizedFunctionWarning = "Warning: debugging optimized function"
type cmdPrefix int type cmdPrefix int
const ( const (
noPrefix = cmdPrefix(0) noPrefix = cmdPrefix(0)
scopePrefix = cmdPrefix(1 << iota) onPrefix = cmdPrefix(1 << iota)
onPrefix
) )
type callContext struct { type callContext struct {
@ -40,6 +39,18 @@ type callContext struct {
Breakpoint *api.Breakpoint Breakpoint *api.Breakpoint
} }
func (ctx *callContext) scoped() bool {
return ctx.Scope.GoroutineID >= 0 || ctx.Scope.Frame > 0
}
type frameDirection int
const (
frameSet frameDirection = iota
frameUp
frameDown
)
type cmdfunc func(t *Term, ctx callContext, args string) error type cmdfunc func(t *Term, ctx callContext, args string) error
type command struct { type command struct {
@ -62,9 +73,10 @@ func (c command) match(cmdstr string) bool {
// Commands represents the commands for Delve terminal process. // Commands represents the commands for Delve terminal process.
type Commands struct { type Commands struct {
cmds []command cmds []command
lastCmd cmdfunc lastCmd cmdfunc
client service.Client client service.Client
frame int // Current frame as set by frame/up/down commands.
} }
var ( var (
@ -111,11 +123,11 @@ See also: "help on", "help cond" and "help clear"`},
checkpoint. For normal processes restarts the process, optionally changing checkpoint. For normal processes restarts the process, optionally changing
the arguments. With -noargs, the process starts with an empty commandline. the arguments. With -noargs, the process starts with an empty commandline.
`}, `},
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."}, {aliases: []string{"continue", "c"}, cmdFn: c.cont, helpMsg: "Run until breakpoint or program termination."},
{aliases: []string{"step", "s"}, allowedPrefixes: scopePrefix, cmdFn: step, helpMsg: "Single step through program."}, {aliases: []string{"step", "s"}, cmdFn: c.step, helpMsg: "Single step through program."},
{aliases: []string{"step-instruction", "si"}, allowedPrefixes: scopePrefix, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."}, {aliases: []string{"step-instruction", "si"}, cmdFn: c.stepInstruction, helpMsg: "Single step a single cpu instruction."},
{aliases: []string{"next", "n"}, allowedPrefixes: scopePrefix, cmdFn: next, helpMsg: "Step over to next source line."}, {aliases: []string{"next", "n"}, cmdFn: c.next, helpMsg: "Step over to next source line."},
{aliases: []string{"stepout"}, allowedPrefixes: scopePrefix, cmdFn: stepout, helpMsg: "Step out of the current function."}, {aliases: []string{"stepout"}, cmdFn: c.stepout, helpMsg: "Step out of the current function."},
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."}, {aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread. {aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread.
@ -139,7 +151,7 @@ Print out info for every goroutine. The flag controls what information is shown
-g displays location of go instruction that created the goroutine -g displays location of go instruction that created the goroutine
If no flag is specified the default is -u.`}, If no flag is specified the default is -u.`},
{aliases: []string{"goroutine"}, allowedPrefixes: onPrefix | scopePrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine {aliases: []string{"goroutine"}, allowedPrefixes: onPrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine
goroutine goroutine
goroutine <id> goroutine <id>
@ -149,15 +161,15 @@ Called without arguments it will show information about the current goroutine.
Called with a single argument it will switch to the specified goroutine. Called with a single argument it will switch to the specified goroutine.
Called with more arguments it will execute a command on the specified goroutine.`}, Called with more arguments it will execute a command on the specified goroutine.`},
{aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."}, {aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."},
{aliases: []string{"print", "p"}, allowedPrefixes: onPrefix | scopePrefix, cmdFn: printVar, helpMsg: `Evaluate an expression. {aliases: []string{"print", "p"}, allowedPrefixes: onPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression.
[goroutine <n>] [frame <m>] print <expression> [goroutine <n>] [frame <m>] print <expression>
See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions.`}, See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions.`},
{aliases: []string{"whatis"}, allowedPrefixes: scopePrefix, cmdFn: whatisCommand, helpMsg: `Prints type of an expression. {aliases: []string{"whatis"}, cmdFn: whatisCommand, helpMsg: `Prints type of an expression.
whatis <expression>.`}, whatis <expression>.`},
{aliases: []string{"set"}, allowedPrefixes: scopePrefix, cmdFn: setVar, helpMsg: `Changes the value of a variable. {aliases: []string{"set"}, cmdFn: setVar, helpMsg: `Changes the value of a variable.
[goroutine <n>] [frame <m>] set <variable> = <value> [goroutine <n>] [frame <m>] set <variable> = <value>
@ -177,12 +189,12 @@ If regex is specified only the functions matching it will be returned.`},
types [<regex>] types [<regex>]
If regex is specified only the types matching it will be returned.`}, If regex is specified only the types matching it will be returned.`},
{aliases: []string{"args"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: args, helpMsg: `Print function arguments. {aliases: []string{"args"}, allowedPrefixes: onPrefix, cmdFn: args, helpMsg: `Print function arguments.
[goroutine <n>] [frame <m>] args [-v] [<regex>] [goroutine <n>] [frame <m>] args [-v] [<regex>]
If regex is specified only function arguments with a name matching it will be returned. If -v is specified more information about each function argument will be shown.`}, If regex is specified only function arguments with a name matching it will be returned. If -v is specified more information about each function argument will be shown.`},
{aliases: []string{"locals"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: locals, helpMsg: `Print local variables. {aliases: []string{"locals"}, allowedPrefixes: onPrefix, cmdFn: locals, helpMsg: `Print local variables.
[goroutine <n>] [frame <m>] locals [-v] [<regex>] [goroutine <n>] [frame <m>] locals [-v] [<regex>]
@ -200,25 +212,53 @@ If regex is specified only package variables with a name matching it will be ret
Argument -a shows more registers.`}, Argument -a shows more registers.`},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."}, {aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."},
{aliases: []string{"list", "ls", "l"}, allowedPrefixes: scopePrefix, cmdFn: listCommand, helpMsg: `Show source code. {aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code.
[goroutine <n>] [frame <m>] list [<linespec>] [goroutine <n>] [frame <m>] list [<linespec>]
Show source around current point or provided linespec.`}, Show source around current point or provided linespec.`},
{aliases: []string{"stack", "bt"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace. {aliases: []string{"stack", "bt"}, allowedPrefixes: onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace.
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-g] [-s] [-offsets] [goroutine <n>] [frame <m>] stack [<depth>] [-full] [-g] [-s] [-offsets]
-full every stackframe is decorated with the value of its local variables and arguments. -full every stackframe is decorated with the value of its local variables and arguments.
-offsets prints frame offset of each frame -offsets prints frame offset of each frame
`}, `},
{aliases: []string{"frame"}, allowedPrefixes: scopePrefix, cmdFn: c.frame, helpMsg: `Executes command on a different frame. {aliases: []string{"frame"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameSet)
},
helpMsg: `Set the current frame, or execute command on a different frame.
frame <frame index> <command>.`}, frame <m>
frame <m> <command>
The first form sets frame used by subsequent commands such as "print" or "set".
The second form runs the command on the given frame.`},
{aliases: []string{"up"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameUp)
},
helpMsg: `Move the current frame up.
up [<m>]
up [<m>] <command>
Move the current frame up by <m>. The second form runs the command on the given frame.`},
{aliases: []string{"down"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameDown)
},
helpMsg: `Move the current frame down.
down [<m>]
down [<m>] <command>
Move the current frame down by <m>. The second form runs the command on the given frame.`},
{aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: `Executes a file containing a list of delve commands {aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: `Executes a file containing a list of delve commands
source <path>`}, source <path>`},
{aliases: []string{"disassemble", "disass"}, allowedPrefixes: scopePrefix, cmdFn: disassCommand, helpMsg: `Disassembler. {aliases: []string{"disassemble", "disass"}, cmdFn: disassCommand, helpMsg: `Disassembler.
[goroutine <n>] [frame <m>] disassemble [-a <start> <end>] [-l <locspec>] [goroutine <n>] [frame <m>] disassemble [-a <start> <end>] [-l <locspec>]
@ -349,7 +389,7 @@ func (c *Commands) CallWithContext(cmdstr string, t *Term, ctx callContext) erro
} }
func (c *Commands) Call(cmdstr string, t *Term) error { func (c *Commands) Call(cmdstr string, t *Term) error {
ctx := callContext{Prefix: noPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: 0}} ctx := callContext{Prefix: noPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: c.frame}}
return c.CallWithContext(cmdstr, t, ctx) return c.CallWithContext(cmdstr, t, ctx)
} }
@ -544,9 +584,6 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
} }
if len(args) == 1 { if len(args) == 1 {
if ctx.Prefix == scopePrefix {
return errors.New("no command passed to goroutine")
}
if args[0] == "" { if args[0] == "" {
return printscope(t) return printscope(t)
} }
@ -563,13 +600,12 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
if err != nil { if err != nil {
return err return err
} }
c.frame = 0
fmt.Printf("Switched from %d to %d (thread %d)\n", selectedGID(oldState), gid, newState.CurrentThread.ID) fmt.Printf("Switched from %d to %d (thread %d)\n", selectedGID(oldState), gid, newState.CurrentThread.ID)
return nil return nil
} }
var err error var err error
ctx.Prefix = scopePrefix
ctx.Scope.GoroutineID, err = strconv.Atoi(args[0]) ctx.Scope.GoroutineID, err = strconv.Atoi(args[0])
if err != nil { if err != nil {
return err return err
@ -577,21 +613,54 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
return c.CallWithContext(args[1], t, ctx) return c.CallWithContext(args[1], t, ctx)
} }
func (c *Commands) frame(t *Term, ctx callContext, args string) error { // Handle "frame", "up", "down" commands.
v := strings.SplitN(args, " ", 2) func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, direction frameDirection) error {
frame := 1
switch len(v) { arg := ""
case 0, 1: if len(argstr) == 0 {
return errors.New("not enough arguments") if direction == frameSet {
return errors.New("not enough arguments")
}
} else {
args := strings.SplitN(argstr, " ", 2)
var err error
if frame, err = strconv.Atoi(args[0]); err != nil {
return err
}
if len(args) > 1 {
arg = args[1]
}
} }
switch direction {
var err error case frameUp:
ctx.Prefix = scopePrefix frame = c.frame + frame
ctx.Scope.Frame, err = strconv.Atoi(v[0]) case frameDown:
frame = c.frame - frame
}
if len(arg) > 0 {
ctx.Scope.Frame = frame
return c.CallWithContext(arg, t, ctx)
}
if frame < 0 {
return fmt.Errorf("Invalid frame %d", frame)
}
stack, err := t.client.Stacktrace(ctx.Scope.GoroutineID, frame, nil)
if err != nil { if err != nil {
return err return err
} }
return c.CallWithContext(v[1], t, ctx) if frame >= len(stack) {
return fmt.Errorf("Invalid frame %d", frame)
}
c.frame = frame
state, err := t.client.GetState()
if err != nil {
return err
}
printcontext(t, state)
th := stack[frame]
fmt.Printf("Frame %d: %s:%d (PC: %x)\n", frame, ShortenFilePath(th.File), th.Line, th.PC)
printfile(t, th.File, th.Line, true)
return nil
} }
func printscope(t *Term) error { func printscope(t *Term) error {
@ -730,7 +799,8 @@ func printfileNoState(t *Term) {
} }
} }
func cont(t *Term, ctx callContext, args string) error { func (c *Commands) cont(t *Term, ctx callContext, args string) error {
c.frame = 0
stateChan := t.client.Continue() stateChan := t.client.Continue()
var state *api.DebuggerState var state *api.DebuggerState
for state = range stateChan { for state = range stateChan {
@ -768,12 +838,6 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string) err
} }
func scopePrefixSwitch(t *Term, ctx callContext) error { func scopePrefixSwitch(t *Term, ctx callContext) error {
if ctx.Prefix != scopePrefix {
return nil
}
if ctx.Scope.Frame != 0 {
return errors.New("frame prefix not accepted")
}
if ctx.Scope.GoroutineID > 0 { if ctx.Scope.GoroutineID > 0 {
_, err := t.client.SwitchGoroutine(ctx.Scope.GoroutineID) _, err := t.client.SwitchGoroutine(ctx.Scope.GoroutineID)
if err != nil { if err != nil {
@ -790,10 +854,11 @@ func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, err
return state, err return state, err
} }
func step(t *Term, ctx callContext, args string) error { func (c *Commands) step(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil { if err := scopePrefixSwitch(t, ctx); err != nil {
return err return err
} }
c.frame = 0
state, err := exitedToError(t.client.Step()) state, err := exitedToError(t.client.Step())
if err != nil { if err != nil {
printfileNoState(t) printfileNoState(t)
@ -803,11 +868,12 @@ func step(t *Term, ctx callContext, args string) error {
return continueUntilCompleteNext(t, state, "step") return continueUntilCompleteNext(t, state, "step")
} }
func stepInstruction(t *Term, ctx callContext, args string) error { func (c *Commands) stepInstruction(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil { if err := scopePrefixSwitch(t, ctx); err != nil {
return err return err
} }
state, err := exitedToError(t.client.StepInstruction()) state, err := exitedToError(t.client.StepInstruction())
c.frame = 0
if err != nil { if err != nil {
printfileNoState(t) printfileNoState(t)
return err return err
@ -817,11 +883,12 @@ func stepInstruction(t *Term, ctx callContext, args string) error {
return nil return nil
} }
func next(t *Term, ctx callContext, args string) error { func (c *Commands) next(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil { if err := scopePrefixSwitch(t, ctx); err != nil {
return err return err
} }
state, err := exitedToError(t.client.Next()) state, err := exitedToError(t.client.Next())
c.frame = 0
if err != nil { if err != nil {
printfileNoState(t) printfileNoState(t)
return err return err
@ -830,11 +897,12 @@ func next(t *Term, ctx callContext, args string) error {
return continueUntilCompleteNext(t, state, "next") return continueUntilCompleteNext(t, state, "next")
} }
func stepout(t *Term, ctx callContext, args string) error { func (c *Commands) stepout(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil { if err := scopePrefixSwitch(t, ctx); err != nil {
return err return err
} }
state, err := exitedToError(t.client.StepOut()) state, err := exitedToError(t.client.StepOut())
c.frame = 0
if err != nil { if err != nil {
printfileNoState(t) printfileNoState(t)
return err return err
@ -949,7 +1017,7 @@ func breakpoints(t *Term, ctx callContext, args string) error {
return nil return nil
} }
func setBreakpoint(t *Term, tracepoint bool, argstr string) error { func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) error {
args := strings.SplitN(argstr, " ", 2) args := strings.SplitN(argstr, " ", 2)
requestedBp := &api.Breakpoint{} requestedBp := &api.Breakpoint{}
@ -969,7 +1037,7 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error {
} }
requestedBp.Tracepoint = tracepoint requestedBp.Tracepoint = tracepoint
locs, err := t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, locspec) locs, err := t.client.FindLocation(ctx.Scope, locspec)
if err != nil { if err != nil {
if requestedBp.Name == "" { if requestedBp.Name == "" {
return err return err
@ -977,7 +1045,7 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error {
requestedBp.Name = "" requestedBp.Name = ""
locspec = argstr locspec = argstr
var err2 error var err2 error
locs, err2 = t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, locspec) locs, err2 = t.client.FindLocation(ctx.Scope, locspec)
if err2 != nil { if err2 != nil {
return err return err
} }
@ -996,11 +1064,11 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error {
} }
func breakpoint(t *Term, ctx callContext, args string) error { func breakpoint(t *Term, ctx callContext, args string) error {
return setBreakpoint(t, false, args) return setBreakpoint(t, ctx, false, args)
} }
func tracepoint(t *Term, ctx callContext, args string) error { func tracepoint(t *Term, ctx callContext, args string) error {
return setBreakpoint(t, true, args) return setBreakpoint(t, ctx, true, args)
} }
func printVar(t *Term, ctx callContext, args string) error { func printVar(t *Term, ctx callContext, args string) error {
@ -1228,7 +1296,7 @@ func parseStackArgs(argstr string) (stackArgs, error) {
func listCommand(t *Term, ctx callContext, args string) error { func listCommand(t *Term, ctx callContext, args string) error {
switch { switch {
case len(args) == 0 && ctx.Prefix != scopePrefix: case len(args) == 0 && !ctx.scoped():
state, err := t.client.GetState() state, err := t.client.GetState()
if err != nil { if err != nil {
return err return err
@ -1236,7 +1304,7 @@ func listCommand(t *Term, ctx callContext, args string) error {
printcontext(t, state) printcontext(t, state)
return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
case len(args) == 0 && ctx.Prefix == scopePrefix: case len(args) == 0 && ctx.scoped():
locs, err := t.client.Stacktrace(ctx.Scope.GoroutineID, ctx.Scope.Frame, nil) locs, err := t.client.Stacktrace(ctx.Scope.GoroutineID, ctx.Scope.Frame, nil)
if err != nil { if err != nil {
return err return err

@ -343,10 +343,6 @@ func TestScopePrefix(t *testing.T) {
term.MustExec("c") term.MustExec("c")
term.AssertExecError("frame", "not enough arguments") 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(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("goroutine 9000 locals", "Unknown goroutine 9000")
@ -356,6 +352,26 @@ func TestScopePrefix(t *testing.T) {
term.AssertExec("frame 3 print n", "1\n") term.AssertExec("frame 3 print n", "1\n")
term.AssertExec("frame 4 print n", "0\n") term.AssertExec("frame 4 print n", "0\n")
term.AssertExecError("frame 5 print n", "could not find symbol value for n") term.AssertExecError("frame 5 print n", "could not find symbol value for n")
term.MustExec("frame 2")
term.AssertExec("print n", "2\n")
term.MustExec("frame 4")
term.AssertExec("print n", "0\n")
term.MustExec("down")
term.AssertExec("print n", "1\n")
term.MustExec("down 2")
term.AssertExec("print n", "3\n")
term.AssertExecError("down 2", "Invalid frame -1")
term.AssertExec("print n", "3\n")
term.MustExec("up 2")
term.AssertExec("print n", "1\n")
term.AssertExecError("up 100", "Invalid frame 103")
term.AssertExec("print n", "1\n")
term.MustExec("step")
term.AssertExecError("print n", "could not find symbol value for n")
term.MustExec("frame 2")
term.AssertExec("print n", "2\n")
}) })
} }