Files
visual_debugger/tests/test_gdb_debug_graph.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

834 lines
34 KiB
Python

"""Unit tests for debuggers/gdb_debug_graph.py, driven against a fake
`gdb` module (tests/fake_gdb.py) instead of a real gdb process -- see that
module's docstring for why addresses are plain test-supplied ints rather
than derived from id().
"""
import sys
import fake_gdb as fg
sys.modules["gdb"] = fg
import gdb_debug_graph as gg # noqa: E402
INT_T = fg.Type(fg.TYPE_CODE_INT, sizeof=4, display="int")
CHAR_T = fg.Type(fg.TYPE_CODE_CHAR, sizeof=1, display="char")
def make_node_types():
node_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Node<int>")
node_ptr_type = fg.Type(fg.TYPE_CODE_PTR, target=node_type, display="Node<int> *")
node_type._fields = [fg.Field("data"), fg.Field("next")]
return node_type, node_ptr_type
def make_node(node_type, addr, data, next_ptr_value):
return fg.Value(node_type, address=addr, fields={
"data": fg.Value(INT_T, py_value=data),
"next": next_ptr_value,
})
def ptr_to(node_ptr_type, addr, pointee):
return fg.Value(node_ptr_type, ptr_int=addr, pointee=pointee)
def null_ptr(ptr_type):
return fg.Value(ptr_type, ptr_int=0, pointee=None)
def build_chain():
"""30 -> 20 -> 10 -> None, matching the real cpp/rust demo list."""
node_type, node_ptr_type = make_node_types()
n10 = make_node(node_type, 0x10, "10", null_ptr(node_ptr_type))
n20 = make_node(node_type, 0x20, "20", ptr_to(node_ptr_type, 0x10, n10))
n30 = make_node(node_type, 0x30, "30", ptr_to(node_ptr_type, 0x20, n20))
return node_type, node_ptr_type, n30
def _fields(node_json):
return {f["name"]: f for f in node_json["fields"]}
# --- basic struct/pointer walk --------------------------------------------
def test_simple_chain_walk(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type, n30 = build_chain()
result_json = gg.build_graph_json(n30, [])
import json
data = json.loads(result_json)
assert data["kind"] == {"nodeTable": True}
node_shaped = [n for n in data["nodes"] if set(_fields(n)) == {"data", "next"}]
assert len(node_shaped) == 3
next_edges = [e for e in data["edges"] if e["label"] == "next"]
assert len(next_edges) == 2
assert node_shaped[0]["type"] == "Node<int>"
def test_null_pointer_produces_no_edge():
node_type, node_ptr_type, n30 = build_chain()
result_json = gg.build_graph_json(n30, [])
import json
data = json.loads(result_json)
n10 = next(n for n in data["nodes"] if _fields(n)["data"]["value"] == "10")
assert _fields(n10)["next"]["value"] == "0x0"
assert _fields(n10)["next"]["isPointer"] is True
# a null pointer target ("0x0") must never appear as an edge endpoint
assert not any(e["to"] == "0x0" for e in data["edges"])
def test_cycle_is_handled_without_infinite_recursion():
node_type, node_ptr_type = make_node_types()
# placeholders so we can wire a->b->a
a = fg.Value(node_type, address=0x1)
b = fg.Value(node_type, address=0x2)
a._fields = {"data": fg.Value(INT_T, py_value="1"), "next": ptr_to(node_ptr_type, 0x2, b)}
b._fields = {"data": fg.Value(INT_T, py_value="2"), "next": ptr_to(node_ptr_type, 0x1, a)}
result_json = gg.build_graph_json(a, [])
import json
data = json.loads(result_json)
assert len(data["nodes"]) == 2
assert len(data["edges"]) == 2
# --- char* handling ---------------------------------------------------------
def test_char_star_field_shows_full_string_not_one_byte():
node_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T, display="char *")
msg_ptr = fg.Value(char_ptr_t, ptr_int=0x900, py_value="hello world")
holder = fg.Value(node_type, address=0x800, fields={"message": msg_ptr})
node_type._fields = [fg.Field("message")]
result_json = gg.build_graph_json(holder, [])
import json
data = json.loads(result_json)
str_node = next(n for n in data["nodes"] if n["id"] == "0x900")
assert _fields(str_node)["value"]["value"] == "hello world"
def test_char_star_long_string_is_truncated_with_ellipsis():
node_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T, display="char *")
long_text = "x" * 600
msg_ptr = fg.Value(char_ptr_t, ptr_int=0x900, py_value=long_text)
holder = fg.Value(node_type, address=0x800, fields={"message": msg_ptr})
node_type._fields = [fg.Field("message")]
result_json = gg.build_graph_json(holder, [])
import json
data = json.loads(result_json)
str_node = next(n for n in data["nodes"] if n["id"] == "0x900")
value = _fields(str_node)["value"]["value"]
assert len(value) == 501 # 500 chars + ellipsis marker
assert value.endswith("…")
# --- pretty-printer summary (e.g. std::optional<T>) -------------------------
class _FakePrinter:
def __init__(self, children=None, text=None):
self._children = children or []
self._text = text
def children(self):
return self._children
def to_string(self):
return self._text
def test_pretty_printer_single_leaf_is_unwrapped(monkeypatch):
opt_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::optional<int>")
inner = fg.Value(INT_T, py_value="42")
engaged_val = fg.Value(opt_type, address=0x100, fields={})
monkeypatch.setattr(
fg, "default_visualizer",
lambda val: _FakePrinter(children=[("[contained value]", inner)]) if val is engaged_val else None,
)
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"maybe": engaged_val})
holder_type._fields = [fg.Field("maybe")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
assert _fields(root)["maybe"]["value"] == "42"
assert _fields(root)["maybe"]["typeHint"] == "std::optional<int>"
def test_pretty_printer_disengaged_shows_empty_text(monkeypatch):
opt_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::optional<int>")
disengaged_val = fg.Value(opt_type, address=0x100, fields={})
monkeypatch.setattr(
fg, "default_visualizer",
lambda val: _FakePrinter(children=[], text="std::nullopt") if val is disengaged_val else None,
)
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"maybe": disengaged_val})
holder_type._fields = [fg.Field("maybe")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
assert _fields(root)["maybe"]["value"] == "std::nullopt"
def test_pretty_printer_multi_leaf_expands_into_dotted_fields(monkeypatch):
pair_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::pair<int,int>")
pair_val = fg.Value(pair_type, address=0x100, fields={})
first = fg.Value(INT_T, py_value="1")
second = fg.Value(INT_T, py_value="2")
monkeypatch.setattr(
fg, "default_visualizer",
lambda val: _FakePrinter(children=[("first", first), ("second", second)]) if val is pair_val else None,
)
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"p": pair_val})
holder_type._fields = [fg.Field("p")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
fields = _fields(root)
assert fields["p.first"]["value"] == "1"
assert fields["p.second"]["value"] == "2"
# --- Rust enum/variant flattening -------------------------------------------
def test_variant_like_some_flattens_boxed_pointer_payload():
"""_flatten_variant only unwraps a variant's payload field when that
field's *own value* is itself struct-typed (real DWARF wraps a boxed
pointer this way) -- a bare pointer/int payload wouldn't have anything
further to recurse into and is handled by the caller's fallback
instead (see test_variant_like_none_falls_back_to_scalar_rendering).
The single-field-named-"__0" case aliases straight through with no
dotted suffix, so `next` reads as a plain pointer field."""
node_type, node_ptr_type = make_node_types()
n10 = make_node(node_type, 0x10, "10", null_ptr(node_ptr_type))
ptr_val = ptr_to(node_ptr_type, 0x10, n10)
box_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Box<Node<int>>", fields=[fg.Field("__0")])
box_val = fg.Value(box_type, fields={"__0": ptr_val})
option_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="Option<Box<Node<int>>>",
fields=[fg.Field("discriminant", artificial=True), fg.Field("__0")],
)
some_val = fg.Value(option_type, fields={"__0": box_val})
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"next": some_val})
holder_type._fields = [fg.Field("next")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
next_field = _fields(root)["next"]
assert next_field["isPointer"] is True
assert next_field["value"] == "0x10"
assert any(e["from"] == "0x200" and e["to"] == "0x10" and e["label"] == "next" for e in data["edges"])
def test_variant_like_none_falls_back_to_scalar_rendering():
# Same shape, but the "__0" field is inaccessible for the live (None)
# variant -- val["__0"] raises gdb.error, matching real gdb's behavior
# for an inactive Rust enum variant.
option_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="Option<Box<Node<int>>>",
fields=[fg.Field("discriminant", artificial=True), fg.Field("__0")],
)
none_val = fg.Value(option_type, fields={}, py_value="None")
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"next": none_val})
holder_type._fields = [fg.Field("next")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
next_field = _fields(root)["next"]
assert next_field["value"] == "None"
assert next_field.get("isPointer", False) is False
def test_variant_like_multi_field_payload_gets_dotted_labels():
"""When the payload field's value is itself a *multi*-field struct
(e.g. a Rust tuple-variant carrying more than one value), each of that
inner struct's fields becomes its own "outer.inner" leaf instead of
aliasing straight through (aliasing is only for the single-field,
name=="__0" case)."""
rect_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Rect", fields=[fg.Field("width"), fg.Field("height")])
rect_val = fg.Value(rect_type, fields={
"width": fg.Value(INT_T, py_value="3"),
"height": fg.Value(INT_T, py_value="4"),
})
variant_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="Shape::Rectangle",
fields=[fg.Field("discriminant", artificial=True), fg.Field("__0")],
)
shape_val = fg.Value(variant_type, fields={"__0": rect_val})
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder")
holder = fg.Value(holder_type, address=0x200, fields={"shape": shape_val})
holder_type._fields = [fg.Field("shape")]
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x200")
fields = _fields(root)
assert fields["shape.width"]["value"] == "3"
assert fields["shape.height"]["value"] == "4"
# --- the "upgrade a scalar leaf to the full struct" address-sharing case ----
def test_top_scalar_pointer_gets_upgraded_by_richer_struct_walk(monkeypatch):
"""The gdb analog of $debug_graph(top): `top` is a bare `const int*`
whose address coincides with a Node's first member (so it shares the
Node's own address), auto-discovered alongside `list` (a sibling
local). Whichever order they're walked in, the richer struct
description must win -- this is the exact mechanism verify_top_delves_
into_list checks end-to-end; here it's isolated as a pure unit test
against _visit_pointer's node_id-vs-visited bookkeeping."""
node_type, node_ptr_type, n30 = build_chain() # n30 lives at 0x30
list_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="LinkedList<int>")
list_val = fg.Value(list_type, address=0x999, fields={"head_": ptr_to(node_ptr_type, 0x30, n30)})
list_type._fields = [fg.Field("head_")]
const_int_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=INT_T, display="const int *")
# `top` points at 0x30 but its *static* type is a bare int*, sharing
# n30's address since `data` is the node's first member.
top = fg.Value(const_int_ptr_t, ptr_int=0x30, pointee=n30["data"])
frame = fg.Frame(fg.Block([fg.Symbol("list", list_val)], is_global=False, is_static=False))
monkeypatch.setattr(fg, "selected_frame", lambda: frame)
import json
data = json.loads(gg.build_graph_json(top, []))
node_0x30 = next(n for n in data["nodes"] if n["id"] == "0x30")
# must be the rich Node (data+next), not the scalar leaf `top`'s
# walk would have produced on its own
assert set(_fields(node_0x30)) == {"data", "next"}
next_edges = [e for e in data["edges"] if e["label"] == "next"]
assert len(next_edges) == 2
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["list"]["kind"] == "local"
# --- frame-based local/global auto-discovery --------------------------------
def test_frame_locals_walk_nested_blocks_and_dedup_by_name(monkeypatch):
outer_sym = fg.Symbol("y", fg.Value(INT_T, py_value="2"))
# "x" shadowed in both blocks -- only the innermost should be reported
shadow_inner = fg.Symbol("x", fg.Value(INT_T, py_value="inner"))
outer_block = fg.Block([outer_sym, fg.Symbol("x", fg.Value(INT_T, py_value="outer-shadowed"))], is_global=False)
inner_block = fg.Block([shadow_inner], superblock=outer_block, is_global=False)
frame = fg.Frame(inner_block)
monkeypatch.setattr(fg, "selected_frame", lambda: frame)
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="99"), []))
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["x"]["value"] == "inner"
assert roots_by_name["y"]["value"] == "2"
def test_frame_globals_from_symtab_are_discovered(monkeypatch):
global_sym = fg.Symbol("g_count", fg.Value(INT_T, py_value="7"))
static_sym = fg.Symbol("s_flag", fg.Value(INT_T, py_value="1"))
global_block = fg.Block([global_sym], is_global=True)
static_block = fg.Block([static_sym], is_static=True)
symtab = fg.Symtab(global_block=global_block, static_block=static_block)
frame = fg.Frame(fg.Block([]), symtab=symtab)
monkeypatch.setattr(fg, "selected_frame", lambda: frame)
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="0"), []))
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["g_count"]["kind"] == "global"
assert roots_by_name["s_flag"]["kind"] == "global"
def test_no_selected_frame_still_returns_primary_root_only(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="5"), []))
assert data["roots"] == []
# --- watched roots + DebugGraphFunction.invoke() ----------------------------
def test_watched_roots_are_labeled_and_walked(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type, n30 = build_chain()
watched_ptr = ptr_to(node_ptr_type, 0x30, n30)
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="0"), [("top", watched_ptr)]))
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["top"]["kind"] == "watched"
assert roots_by_name["top"]["value"] == "0x30"
def test_debug_graph_function_requires_at_least_one_arg():
fn = gg.DebugGraphFunction()
try:
fn.invoke()
assert False, "expected GdbError"
except fg.GdbError:
pass
def test_debug_graph_function_returns_a_string_value_via_parse_and_eval(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
fn = gg.DebugGraphFunction()
result_value = fn.invoke(fg.Value(INT_T, py_value="42"))
assert result_value.string().startswith(gg._RESULT_MARKER)
# --- _JsonResultPrettyPrinter / _pretty_print_lookup ------------------------
def test_pretty_print_lookup_recognizes_our_own_result_marker():
text = gg._RESULT_MARKER + '..."}'
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T)
val = fg.Value(char_ptr_t, ptr_int=0x1, py_value=text)
printer = gg._pretty_print_lookup(val)
assert printer is not None
assert printer.to_string() == text
assert printer.display_hint() == "string"
def test_pretty_print_lookup_ignores_unrelated_strings():
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T)
val = fg.Value(char_ptr_t, ptr_int=0x1, py_value="just a normal char* the user's program made")
assert gg._pretty_print_lookup(val) is None
def test_pretty_print_lookup_ignores_non_char_pointee():
int_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=INT_T)
val = fg.Value(int_ptr_t, ptr_int=0x1, py_value="30")
assert gg._pretty_print_lookup(val) is None
# --- direct-call coverage of the smaller error-handling branches -----------
class _RaisesOnStr:
def __str__(self):
raise fg.error("gdb couldn't format this value")
def test_format_scalar_falls_back_on_gdb_error():
assert gg._format_scalar(_RaisesOnStr()) == "<error: gdb couldn't format this value>"
def test_is_variant_like_false_when_fields_raises_type_error():
class BrokenFieldsType(fg.Type):
def fields(self):
raise TypeError("fields unavailable")
t = BrokenFieldsType(fg.TYPE_CODE_STRUCT, tag="Weird")
assert gg._is_variant_like(t) is False
def test_is_variant_like_false_for_non_struct_type():
assert gg._is_variant_like(INT_T) is False
def test_flatten_variant_yields_nothing_when_payload_is_not_struct_like():
"""Payload field access succeeds but its value isn't struct/union
typed (e.g. gdb hands back the raw scalar directly) -- nothing further
to recurse into, so the caller falls back to whole-value rendering."""
variant_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="SomeVariant",
fields=[fg.Field("discriminant", artificial=True), fg.Field("__0")],
)
val = fg.Value(variant_type, fields={"__0": fg.Value(INT_T, py_value="5")})
assert list(gg._flatten_variant(val, variant_type, "label")) == []
def test_pretty_printer_summary_children_raising_gdb_error_falls_back_to_to_string(monkeypatch):
class ExplodingChildrenPrinter:
def children(self):
raise fg.error("children unavailable")
def to_string(self):
return "fallback text"
dummy = object()
monkeypatch.setattr(fg, "default_visualizer", lambda val: ExplodingChildrenPrinter() if val is dummy else None)
leaves, empty_text = gg._pretty_printer_summary(dummy)
assert leaves == []
assert empty_text == "fallback text"
def test_pretty_printer_summary_to_string_raising_gdb_error():
class ExplodingToStringPrinter:
def to_string(self):
raise fg.error("to_string unavailable")
import gdb_debug_graph
old = gdb_debug_graph.gdb.default_visualizer
gdb_debug_graph.gdb.default_visualizer = lambda val: ExplodingToStringPrinter()
try:
leaves, empty_text = gg._pretty_printer_summary(object())
finally:
gdb_debug_graph.gdb.default_visualizer = old
assert leaves == []
assert empty_text == "<pretty-printer error>"
def test_type_name_returns_none_when_tag_access_raises_gdb_error():
class ExplodingTagType:
code = fg.TYPE_CODE_INT
@property
def tag(self):
raise fg.error("tag inaccessible")
assert gg._type_name(ExplodingTagType()) is None
def test_process_member_pointer_int_conversion_error_treated_as_null(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type = make_node_types()
# a pointer-typed field whose Value has no ptr_int wired up at all --
# int(val) raises inside our fake, exactly like a gdb.error would for
# an unreadable/optimized-out pointer.
broken_ptr = fg.Value(node_ptr_type)
holder_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Holder", fields=[fg.Field("next")])
holder = fg.Value(holder_type, address=0x1, fields={"next": broken_ptr})
import json
data = json.loads(gg.build_graph_json(holder, []))
root = next(n for n in data["nodes"] if n["id"] == "0x1")
assert _fields(root)["next"]["value"] == "0x0"
assert data["edges"] == []
def test_describe_struct_skips_a_field_whose_access_raises_gdb_error():
class PartiallyReadableValue(fg.Value):
def __getitem__(self, name):
if name == "broken":
raise fg.error("optimized out")
return super().__getitem__(name)
holder_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="Holder",
fields=[fg.Field("broken"), fg.Field("ok")],
)
holder = PartiallyReadableValue(holder_type, address=0x1, fields={"ok": fg.Value(INT_T, py_value="1")})
nodes, edges, visited = {}, [], set()
gg._describe_struct(holder, "0x1", nodes, edges, visited)
assert set(_fields({"fields": nodes["0x1"]["fields"]})) == {"ok"}
def test_visit_pointer_char_star_skips_when_node_already_described():
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T)
ptr_val = fg.Value(char_ptr_t, ptr_int=0x50, py_value="should not be seen")
nodes = {"0x50": {"fields": [{"name": "value", "value": "already here", "isPointer": False}], "type": "char *"}}
gg._visit_pointer(ptr_val, 0x50, nodes, [], set())
assert _fields(nodes["0x50"])["value"]["value"] == "already here"
def test_visit_pointer_char_star_falls_back_on_unicode_decode_error():
class BadStringValue(fg.Value):
def string(self, length=None):
raise UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte")
def dereference(self):
return fg.Value(CHAR_T, py_value="0")
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T)
ptr_val = BadStringValue(char_ptr_t, ptr_int=0x60)
nodes = {}
gg._visit_pointer(ptr_val, 0x60, nodes, [], set())
assert _fields(nodes["0x60"])["value"]["value"] == "0"
def test_visit_pointer_generic_scalar_skips_when_node_already_described():
int_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=INT_T)
ptr_val = fg.Value(int_ptr_t, ptr_int=0x70, pointee=fg.Value(INT_T, py_value="99"))
nodes = {"0x70": {"fields": [{"name": "value", "value": "already here", "isPointer": False}], "type": "int *"}}
gg._visit_pointer(ptr_val, 0x70, nodes, [], set())
assert _fields(nodes["0x70"])["value"]["value"] == "already here"
def test_walk_primary_root_null_pointer_produces_no_node():
node_type, node_ptr_type = make_node_types()
null_root = null_ptr(node_ptr_type)
nodes, edges, visited = {}, [], set()
addr = gg._walk_primary_root(null_root, nodes, edges, visited)
assert addr is None
assert nodes == {}
def test_add_named_root_pointer_int_error_is_skipped_entirely():
node_type, node_ptr_type = make_node_types()
broken_ptr = fg.Value(node_ptr_type) # int(val) raises
roots_out = []
gg._add_named_root("bad", broken_ptr, "local", {}, [], set(), roots_out, set())
assert roots_out == []
def test_add_named_root_struct_address_error_is_skipped_entirely():
class NoAddressValue(fg.Value):
@property
def address(self):
raise fg.error("no address")
struct_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Weird", fields=[])
val = NoAddressValue(struct_type, fields={})
roots_out = []
gg._add_named_root("weird", val, "local", {}, [], set(), roots_out, set())
assert roots_out == []
def test_add_named_root_struct_already_visited_still_adds_root_without_redescribing():
struct_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Thing", fields=[fg.Field("x")])
val = fg.Value(struct_type, address=0x5, fields={"x": fg.Value(INT_T, py_value="1")})
nodes = {"0x5": {"fields": [{"name": "x", "value": "STALE"}], "type": "Thing"}}
roots_out = []
gg._add_named_root("thing", val, "local", nodes, [], {"0x5"}, roots_out, set())
assert roots_out == [("thing", "0x5", "local", "Thing")]
assert nodes["0x5"]["fields"][0]["value"] == "STALE" # not re-described
def test_frame_local_symbols_skips_non_variable_non_argument_symbols():
label_sym = fg.Symbol("a_label", fg.Value(INT_T, py_value="0"), is_variable=False, is_argument=False)
real_sym = fg.Symbol("real", fg.Value(INT_T, py_value="1"), is_variable=True)
frame = fg.Frame(fg.Block([label_sym, real_sym]))
names = [s.name for s in gg._frame_local_symbols(frame)]
assert names == ["real"]
def test_file_global_symbols_returns_early_when_find_sal_raises():
class BrokenSalFrame(fg.Frame):
def find_sal(self):
raise fg.error("no line info")
frame = BrokenSalFrame(fg.Block([]))
assert list(gg._file_global_symbols(frame)) == []
def test_file_global_symbols_returns_early_when_symtab_is_none():
frame = fg.Frame(fg.Block([]), symtab=None)
assert list(gg._file_global_symbols(frame)) == []
def test_build_graph_json_watched_root_int_error_treated_as_null(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type = make_node_types()
broken_ptr = fg.Value(node_ptr_type)
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="0"), [("bad", broken_ptr)]))
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["bad"]["value"] == "0x0"
def test_describe_struct_root_with_pretty_printer_summary_single_leaf(monkeypatch):
opt_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::optional<int>")
root_val = fg.Value(opt_type, address=0x1, fields={})
inner = fg.Value(INT_T, py_value="7")
monkeypatch.setattr(fg, "default_visualizer", lambda val: _FakePrinter(children=[("[contained value]", inner)]))
nodes, edges, visited = {}, [], set()
gg._describe_struct(root_val, "0x1", nodes, edges, visited)
assert _fields({"fields": nodes["0x1"]["fields"]})["value"]["value"] == "7"
def test_describe_struct_root_with_pretty_printer_summary_disengaged(monkeypatch):
opt_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::optional<int>")
root_val = fg.Value(opt_type, address=0x1, fields={})
monkeypatch.setattr(fg, "default_visualizer", lambda val: _FakePrinter(children=[], text="std::nullopt"))
nodes, edges, visited = {}, [], set()
gg._describe_struct(root_val, "0x1", nodes, edges, visited)
assert _fields({"fields": nodes["0x1"]["fields"]})["value"]["value"] == "std::nullopt"
def test_describe_struct_root_with_pretty_printer_summary_multi_leaf(monkeypatch):
pair_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="std::pair<int,int>")
root_val = fg.Value(pair_type, address=0x1, fields={})
first = fg.Value(INT_T, py_value="1")
second = fg.Value(INT_T, py_value="2")
monkeypatch.setattr(fg, "default_visualizer", lambda val: _FakePrinter(children=[("first", first), ("second", second)]))
nodes, edges, visited = {}, [], set()
gg._describe_struct(root_val, "0x1", nodes, edges, visited)
fields = _fields({"fields": nodes["0x1"]["fields"]})
assert fields["first"]["value"] == "1"
assert fields["second"]["value"] == "2"
def test_add_named_root_struct_already_in_skip_addrs_is_not_added():
struct_type = fg.Type(fg.TYPE_CODE_STRUCT, tag="Thing", fields=[])
val = fg.Value(struct_type, address=0x5, fields={})
roots_out = []
gg._add_named_root("thing", val, "local", {}, [], set(), roots_out, {"0x5"})
assert roots_out == []
def test_file_global_symbols_skips_duplicate_names_and_non_variables():
dup1 = fg.Symbol("dup", fg.Value(INT_T, py_value="1"))
dup2 = fg.Symbol("dup", fg.Value(INT_T, py_value="2"))
not_a_var = fg.Symbol("fn", fg.Value(INT_T, py_value="?"), is_variable=False)
block = fg.Block([dup1, dup2, not_a_var], is_global=True)
symtab = fg.Symtab(global_block=block)
frame = fg.Frame(fg.Block([]), symtab=symtab)
names = [s.name for s in gg._file_global_symbols(frame)]
assert names == ["dup"]
def test_json_result_pretty_printer_falls_back_when_string_raises():
class BadStringValue(fg.Value):
def string(self, length=None):
raise UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte")
def __str__(self):
return "fallback repr"
val = BadStringValue(fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T), ptr_int=0x1)
printer = gg._JsonResultPrettyPrinter(val)
assert printer.to_string() == "fallback repr"
def test_pretty_print_lookup_handles_array_typed_values():
array_type = fg.Type(fg.TYPE_CODE_ARRAY, target=CHAR_T)
val = fg.Value(array_type, py_value=gg._RESULT_MARKER + '..."}')
printer = gg._pretty_print_lookup(val)
assert printer is not None
def test_pretty_print_lookup_returns_none_when_string_probe_raises():
class BadStringValue(fg.Value):
def string(self, length=None):
raise fg.error("cannot read memory")
char_ptr_t = fg.Type(fg.TYPE_CODE_PTR, target=CHAR_T)
val = BadStringValue(char_ptr_t, ptr_int=0x1)
assert gg._pretty_print_lookup(val) is None
def test_debug_graph_function_invoke_with_watched_name_ptr_pairs(monkeypatch):
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type, n30 = build_chain()
name_val = fg.Value(CHAR_T, py_value="top")
fn = gg.DebugGraphFunction()
result_value = fn.invoke(fg.Value(INT_T, py_value="0"), name_val, ptr_to(node_ptr_type, 0x30, n30))
import json
data = json.loads(result_value.string())
roots_by_name = {r["name"]: r for r in data["roots"]}
assert roots_by_name["top"]["kind"] == "watched"
def test_debug_graph_function_invoke_name_string_extraction_falls_back(monkeypatch):
"""If the extra "name" argument doesn't support .string() (TypeError),
str(name_val) is used instead."""
monkeypatch.setattr(fg, "selected_frame", lambda: (_ for _ in ()).throw(fg.error("no frame")))
node_type, node_ptr_type, n30 = build_chain()
class NoStringName:
def string(self):
raise TypeError("not a string-like value")
def __str__(self):
return "fallback_name"
fn = gg.DebugGraphFunction()
result_value = fn.invoke(fg.Value(INT_T, py_value="0"), NoStringName(), ptr_to(node_ptr_type, 0x30, n30))
import json
data = json.loads(result_value.string())
roots_by_name = {r["name"]: r for r in data["roots"]}
assert "fallback_name" in roots_by_name
def test_describe_struct_skips_anonymous_and_base_class_fields():
holder_type = fg.Type(
fg.TYPE_CODE_STRUCT, tag="Derived",
fields=[fg.Field(None), fg.Field("Base", is_base_class=True), fg.Field("own")],
)
holder = fg.Value(holder_type, address=0x1, fields={"own": fg.Value(INT_T, py_value="1")})
nodes, edges, visited = {}, [], set()
gg._describe_struct(holder, "0x1", nodes, edges, visited)
assert set(_fields({"fields": nodes["0x1"]["fields"]})) == {"own"}
def test_add_named_root_pointer_success_path_visits_and_appends():
node_type, node_ptr_type, n30 = build_chain()
ptr_val = ptr_to(node_ptr_type, 0x30, n30)
nodes, edges, visited, roots_out = {}, [], set(), []
gg._add_named_root("top", ptr_val, "local", nodes, edges, visited, roots_out, set())
assert roots_out == [("top", "0x30", "local", "Node<int> *")]
assert "0x30" in nodes
def test_build_graph_json_skips_frame_locals_and_globals_already_used_by_name(monkeypatch):
"""A name already claimed by an explicit watched root must not be
re-added as a local/global, even if the frame also has a symbol with
that same name."""
same_name_local = fg.Symbol("dup", fg.Value(INT_T, py_value="local-version"))
same_name_global = fg.Symbol("dup", fg.Value(INT_T, py_value="global-version"))
global_block = fg.Block([same_name_global], is_global=True)
symtab = fg.Symtab(global_block=global_block)
frame = fg.Frame(fg.Block([same_name_local]), symtab=symtab)
monkeypatch.setattr(fg, "selected_frame", lambda: frame)
watched_ptr = fg.Value(INT_T, py_value="watched-version")
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="0"), [("dup", watched_ptr)]))
dup_roots = [r for r in data["roots"] if r["name"] == "dup"]
assert len(dup_roots) == 1
assert dup_roots[0]["kind"] == "watched"
def test_add_named_root_null_pointer_is_skipped_entirely():
node_type, node_ptr_type = make_node_types()
roots_out = []
gg._add_named_root("nil", null_ptr(node_ptr_type), "local", {}, [], set(), roots_out, set())
assert roots_out == []
def test_add_named_root_pointer_already_in_skip_addrs_is_not_added():
node_type, node_ptr_type, n30 = build_chain()
ptr_val = ptr_to(node_ptr_type, 0x30, n30)
roots_out = []
gg._add_named_root("top", ptr_val, "local", {}, [], set(), roots_out, {"0x30"})
assert roots_out == []
def test_pretty_print_lookup_returns_none_for_non_pointer_non_array_type():
assert gg._pretty_print_lookup(fg.Value(INT_T, py_value="5")) is None
def test_build_graph_json_skips_locals_and_globals_whose_value_raises():
class BrokenValueSymbol(fg.Symbol):
def value(self, frame):
raise fg.error("optimized out")
local_sym = BrokenValueSymbol("bad_local", None)
global_sym = BrokenValueSymbol("bad_global", None)
global_block = fg.Block([global_sym], is_global=True)
symtab = fg.Symtab(global_block=global_block)
frame = fg.Frame(fg.Block([local_sym]), symtab=symtab)
import gdb_debug_graph as gg_mod
gg_mod.gdb.selected_frame = lambda: frame
try:
import json
data = json.loads(gg.build_graph_json(fg.Value(INT_T, py_value="0"), []))
finally:
gg_mod.gdb.selected_frame = fg.selected_frame
assert data["roots"] == []