diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index c9f192630a..398a35a1ad 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -211,7 +211,7 @@ Set breakpoint condition. Specifies that the breakpoint, tracepoint or watchpoint should break only if the boolean expression is true. -See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Documentation/cli/expr.md) for a description of supported expressions. +See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Documentation/cli/expr.md) for a description of supported expressions and [Documentation/cli/cond.md](//github.com/go-delve/delve/tree/master/Documentation/cli/cond.md) for a description of how breakpoint conditions are evaluated. With the -hitcount option a condition on the breakpoint hit count can be set, the following operators are supported diff --git a/Documentation/cli/cond.md b/Documentation/cli/cond.md new file mode 100644 index 0000000000..46d962b9af --- /dev/null +++ b/Documentation/cli/cond.md @@ -0,0 +1,15 @@ +# Breakpoint conditions + +Breakpoints have two conditions: + +* The normal condition, which is specified using the command `cond ` (or by setting the Cond field when amending a breakpoint via the API), is any [expression](expr.md) which evaluates to true or false. +* The hitcount condition, which is specified `cond -hitcount ` (or by setting the HitCond field when amending a breakpoint via the API), is a constraint on the number of times the breakpoint has been hit. + +When a breakpoint location is encountered during the execution of the program, the debugger will: + +* Evaluate the normal condition +* Stop if there is an error while evaluating the normal condition +* If the normal condition evaluates to true the hit count is incremented +* Evaluate the hitcount condition +* If the hitcount condition is also satisfied stop the execution at the breakpoint + diff --git a/Documentation/cli/expr.md b/Documentation/cli/expr.md index a2b9399db0..b8202dd194 100644 --- a/Documentation/cli/expr.md +++ b/Documentation/cli/expr.md @@ -119,6 +119,7 @@ Delve defines two special variables: * `runtime.curg` evaluates to the 'g' struct for the current goroutine, in particular `runtime.curg.goid` is the goroutine id of the current goroutine. * `runtime.frameoff` is the offset of the frame's base address from the bottom of the stack. +* `runtime.bphitcount[X]` is the total hitcount for breakpoint X, which can be either an ID or the breakpoint name as a string. ## Access to variables from previous frames diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 006a00642e..119bdf56fe 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -340,3 +340,25 @@ var = eval( {"FollowPointers":True, "MaxVariableRecurse":2, "MaxStringLen":100, "MaxArrayValues":10, "MaxStructFields":100} ) ``` + +## Chain breakpoints + +Chain a number of breakpoints such that breakpoint n+1 is only hit after breakpoint n is hit: + +```python +def command_breakchain(*args): + v = args.split(" ") + + bp = get_breakpoint(int(v[0]), "").Breakpoint + bp.HitCond = "== 1" + amend_breakpoint(bp) + + for i in range(1, len(v)): + bp = get_breakpoint(int(v[i]), "").Breakpoint + if i != len(v)-1: + bp.HitCond = "== 1" + bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0" + amend_breakpoint(bp) +``` + +To be used as `chain 1 2 3` where `1`, `2`, and `3` are IDs of breakpoints to chain together. diff --git a/_fixtures/bphitcountchain.go b/_fixtures/bphitcountchain.go new file mode 100644 index 0000000000..04b71cf65a --- /dev/null +++ b/_fixtures/bphitcountchain.go @@ -0,0 +1,31 @@ +package main + +import "fmt" + +func breakfunc1() { + fmt.Println("breakfunc1") +} + +func breakfunc2() { + fmt.Println("breakfunc2") +} + +func breakfunc3() { + fmt.Println("breakfunc3") +} + +func main() { + breakfunc2() + breakfunc3() + + breakfunc1() // hit + breakfunc3() + breakfunc1() + + breakfunc2() // hit + breakfunc1() + + breakfunc3() // hit + breakfunc1() + breakfunc2() +} diff --git a/_fixtures/chain_breakpoints.star b/_fixtures/chain_breakpoints.star new file mode 100644 index 0000000000..4902f7d03e --- /dev/null +++ b/_fixtures/chain_breakpoints.star @@ -0,0 +1,13 @@ +def command_chain(args): + v = args.split(" ") + + bp = get_breakpoint(int(v[0]), "").Breakpoint + bp.HitCond = "== 1" + amend_breakpoint(bp) + + for i in range(1, len(v)): + bp = get_breakpoint(int(v[i]), "").Breakpoint + if i != len(v)-1: + bp.HitCond = "== 1" + bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0" + amend_breakpoint(bp) diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go index 596eefc455..93aa666256 100644 --- a/pkg/proc/breakpoints.go +++ b/pkg/proc/breakpoints.go @@ -11,11 +11,13 @@ import ( "go/printer" "go/token" "reflect" + "strconv" "github.com/go-delve/delve/pkg/dwarf/godwarf" "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/dwarf/reader" "github.com/go-delve/delve/pkg/goversion" + "github.com/go-delve/delve/pkg/proc/evalop" "github.com/go-delve/delve/pkg/proc/internal/ebpf" ) @@ -920,6 +922,24 @@ func (bpmap *BreakpointMap) HasHWBreakpoints() bool { return false } +func totalHitCountByName(lbpmap map[int]*LogicalBreakpoint, s string) (uint64, error) { + for _, bp := range lbpmap { + if bp.Name == s { + return bp.TotalHitCount, nil + } + } + return 0, fmt.Errorf("could not find breakpoint named %q", s) +} + +func totalHitCountByID(lbpmap map[int]*LogicalBreakpoint, id int) (uint64, error) { + for _, bp := range lbpmap { + if bp.LogicalID == int(id) { + return bp.TotalHitCount, nil + } + } + return 0, fmt.Errorf("could not find breakpoint with ID = %d", id) +} + // BreakpointState describes the state of a breakpoint in a thread. type BreakpointState struct { *Breakpoint @@ -1063,6 +1083,8 @@ type LogicalBreakpoint struct { // condSatisfiable is true when 'cond && hitCond' can potentially be true. condSatisfiable bool + // condUsesHitCounts is true when 'cond' uses breakpoint hitcounts + condUsesHitCounts bool UserData interface{} // Any additional information about the breakpoint // Name of root function from where tracing needs to be done @@ -1105,15 +1127,135 @@ func (lbp *LogicalBreakpoint) Cond() string { return buf.String() } -func breakpointConditionSatisfiable(lbp *LogicalBreakpoint) bool { - if lbp.hitCond == nil || lbp.HitCondPerG { +func breakpointConditionSatisfiable(lbpmap map[int]*LogicalBreakpoint, lbp *LogicalBreakpoint) bool { + if lbp.hitCond != nil && !lbp.HitCondPerG { + switch lbp.hitCond.Op { + case token.EQL, token.LEQ: + if int(lbp.TotalHitCount) >= lbp.hitCond.Val { + return false + } + case token.LSS: + if int(lbp.TotalHitCount) >= lbp.hitCond.Val-1 { + return false + } + } + } + if !lbp.condUsesHitCounts { return true } - switch lbp.hitCond.Op { - case token.EQL, token.LEQ: - return int(lbp.TotalHitCount) < lbp.hitCond.Val - case token.LSS: - return int(lbp.TotalHitCount) < lbp.hitCond.Val-1 + + toint := func(x ast.Expr) (uint64, bool) { + lit, ok := x.(*ast.BasicLit) + if !ok || lit.Kind != token.INT { + return 0, false + } + n, err := strconv.Atoi(lit.Value) + return uint64(n), err == nil && n >= 0 } - return true + + hitcountexpr := func(x ast.Expr) (uint64, bool) { + idx, ok := x.(*ast.IndexExpr) + if !ok { + return 0, false + } + selx, ok := idx.X.(*ast.SelectorExpr) + if !ok { + return 0, false + } + ident, ok := selx.X.(*ast.Ident) + if !ok || ident.Name != "runtime" || selx.Sel.Name != evalop.BreakpointHitCountVarName { + return 0, false + } + lit, ok := idx.Index.(*ast.BasicLit) + if !ok { + return 0, false + } + switch lit.Kind { + case token.INT: + n, _ := strconv.Atoi(lit.Value) + thc, err := totalHitCountByID(lbpmap, n) + return thc, err == nil + case token.STRING: + v, _ := strconv.Unquote(lit.Value) + thc, err := totalHitCountByName(lbpmap, v) + return thc, err == nil + default: + return 0, false + } + } + + var satisf func(n ast.Node) bool + satisf = func(n ast.Node) bool { + parexpr, ok := n.(*ast.ParenExpr) + if ok { + return satisf(parexpr.X) + } + binexpr, ok := n.(*ast.BinaryExpr) + if !ok { + return true + } + switch binexpr.Op { + case token.AND: + return satisf(binexpr.X) && satisf(binexpr.Y) + case token.OR: + if !satisf(binexpr.X) { + return false + } + if !satisf(binexpr.Y) { + return false + } + return true + case token.EQL, token.LEQ, token.LSS, token.NEQ, token.GTR, token.GEQ: + default: + return true + } + + hitcount, ok1 := hitcountexpr(binexpr.X) + val, ok2 := toint(binexpr.Y) + if !ok1 || !ok2 { + return true + } + + switch binexpr.Op { + case token.EQL: + return hitcount == val + case token.LEQ: + return hitcount <= val + case token.LSS: + return hitcount < val + case token.NEQ: + return hitcount != val + case token.GTR: + return hitcount > val + case token.GEQ: + return hitcount >= val + } + return true + } + + return satisf(lbp.cond) +} + +func breakpointConditionUsesHitCounts(lbp *LogicalBreakpoint) bool { + if lbp.cond == nil { + return false + } + r := false + ast.Inspect(lbp.cond, func(n ast.Node) bool { + if r { + return false + } + seln, ok := n.(*ast.SelectorExpr) + if ok { + ident, ok := seln.X.(*ast.Ident) + if ok { + if ident.Name == "runtime" && seln.Sel.Name == evalop.BreakpointHitCountVarName { + r = true + return false + } + } + } + return true + }) + return r } diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index 770204758f..ba2b79a990 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -1240,6 +1240,9 @@ func (stack *evalStack) executeOp() { case *evalop.PushDebugPinner: stack.push(stack.debugPinner) + case *evalop.PushBreakpointHitCount: + stack.push(newVariable("runtime."+evalop.BreakpointHitCountVarName, fakeAddressUnresolv, godwarf.FakeSliceType(godwarf.FakeBasicType("uint", 64)), scope.BinInfo, scope.Mem)) + default: stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op) } @@ -2064,6 +2067,33 @@ func (scope *EvalScope) evalIndex(op *evalop.Index, stack *evalStack) { return } + if xev.Name == "runtime."+evalop.BreakpointHitCountVarName { + if idxev.Kind == reflect.String { + s := constant.StringVal(idxev.Value) + thc, err := totalHitCountByName(scope.target.Breakpoints().Logical, s) + if err == nil { + stack.push(newConstant(constant.MakeUint64(thc), scope.Mem)) + } + stack.err = err + return + } + n, err := idxev.asInt() + if err != nil { + n2, err := idxev.asUint() + if err != nil { + stack.err = fmt.Errorf("can not index %s with %s", xev.Name, exprToString(op.Node.Index)) + return + } + n = int64(n2) + } + thc, err := totalHitCountByID(scope.target.Breakpoints().Logical, int(n)) + if err == nil { + stack.push(newConstant(constant.MakeUint64(thc), scope.Mem)) + } + stack.err = err + return + } + if xev.Flags&VariableCPtr == 0 { xev = xev.maybeDereference() } diff --git a/pkg/proc/evalop/evalcompile.go b/pkg/proc/evalop/evalcompile.go index af3f92999e..ce03244dc0 100644 --- a/pkg/proc/evalop/evalcompile.go +++ b/pkg/proc/evalop/evalcompile.go @@ -18,8 +18,12 @@ import ( ) var ( - ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'") - DebugPinnerFunctionName = "runtime.debugPinnerV1" + ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'") +) + +const ( + BreakpointHitCountVarName = "bphitcount" + DebugPinnerFunctionName = "runtime.debugPinnerV1" ) type compileCtx struct { @@ -276,6 +280,9 @@ func (ctx *compileCtx) compileAST(t ast.Expr) error { case x.Name == "runtime" && node.Sel.Name == "rangeParentOffset": ctx.pushOp(&PushRangeParentOffset{}) + case x.Name == "runtime" && node.Sel.Name == BreakpointHitCountVarName: + ctx.pushOp(&PushBreakpointHitCount{}) + default: ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name}) } diff --git a/pkg/proc/evalop/ops.go b/pkg/proc/evalop/ops.go index d64c099545..69b8b11a59 100644 --- a/pkg/proc/evalop/ops.go +++ b/pkg/proc/evalop/ops.go @@ -324,3 +324,10 @@ type PushPinAddress struct { } func (*PushPinAddress) depthCheck() (npop, npush int) { return 0, 1 } + +// PushBreakpointHitCount pushes a special array containing the hit counts +// of breakpoints. +type PushBreakpointHitCount struct { +} + +func (*PushBreakpointHitCount) depthCheck() (npop, npush int) { return 0, 1 } diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index be59bde6e8..5258498a63 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -98,6 +98,17 @@ func withTestProcessArgs(name string, t testing.TB, wd string, args []string, bu buildFlags |= protest.BuildModePIE } fixture := protest.BuildFixture(name, buildFlags) + + grp := startTestProcessArgs(fixture, t, wd, args) + + defer func() { + grp.Detach(true) + }() + + fn(grp.Selected, grp, fixture) +} + +func startTestProcessArgs(fixture protest.Fixture, t testing.TB, wd string, args []string) *proc.TargetGroup { var grp *proc.TargetGroup var err error var tracedir string @@ -118,12 +129,7 @@ func withTestProcessArgs(name string, t testing.TB, wd string, args []string, bu if err != nil { t.Fatal("Launch():", err) } - - defer func() { - grp.Detach(true) - }() - - fn(grp.Selected, grp, fixture) + return grp } func getRegisters(p *proc.Target, t *testing.T) proc.Registers { @@ -250,6 +256,7 @@ func setFunctionBreakpoint(p *proc.Target, t testing.TB, fname string) *proc.Bre if err != nil { t.Fatalf("FindFunctionLocation(%s): %v", fname, err) } + bp.Logical.Set.FunctionName = fname return bp } @@ -5562,3 +5569,98 @@ func TestStackwatchClearBug(t *testing.T) { } }) } + +func TestChainedBreakpoint(t *testing.T) { + assertCallerLine := func(t *testing.T, p *proc.Target, pos string, tgt int) { + t.Helper() + frames, err := proc.ThreadStacktrace(p, p.CurrentThread(), 5) + assertNoError(err, t, "ThreadStacktrace") + t.Logf("%s: %s:%d", pos, frames[1].Call.File, frames[1].Call.Line) + if frames[1].Call.Line != tgt { + t.Fatalf("wrong line number, expected %d", tgt) + } + } + + withTestProcess("bphitcountchain", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + numphys := func(lbp *proc.LogicalBreakpoint) int { + count := 0 + for _, bp := range p.Breakpoints().M { + if bp.LogicalID() == lbp.LogicalID { + count++ + } + } + return count + } + + bp := setFunctionBreakpoint(p, t, "main.breakfunc3") + lbp3 := bp.Logical + bp = setFunctionBreakpoint(p, t, "main.breakfunc2") + lbp2 := bp.Logical + bp = setFunctionBreakpoint(p, t, "main.breakfunc1") + lbp1 := bp.Logical + + assertPhysCount := func(lbp1cnt, lbp2cnt, lbp3cnt int) { + t.Helper() + t.Logf("lbp1: %d lbp2: %d lbp3: %d", numphys(lbp1), numphys(lbp2), numphys(lbp3)) + if numphys(lbp1) != lbp1cnt || numphys(lbp2) != lbp2cnt || numphys(lbp3) != lbp3cnt { + t.Fatal("wrong number of physical breakpoints") + } + } + + assertNoError(grp.ChangeBreakpointCondition(lbp1, "", "== 1", false), t, "ChangeBreakpointCondition") + assertNoError(grp.ChangeBreakpointCondition(lbp2, fmt.Sprintf("runtime.bphitcount[%d] > 0", lbp1.LogicalID), "== 1", false), t, "ChangeBreakpointCondition") + assertNoError(grp.ChangeBreakpointCondition(lbp3, fmt.Sprintf("runtime.bphitcount[%d] > 0", lbp2.LogicalID), "== 1", false), t, "ChangeBreakpointCondition") + + assertPhysCount(1, 0, 0) + + assertNoError(grp.Continue(), t, "Continue 1") + assertCallerLine(t, p, "continue 1", 21) + + assertNoError(grp.Continue(), t, "Continue 2") + assertCallerLine(t, p, "continue 2", 25) + + assertPhysCount(0, 1, 0) + + assertNoError(grp.Continue(), t, "Continue 3") + assertCallerLine(t, p, "continue 3", 28) + + assertPhysCount(0, 0, 1) + + err := grp.Continue() + if !errors.As(err, &proc.ErrProcessExited{}) { + assertNoError(err, t, "Continue 4") + } + + // === Restart === + + t.Logf("=== Restart ===") + + grp2 := startTestProcessArgs(fixture, t, ".", []string{}) + proc.Restart(grp2, grp, func(lbp *proc.LogicalBreakpoint, err error) { + t.Fatalf("discarded logical breakpoint %v: %v", lbp, err) + }) + + grp = grp2 + p = grp.Selected + + assertPhysCount(1, 0, 0) + + assertNoError(grp.Continue(), t, "Continue 1") + assertCallerLine(t, p, "continue 1", 21) + + assertNoError(grp.Continue(), t, "Continue 2") + assertCallerLine(t, p, "continue 2", 25) + + assertPhysCount(0, 1, 0) + + assertNoError(grp.Continue(), t, "Continue 3") + assertCallerLine(t, p, "continue 3", 28) + + assertPhysCount(0, 0, 1) + + err = grp.Continue() + if !errors.As(err, &proc.ErrProcessExited{}) { + assertNoError(err, t, "Continue 4") + } + }) +} diff --git a/pkg/proc/target_group.go b/pkg/proc/target_group.go index b31a578f07..8ebec98a2c 100644 --- a/pkg/proc/target_group.go +++ b/pkg/proc/target_group.go @@ -71,6 +71,7 @@ func NewGroup(procgrp ProcessGroup, cfg NewTargetGroupConfig) (*TargetGroup, Add // Breakpoints that can not be set will be discarded, if discard is not nil // it will be called for each discarded breakpoint. func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) { + toenable := []*LogicalBreakpoint{} for _, bp := range oldgrp.LogicalBreakpoints { if _, ok := grp.LogicalBreakpoints[bp.LogicalID]; ok { continue @@ -80,14 +81,17 @@ func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) bp.HitCount = make(map[int64]uint64) bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart if bp.enabled { - bp.condSatisfiable = breakpointConditionSatisfiable(bp) - err := grp.enableBreakpoint(bp) - if err != nil { - if discard != nil { - discard(bp, err) - } - delete(grp.LogicalBreakpoints, bp.LogicalID) + toenable = append(toenable, bp) + } + } + for _, bp := range toenable { + bp.condSatisfiable = breakpointConditionSatisfiable(grp.LogicalBreakpoints, bp) + err := grp.enableBreakpoint(bp) + if err != nil { + if discard != nil { + discard(bp, err) } + delete(grp.LogicalBreakpoints, bp.LogicalID) } } if oldgrp.followExecEnabled { @@ -265,7 +269,7 @@ func (grp *TargetGroup) SetBreakpointEnabled(lbp *LogicalBreakpoint, enabled boo err = grp.disableBreakpoint(lbp) case !lbp.enabled && enabled: lbp.enabled = true - lbp.condSatisfiable = breakpointConditionSatisfiable(lbp) + lbp.condSatisfiable = breakpointConditionSatisfiable(grp.LogicalBreakpoints, lbp) err = grp.enableBreakpoint(lbp) } return @@ -424,12 +428,14 @@ func (grp *TargetGroup) ChangeBreakpointCondition(lbp *LogicalBreakpoint, cond, lbp.HitCondPerG = hitCondPerG } + lbp.condUsesHitCounts = breakpointConditionUsesHitCounts(lbp) + if lbp.enabled { switch { - case lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp): + case lbp.condSatisfiable && !breakpointConditionSatisfiable(grp.LogicalBreakpoints, lbp): lbp.condSatisfiable = false grp.disableBreakpoint(lbp) - case !lbp.condSatisfiable && breakpointConditionSatisfiable(lbp): + case !lbp.condSatisfiable && breakpointConditionSatisfiable(grp.LogicalBreakpoints, lbp): lbp.condSatisfiable = true grp.enableBreakpoint(lbp) } @@ -483,12 +489,18 @@ func parseHitCondition(hitCond string) (token.Token, int, error) { func (grp *TargetGroup) manageUnsatisfiableBreakpoints() error { for _, lbp := range grp.LogicalBreakpoints { if lbp.enabled { - if lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp) { + if lbp.condSatisfiable && !breakpointConditionSatisfiable(grp.LogicalBreakpoints, lbp) { lbp.condSatisfiable = false err := grp.disableBreakpoint(lbp) if err != nil { return err } + } else if lbp.condUsesHitCounts && !lbp.condSatisfiable && breakpointConditionSatisfiable(grp.LogicalBreakpoints, lbp) { + lbp.condSatisfiable = true + err := grp.enableBreakpoint(lbp) + if err != nil { + return err + } } } } diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index bab10d5a45..c513bb3581 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -1643,6 +1643,10 @@ func (v *Variable) loadSliceInfo(t *godwarf.SliceType) { } } + if v.Addr == fakeAddressUnresolv && v.fieldType == nil { + return + } + v.stride = v.fieldType.Size() if t, ok := v.fieldType.(*godwarf.PtrType); ok { v.stride = t.ByteSize diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 51d39b32e4..f28df194f3 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -514,7 +514,7 @@ The command 'on x -edit' can be used to edit the list of commands executed when Specifies that the breakpoint, tracepoint or watchpoint should break only if the boolean expression is true. -See Documentation/cli/expr.md for a description of supported expressions. +See Documentation/cli/expr.md for a description of supported expressions and Documentation/cli/cond.md for a description of how breakpoint conditions are evaluated. With the -hitcount option a condition on the breakpoint hit count can be set, the following operators are supported diff --git a/pkg/terminal/starlark_test.go b/pkg/terminal/starlark_test.go index 987b57bc7f..b4e9ab8845 100644 --- a/pkg/terminal/starlark_test.go +++ b/pkg/terminal/starlark_test.go @@ -139,6 +139,7 @@ func testStarlarkAmendBreakpoint(t *testing.T, term *FakeTerminal) { if !strings.Contains(out, "Stacktrace:2") || !strings.Contains(out, `HitCond:"== 2"`) { t.Fatalf("wrong output") } + term.MustExec("clear afuncbreak") } func TestStarlarkVariable(t *testing.T) { @@ -256,3 +257,46 @@ v.Children[0].Children[0].Value.XXX } }) } + +func TestStarlarkChainBreakpointsExample(t *testing.T) { + withTestTerminal("bphitcountchain", t, func(term *FakeTerminal) { + term.MustExec("source " + findStarFile("chain_breakpoints")) + term.MustExec("break main.breakfunc1") + term.MustExec("break main.breakfunc2") + term.MustExec("break main.breakfunc3") + term.MustExec("chain 1 2 3") + out := term.MustExec("breakpoints") + t.Log(out) + + numphys := func(id int) int { + bp, err := term.client.GetBreakpoint(id) + if err != nil { + t.Fatalf("Error getting breakpoint %d: %v", id, err) + } + return len(bp.Addrs) + } + + assertPhysCount := func(lbp1cnt, lbp2cnt, lbp3cnt int) { + t.Helper() + t.Logf("lbp1: %d lbp2: %d lbp3: %d", numphys(1), numphys(2), numphys(3)) + if numphys(1) != lbp1cnt || numphys(2) != lbp2cnt || numphys(3) != lbp3cnt { + t.Fatal("Wrong number of physical breakpoints") + } + } + + assertPhysCount(1, 0, 0) + + term.MustExec("continue") + listIsAt(t, term, "frame 1 list", 21, -1, -1) + + term.MustExec("continue") + listIsAt(t, term, "frame 1 list", 25, -1, -1) + + assertPhysCount(0, 1, 0) + + term.MustExec("continue") + listIsAt(t, term, "frame 1 list", 28, -1, -1) + + assertPhysCount(0, 0, 1) + }) +}