Files
visual_debugger/tests/fake_gdb.py
mynameisdeleted 9556aa08a7 Add unit tests for py_debug_graph module
This commit introduces a comprehensive suite of unit tests for the
py_debug_graph module, which is responsible for building debug graphs
in Python. The tests cover various scenarios including linked lists,
cycles, scalar values, and the handling of local and global variables
in the debugging context. Additionally, it verifies the behavior of
the debug_graph function and ensures that the builtins.root patch
works as intended. These tests aim to enhance the reliability and
robustness of the debugging functionality.
2026-07-09 19:31:14 -04:00

189 lines
5.5 KiB
Python

"""Minimal fake of the `gdb` Python API -- just enough surface for
debuggers/gdb_debug_graph.py to run its DWARF-walking logic against
synthetic Type/Value/Frame objects, without a real gdb process attached to
a real binary.
Addresses are plain ints supplied by the test (not derived from id()), so
a test can deliberately construct two differently-typed pointers ("const
int*" and "Node<int>*") that happen to share the same address -- exactly
the "top points at the first member of the node it coincides with"
scenario gdb_debug_graph.py's _visit_pointer() is built to handle.
"""
TYPE_CODE_STRUCT = 1
TYPE_CODE_UNION = 2
TYPE_CODE_PTR = 3
TYPE_CODE_INT = 4
TYPE_CODE_CHAR = 5
TYPE_CODE_ARRAY = 6
class error(Exception): # noqa: N801 - matches gdb's own (lowercase) exception name
pass
class GdbError(Exception):
pass
class Field:
def __init__(self, name, artificial=False, is_base_class=False):
self.name = name
self.artificial = artificial
self.is_base_class = is_base_class
class Type:
def __init__(self, code, tag=None, fields=None, target=None, sizeof=8, display=None):
self.code = code
self.tag = tag
self._fields = fields or []
self._target = target
self.sizeof = sizeof
self._display = display if display is not None else (tag or "<type>")
def fields(self):
return self._fields
def strip_typedefs(self):
return self
def target(self):
if self._target is None:
raise error("type has no target (not a pointer/array)")
return self._target
def __str__(self):
return self._display
class Value:
"""`address` is this value's own location (meaningful for struct/union
values, read via `.address`); `ptr_int`/`pointee` describe what a
*pointer*-typed value points at (read via `int(value)`/`.dereference()`).
A struct Value and a "pointer to that struct" Value are two separate
objects the test wires together explicitly, same as real gdb."""
def __init__(self, type_, *, address=None, ptr_int=None, pointee=None, fields=None, py_value=None):
self.type = type_
self._address = address
self._ptr_int = ptr_int
self._pointee = pointee
self._fields = fields or {}
self._py_value = py_value
def __getitem__(self, name):
try:
return self._fields[name]
except KeyError:
raise error("there is no member named %s" % name)
def __int__(self):
if self._ptr_int is None:
raise error("value is not a pointer-like value")
return self._ptr_int
@property
def address(self):
if self._address is None:
raise error("value has no address")
return Value(Type(TYPE_CODE_PTR, target=self.type), ptr_int=self._address, pointee=self)
def dereference(self):
if self._pointee is None:
raise error("attempt to dereference a generic pointer")
return self._pointee
def string(self, length=None):
if not isinstance(self._py_value, str):
raise error("value is not a string")
return self._py_value if length is None else self._py_value[:length]
def __str__(self):
return str(self._py_value)
# ---- pretty-printer support ----
def default_visualizer(val):
"""Overridden per-test via monkeypatch; no printer by default."""
return None
# ---- frame/block/symbol support (for local/global auto-discovery) ----
class Symbol:
def __init__(self, name, value, is_variable=True, is_argument=False):
self.name = name
self._value = value
self.is_variable = is_variable
self.is_argument = is_argument
def value(self, frame):
return self._value
class Block:
def __init__(self, symbols=(), superblock=None, is_global=False, is_static=False):
self._symbols = list(symbols)
self.superblock = superblock
self.is_global = is_global
self.is_static = is_static
def __iter__(self):
return iter(self._symbols)
class Symtab:
def __init__(self, global_block=None, static_block=None):
self._global_block = global_block
self._static_block = static_block
def global_block(self):
return self._global_block
def static_block(self):
return self._static_block
class Sal:
def __init__(self, symtab):
self.symtab = symtab
class Frame:
def __init__(self, block, symtab=None):
self._block = block
self._symtab = symtab
def block(self):
return self._block
def find_sal(self):
return Sal(self._symtab)
def selected_frame():
"""Overridden per-test via monkeypatch; no frame by default."""
raise error("No frame selected")
def parse_and_eval(expr):
"""Approximates gdb's expression parser for exactly the one shape
_as_debugger_string() ever produces: a double-quoted, backslash-escaped
string literal. Returns a Value whose .string() recovers the original
(unescaped) text, like a real char* gdb builds from that literal would."""
if not (expr.startswith('"') and expr.endswith('"') and len(expr) >= 2):
raise error("unsupported fake parse_and_eval input: %r" % expr)
inner = expr[1:-1]
unescaped = inner.replace('\\"', '"').replace("\\\\", "\\")
return Value(Type(TYPE_CODE_PTR, target=Type(TYPE_CODE_CHAR, sizeof=1)), py_value=unescaped)
pretty_printers = []
class Function:
def __init__(self, name):
self._name = name