// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux package debug // Architecture-neutral tests: label and line tables, and the breakpoint // manager against the mock tracer. These do not launch a debuggee, so they // build on every supported linux architecture. import ( "strings" "testing" ) func TestLineAt(t *testing.T) { lines := []SourceLine{ {Offset: 0, Line: 5}, {Offset: 5, Line: 6}, {Offset: 10, Line: 7}, {Offset: 15, Line: 8}, } tests := []struct { offset int want int }{ {0, 5}, {1, 5}, {4, 5}, {5, 6}, {7, 6}, {10, 7}, {12, 7}, {15, 8}, {20, 8}, } for _, tt := range tests { got := lineAt(lines, tt.offset) if got != tt.want { t.Errorf("lineAt(lines, %d) = %d, want %d", tt.offset, got, tt.want) } } // Empty table. if lineAt(nil, 5) != 0 { t.Error("lineAt(nil, 5) should return 0") } } func TestOffsetForLine(t *testing.T) { lines := []SourceLine{ {Offset: 0, Line: 5}, {Offset: 5, Line: 6}, {Offset: 10, Line: 7}, } tests := []struct { line int want int }{ {5, 0}, {6, 5}, {7, 10}, {99, -1}, // not found {0, -1}, // not found } for _, tt := range tests { got := offsetForLine(lines, tt.line) if got != tt.want { t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want) } } } func TestNearestLabel(t *testing.T) { labels := []Label{ {Name: "start", Offset: 0}, {Name: "loop", Offset: 10}, {Name: "done", Offset: 20}, } tests := []struct { offset int want string }{ {0, "start"}, {5, "start"}, {10, "loop"}, {15, "loop"}, {20, "done"}, {25, "done"}, } for _, tt := range tests { got := nearestLabel(labels, tt.offset) if got != tt.want { t.Errorf("nearestLabel(labels, %d) = %q, want %q", tt.offset, got, tt.want) } } } func TestBreakpointsSetAndClear(t *testing.T) { tr := newMockTracer() bm := NewBreakpoints(tr) // Set a breakpoint at address 0x1000. bp, err := bm.Set(0x1000, "test") if err != nil { t.Fatalf("Set: %v", err) } if !bp.Enabled { t.Error("breakpoint not enabled") } if bp.Label != "test" { t.Errorf("label = %q, want test", bp.Label) } // Verify Peek was called. if len(tr.peeks) != 1 || tr.peeks[0] != 0x1000 { t.Errorf("peeks = %v, want [0x1000]", tr.peeks) } // Verify Poke wrote the breakpoint instruction's bytes. if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 { t.Errorf("pokes = %v", tr.pokes) } if got := tr.pokes[0].val & breakpointMask(); got != breakpointWord(breakpointInsn) { t.Errorf("patched bytes %#x, want %#x", got, breakpointWord(breakpointInsn)) } // At should find it. if bm.At(0x1000) == nil { t.Error("At(0x1000) returned nil") } // All should return it. all := bm.All() if len(all) != 1 { t.Errorf("All() = %d breakpoints, want 1", len(all)) } // Clear it. if err := bm.Clear(0x1000); err != nil { t.Fatalf("Clear: %v", err) } if bm.At(0x1000) != nil { t.Error("At(0x1000) after Clear should be nil") } } // TestBreakpointRestoreWidth proves the restore path writes back every // byte of the breakpoint instruction's width, not just the first byte: on // arm64, riscv64 and loong64 the instruction is four bytes, and restoring // one byte would leave three bytes of the trap instruction in place. func TestBreakpointRestoreWidth(t *testing.T) { tr := newMockTracer() bm := NewBreakpoints(tr) tr.mem[0x3000] = 0x11 tr.mem[0x3001] = 0x22 tr.mem[0x3002] = 0x33 tr.mem[0x3003] = 0x44 if _, err := bm.Set(0x3000, "width"); err != nil { t.Fatalf("Set: %v", err) } for i, b := range breakpointInsn { if tr.mem[0x3000+uint64(i)] != b { t.Fatalf("byte %d after Set = %#x, want the breakpoint byte %#x", i, tr.mem[0x3000+uint64(i)], b) } } if len(bm.At(0x3000).Orig) != len(breakpointInsn) { t.Fatalf("Orig holds %d bytes, want %d", len(bm.At(0x3000).Orig), len(breakpointInsn)) } if err := bm.Clear(0x3000); err != nil { t.Fatalf("Clear: %v", err) } want := []byte{0x11, 0x22, 0x33, 0x44} for i, b := range want { if tr.mem[0x3000+uint64(i)] != b { t.Errorf("byte %d after Clear = %#x, want %#x (restore must cover the full instruction width)", i, tr.mem[0x3000+uint64(i)], b) } } } func TestBreakpointsSetWithCond(t *testing.T) { tr := newMockTracer() bm := NewBreakpoints(tr) cond := &Condition{Reg: "rax", Op: "==", Value: 42} bp, err := bm.SetWithCond(0x2000, "cond_test", cond) if err != nil { t.Fatalf("SetWithCond: %v", err) } if bp.Cond == nil || bp.Cond.Value != 42 { t.Error("condition not set") } // Re-setting the same address should update the condition. cond2 := &Condition{Reg: "rbx", Op: "<", Value: 100} bp2, err := bm.SetWithCond(0x2000, "cond_test2", cond2) if err != nil { t.Fatalf("SetWithCond (update): %v", err) } if bp2.Cond.Value != 100 { t.Error("condition not updated") } // Should have only 1 Peek (first Set), second is update (no Peek needed). if len(tr.peeks) != 1 { t.Errorf("expected 1 Peek, got %d", len(tr.peeks)) } } func TestBreakpointsClearAll(t *testing.T) { tr := newMockTracer() bm := NewBreakpoints(tr) bm.Set(0x1000, "a") bm.Set(0x2000, "b") bm.Set(0x3000, "c") if len(bm.All()) != 3 { t.Fatalf("expected 3 breakpoints, got %d", len(bm.All())) } bm.ClearAll() if len(bm.All()) != 0 { t.Errorf("ClearAll: expected 0 breakpoints, got %d", len(bm.All())) } } func TestBreakpointInfo(t *testing.T) { tr := newMockTracer() bm := NewBreakpoints(tr) bm.Set(0x4000, "info_test") info := bm.Info() if info == "" { t.Error("Info returned empty string") } if !strings.Contains(info, "info_test") { t.Errorf("Info %q does not contain label", info) } } // TestConditionString covers the display of all three condition forms. func TestConditionString(t *testing.T) { tests := []struct { cond Condition want string }{ {Condition{Reg: "rax", Op: "==", Value: 42}, "rax == 0x2a"}, {Condition{Reg: "rax", Op: "!=", Reg2: "rbx"}, "rax != rbx"}, {Condition{Reg: "rax", Op: "<", MemAddr: 0x5000}, "rax < *0x5000"}, } for _, tt := range tests { if got := tt.cond.String(); got != tt.want { t.Errorf("Condition.String() = %q, want %q", got, tt.want) } } }