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.
834 lines
34 KiB
Python
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"] == []
|