// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package debug import ( "strings" "testing" ) func TestConditionEval(t *testing.T) { regs := &Regs{ RAX: 42, RBX: 0, RCX: 100, RIP: 0x1000, RSP: 0x2000, R8: 8, R15: 15, } tests := []struct { cond Condition want bool }{ {Condition{Reg: "rax", Op: "==", Value: 42}, true}, {Condition{Reg: "rax", Op: "==", Value: 43}, false}, {Condition{Reg: "rax", Op: "!=", Value: 43}, true}, {Condition{Reg: "rax", Op: "!=", Value: 42}, false}, {Condition{Reg: "rax", Op: "<", Value: 50}, true}, {Condition{Reg: "rax", Op: "<", Value: 40}, false}, {Condition{Reg: "rax", Op: ">", Value: 40}, true}, {Condition{Reg: "rax", Op: ">", Value: 50}, false}, {Condition{Reg: "rax", Op: "<=", Value: 42}, true}, {Condition{Reg: "rax", Op: ">=", Value: 42}, true}, {Condition{Reg: "rbx", Op: "==", Value: 0}, true}, {Condition{Reg: "rcx", Op: ">", Value: 50}, true}, {Condition{Reg: "rip", Op: "==", Value: 0x1000}, true}, {Condition{Reg: "rsp", Op: ">", Value: 0x1000}, true}, {Condition{Reg: "r8", Op: "==", Value: 8}, true}, {Condition{Reg: "r15", Op: "==", Value: 15}, true}, {Condition{Reg: "eax", Op: "==", Value: 42}, true}, // 32-bit alias {Condition{Reg: "ax", Op: "==", Value: 42}, true}, // 16-bit alias {Condition{Reg: "unknown", Op: "==", Value: 0}, true}, // unknown reg → don't block {Condition{Reg: "rax", Op: "??", Value: 0}, true}, // unknown op → don't block } for _, tt := range tests { got := tt.cond.Eval(regs) if got != tt.want { t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v", tt.cond.Reg, tt.cond.Op, tt.cond.Value, got, tt.want) } } } 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 TestDecodeRflags(t *testing.T) { tests := []struct { flags uint64 want string }{ {0x202, "IF"}, // only IF set (bit 9) {0x246, "PF ZF IF"}, // PF(2) + ZF(6) + IF(9) {0x001, "CF"}, // carry flag {0x080, "SF"}, // sign flag {0x800, "OF"}, // overflow flag {0x000, "none"}, // no flags {0x202 | 0x001, "CF IF"}, // CF + IF {0x3F7, "CF PF AF ZF SF TF IF"}, // all arithmetic flags } for _, tt := range tests { got := decodeRflags(tt.flags) if got != tt.want { t.Errorf("decodeRflags(%#x) = %q, want %q", tt.flags, 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 INT3. if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 { t.Errorf("pokes = %v", tr.pokes) } // 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") } } 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) } }