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.
189 lines
5.5 KiB
Python
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
|