// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause //go:build linux && amd64 package debug import ( "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, nil) 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) } } } // TestConditionEvalMem covers the register-memory form: the value is read // through the supplied reader, and a missing or failing reader must not // block the breakpoint. func TestConditionEvalMem(t *testing.T) { regs := &Regs{RAX: 7} mem := func(addr uint64) (uint64, bool) { if addr == 0x5000 { return 7, true } return 0, false } eq := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000} if !eq.Eval(regs, mem) { t.Error("register-memory comparison with matching word should hold") } ne := Condition{Reg: "rax", Op: "!=", MemAddr: 0x5000} if ne.Eval(regs, mem) { t.Error("register-memory comparison with mismatching word should not hold") } bad := Condition{Reg: "rax", Op: "==", MemAddr: 0x6000} if !bad.Eval(regs, mem) { t.Error("unreadable memory must not block the breakpoint") } noReader := Condition{Reg: "rax", Op: "==", MemAddr: 0x5000} if !noReader.Eval(regs, nil) { t.Error("missing memory reader must not block the breakpoint") } } 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 TestWatchpointSlotTracking(t *testing.T) { s := &Session{} // per-session slots start free // All four slots are free initially. for i := range 4 { if s.IsWatchpointSlotUsed(i) { t.Errorf("slot %d should be free initially", i) } } if got := s.FindFreeWatchpointSlot(); got != 0 { t.Errorf("FindFreeWatchpointSlot() = %d, want 0", got) } // Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint). s.wpSlots[0] = true s.wpSlots[2] = true if !s.IsWatchpointSlotUsed(0) { t.Error("slot 0 should be in use") } if s.IsWatchpointSlotUsed(1) { t.Error("slot 1 should be free") } if !s.IsWatchpointSlotUsed(2) { t.Error("slot 2 should be in use") } if s.IsWatchpointSlotUsed(3) { t.Error("slot 3 should be free") } if got := s.FindFreeWatchpointSlot(); got != 1 { t.Errorf("FindFreeWatchpointSlot() = %d, want 1", got) } // Out-of-range slot queries return false. if s.IsWatchpointSlotUsed(-1) { t.Error("slot -1 should be reported as free (out of range)") } if s.IsWatchpointSlotUsed(4) { t.Error("slot 4 should be reported as free (out of range)") } // Mark all slots used: FindFreeWatchpointSlot returns -1. for i := range 4 { s.wpSlots[i] = true } if got := s.FindFreeWatchpointSlot(); got != -1 { t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got) } } // TestUnwatchSlotBound checks the bound the REPL parses against: it must // cover the architecture's whole slot range, not a hardcoded 0-3. func TestUnwatchSlotBound(t *testing.T) { max := maxWatchpoints() if max < 4 { t.Fatalf("maxWatchpoints() = %d, want at least 4", max) } s := &Session{} if s.IsWatchpointSlotUsed(max - 1) { t.Errorf("slot %d should be free initially", max-1) } if s.IsWatchpointSlotUsed(max) { t.Errorf("slot %d must be out of range", max) } }