[libc++] Fix mi-mode in GDB pretty printers (#120951)

GDB/MI requires unique names for each child, otherwise fails with
"Duplicate variable object name". Also wrapped containers printers
were flattened for cleaner visualization in IDEs and CLI.

Fixes #62340
This commit is contained in:
Sv. Lockal 2025-01-10 00:40:36 +08:00 committed by GitHub
parent 5e6c74d086
commit 473510abf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 112 additions and 48 deletions

View File

@ -15,6 +15,7 @@ See gdb_pretty_printer_test.sh.cpp on how to write a test case.
"""
from __future__ import print_function
import json
import re
import gdb
import sys
@ -29,6 +30,7 @@ test_failures = 0
# we exit.
has_run_tests = False
has_execute_mi = tuple(map(int, gdb.VERSION.split("."))) >= (14, 2)
class CheckResult(gdb.Command):
def __init__(self):
@ -42,36 +44,43 @@ class CheckResult(gdb.Command):
# Stack frame is:
# 0. StopForDebugger
# 1. ComparePrettyPrintToChars or ComparePrettyPrintToRegex
# 1. CompareListChildrenToChars, ComparePrettyPrintToChars or ComparePrettyPrintToRegex
# 2. TestCase
compare_frame = gdb.newest_frame().older()
testcase_frame = compare_frame.older()
test_loc = testcase_frame.find_sal()
test_loc_str = test_loc.symtab.filename + ":" + str(test_loc.line)
# Use interactive commands in the correct context to get the pretty
# printed version
value_str = self._get_value_string(compare_frame, testcase_frame)
frame_name = compare_frame.name()
if frame_name.startswith("CompareListChildren"):
if has_execute_mi:
value = self._get_children(compare_frame)
else:
print("SKIPPED: " + test_loc_str)
return
else:
value = self._get_value(compare_frame, testcase_frame)
# Ignore the convenience variable name and newline
value = value_str[value_str.find("= ") + 2 : -1]
gdb.newest_frame().select()
expectation_val = compare_frame.read_var("expectation")
check_literal = expectation_val.string(encoding="utf-8")
if "PrettyPrintToRegex" in compare_frame.name():
if "PrettyPrintToRegex" in frame_name:
test_fails = not re.search(check_literal, value)
else:
test_fails = value != check_literal
if test_fails:
global test_failures
print("FAIL: " + test_loc.symtab.filename + ":" + str(test_loc.line))
print("FAIL: " + test_loc_str)
print("GDB printed:")
print(" " + repr(value))
print("Value should match:")
print(" " + repr(check_literal))
test_failures += 1
else:
print("PASS: " + test_loc.symtab.filename + ":" + str(test_loc.line))
print("PASS: " + test_loc_str)
except RuntimeError as e:
# At this point, lots of different things could be wrong, so don't try to
@ -81,9 +90,28 @@ class CheckResult(gdb.Command):
print(str(e))
test_failures += 1
def _get_value_string(self, compare_frame, testcase_frame):
def _get_children(self, compare_frame):
compare_frame.select()
if "ComparePrettyPrint" in compare_frame.name():
gdb.execute_mi("-var-create", "value", "*", "value")
r = gdb.execute_mi("-var-list-children", "--simple-values", "value")
gdb.execute_mi("-var-delete", "value")
children = r["children"]
if r["displayhint"] == "map":
r = [
{
"key": json.loads(children[2 * i]["value"]),
"value": json.loads(children[2 * i + 1]["value"]),
}
for i in range(len(children) // 2)
]
else:
r = [json.loads(el["value"]) for el in children]
return json.dumps(r, sort_keys=True)
def _get_value(self, compare_frame, testcase_frame):
compare_frame.select()
frame_name = compare_frame.name()
if frame_name.startswith("ComparePrettyPrint"):
s = gdb.execute("p value", to_string=True)
else:
value_str = str(compare_frame.read_var("value"))
@ -91,8 +119,10 @@ class CheckResult(gdb.Command):
testcase_frame.select()
s = gdb.execute("p " + clean_expression_str, to_string=True)
if sys.version_info.major == 2:
return s.decode("utf-8")
return s
s = s.decode("utf-8")
# Ignore the convenience variable name and newline
return s[s.find("= ") + 2 : -1]
def exit_handler(event=None):
@ -112,6 +142,10 @@ def exit_handler(event=None):
# Disable terminal paging
gdb.execute("set height 0")
gdb.execute("set python print-stack full")
if has_execute_mi:
gdb.execute_mi("-enable-pretty-printing")
test_failures = 0
CheckResult()
test_bp = gdb.Breakpoint("StopForDebugger")

View File

@ -129,6 +129,12 @@ void CompareExpressionPrettyPrintToRegex(
StopForDebugger(&value, &expectation);
}
template <typename TypeToPrint>
void CompareListChildrenToChars(TypeToPrint value, const char* expectation) {
MarkAsLive(value);
StopForDebugger(&value, &expectation);
}
namespace example {
struct example_struct {
int a = 0;
@ -361,28 +367,28 @@ void multimap_test() {
void queue_test() {
std::queue<int> i_am_empty;
ComparePrettyPrintToChars(i_am_empty,
"std::queue wrapping = {std::deque is empty}");
ComparePrettyPrintToChars(i_am_empty, "std::queue wrapping: std::deque is empty");
std::queue<int> one_two_three(std::deque<int>{1, 2, 3});
ComparePrettyPrintToChars(one_two_three,
"std::queue wrapping = {"
"std::deque with 3 elements = {1, 2, 3}}");
ComparePrettyPrintToChars(
one_two_three,
"std::queue wrapping: "
"std::deque with 3 elements = {1, 2, 3}");
}
void priority_queue_test() {
std::priority_queue<int> i_am_empty;
ComparePrettyPrintToChars(i_am_empty,
"std::priority_queue wrapping = {std::vector of length 0, capacity 0}");
ComparePrettyPrintToChars(i_am_empty, "std::priority_queue wrapping: std::vector of length 0, capacity 0");
std::priority_queue<int> one_two_three;
one_two_three.push(11111);
one_two_three.push(22222);
one_two_three.push(33333);
ComparePrettyPrintToRegex(one_two_three,
R"(std::priority_queue wrapping = )"
R"({std::vector of length 3, capacity 3 = {33333)");
ComparePrettyPrintToRegex(
one_two_three,
R"(std::priority_queue wrapping: )"
R"(std::vector of length 3, capacity 3 = {33333)");
ComparePrettyPrintToRegex(one_two_three, ".*11111.*");
ComparePrettyPrintToRegex(one_two_three, ".*22222.*");
@ -410,25 +416,22 @@ void set_test() {
void stack_test() {
std::stack<int> test0;
ComparePrettyPrintToChars(test0,
"std::stack wrapping = {std::deque is empty}");
ComparePrettyPrintToChars(test0, "std::stack wrapping: std::deque is empty");
test0.push(5);
test0.push(6);
ComparePrettyPrintToChars(
test0, "std::stack wrapping = {std::deque with 2 elements = {5, 6}}");
ComparePrettyPrintToChars(test0, "std::stack wrapping: std::deque with 2 elements = {5, 6}");
std::stack<bool> test1;
test1.push(true);
test1.push(false);
ComparePrettyPrintToChars(
test1,
"std::stack wrapping = {std::deque with 2 elements = {true, false}}");
ComparePrettyPrintToChars(test1, "std::stack wrapping: std::deque with 2 elements = {true, false}");
std::stack<std::string> test2;
test2.push("Hello");
test2.push("World");
ComparePrettyPrintToChars(test2,
"std::stack wrapping = {std::deque with 2 elements "
"= {\"Hello\", \"World\"}}");
ComparePrettyPrintToChars(
test2,
"std::stack wrapping: std::deque with 2 elements "
"= {\"Hello\", \"World\"}");
}
void multiset_test() {
@ -662,6 +665,25 @@ void streampos_test() {
ComparePrettyPrintToRegex(test1, "^std::fpos with stream offset:5( with state: {count:0 value:0})?$");
}
void mi_mode_test() {
std::map<int, std::string> one_two_three_map;
one_two_three_map.insert({1, "one"});
one_two_three_map.insert({2, "two"});
one_two_three_map.insert({3, "three"});
CompareListChildrenToChars(
one_two_three_map, R"([{"key": 1, "value": "one"}, {"key": 2, "value": "two"}, {"key": 3, "value": "three"}])");
std::unordered_map<int, std::string> one_two_three_umap;
one_two_three_umap.insert({3, "three"});
one_two_three_umap.insert({2, "two"});
one_two_three_umap.insert({1, "one"});
CompareListChildrenToChars(
one_two_three_umap, R"([{"key": 3, "value": "three"}, {"key": 2, "value": "two"}, {"key": 1, "value": "one"}])");
std::deque<int> one_two_three_deque{1, 2, 3};
CompareListChildrenToChars(one_two_three_deque, "[1, 2, 3]");
}
int main(int, char**) {
framework_self_test();
@ -694,5 +716,6 @@ int main(int, char**) {
unordered_set_iterator_test();
pointer_negative_test();
streampos_test();
mi_mode_test();
return 0;
}

View File

@ -446,10 +446,13 @@ class StdDequePrinter(object):
num_emitted = 0
current_addr = self.start_ptr
start_index = self.first_block_start_index
i = 0
while num_emitted < self.size:
end_index = min(start_index + self.size - num_emitted, self.block_size)
for _, elem in self._bucket_it(current_addr, start_index, end_index):
yield "", elem
key_name = "[%d]" % i
i += 1
yield key_name, elem
num_emitted += end_index - start_index
current_addr = gdb.Value(addr_as_long(current_addr) + _pointer_size).cast(
self.node_type
@ -494,8 +497,8 @@ class StdListPrinter(object):
def _list_iter(self):
current_node = self.first_node
for _ in range(self.size):
yield "", current_node.cast(self.nodetype).dereference()["__value_"]
for i in range(self.size):
yield "[%d]" % i, current_node.cast(self.nodetype).dereference()["__value_"]
current_node = current_node.dereference()["__next_"]
def __iter__(self):
@ -512,15 +515,14 @@ class StdQueueOrStackPrinter(object):
"""Print a std::queue or std::stack."""
def __init__(self, val):
self.val = val
self.underlying = val["c"]
self.typename = _remove_generics(_prettify_typename(val.type))
self.visualizer = gdb.default_visualizer(val["c"])
def to_string(self):
typename = _remove_generics(_prettify_typename(self.val.type))
return "%s wrapping" % typename
return "%s wrapping: %s" % (self.typename, self.visualizer.to_string())
def children(self):
return iter([("", self.underlying)])
return self.visualizer.children()
def display_hint(self):
return "array"
@ -530,19 +532,18 @@ class StdPriorityQueuePrinter(object):
"""Print a std::priority_queue."""
def __init__(self, val):
self.val = val
self.underlying = val["c"]
self.typename = _remove_generics(_prettify_typename(val.type))
self.visualizer = gdb.default_visualizer(val["c"])
def to_string(self):
# TODO(tamur): It would be nice to print the top element. The technical
# difficulty is that, the implementation refers to the underlying
# container, which is a generic class. libstdcxx pretty printers do not
# print the top element.
typename = _remove_generics(_prettify_typename(self.val.type))
return "%s wrapping" % typename
return "%s wrapping: %s" % (self.typename, self.visualizer.to_string())
def children(self):
return iter([("", self.underlying)])
return self.visualizer.children()
def display_hint(self):
return "array"
@ -622,13 +623,16 @@ class AbstractRBTreePrinter(object):
"""Traverses the binary search tree in order."""
current = self.util.root
skip_left_child = False
i = 0
while True:
if not skip_left_child and self.util.left_child(current):
current = self.util.left_child(current)
continue
skip_left_child = False
for key_value in self._get_key_value(current):
yield "", key_value
key_name = "[%d]" % i
i += 1
yield key_name, key_value
right_child = self.util.right_child(current)
if right_child:
current = right_child
@ -784,10 +788,13 @@ class AbstractUnorderedCollectionPrinter(object):
def _list_it(self, sentinel_ptr):
next_ptr = sentinel_ptr["__next_"]
i = 0
while str(next_ptr.cast(_void_pointer_type)) != "0x0":
next_val = next_ptr.cast(self.cast_type).dereference()
for key_value in self._get_key_value(next_val):
yield "", key_value
key_name = "[%d]" % i
i += 1
yield key_name, key_value
next_ptr = next_val["__next_"]
def to_string(self):
@ -851,8 +858,8 @@ class AbstractHashMapIteratorPrinter(object):
return self if self.addr else iter(())
def __iter__(self):
for key_value in self._get_key_value():
yield "", key_value
for i, key_value in enumerate(self._get_key_value()):
yield "[%d]" % i, key_value
class StdUnorderedSetIteratorPrinter(AbstractHashMapIteratorPrinter):