[libcxx-commits] [libcxx] [libcxx] fix mi_mode_test failure with libc++-21 (PR #153969)
Sv. Lockal via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Aug 22 12:48:09 PDT 2025
https://github.com/AngryLoki updated https://github.com/llvm/llvm-project/pull/153969
>From e70ad410ddaa705749be085beb13aeb980ac0210 Mon Sep 17 00:00:00 2001
From: "Sv. Lockal" <lockalsash at gmail.com>
Date: Sat, 16 Aug 2025 17:58:35 +0000
Subject: [PATCH] [libcxx] fix mi_mode_test failure with libc++-21
This test attempts to compare JSON-serialized objects produced by the GDB pretty printer. While it happened to work in libc++-19, it fails in libc++-21 because something changed `std::unordered_map` (and it does not guarantee ordering).
Instead of comparing with fixed string, test now dumps containers using simple json-like serializer.
The issue was never detected by CI, because related test used `gdb.execute_mi` method, available inly since GDB 14.2 (while CI use GDB 12).
Now test provides a polyfill for older GDB releases.
Closes #153940
---
.../libcxx/gdb/gdb_pretty_printer_test.py | 216 +++++++++++++++++-
.../libcxx/gdb/gdb_pretty_printer_test.sh.cpp | 79 +++++--
2 files changed, 269 insertions(+), 26 deletions(-)
diff --git a/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.py b/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.py
index da09092b690c4..0c9566ffaec12 100644
--- a/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.py
+++ b/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.py
@@ -16,7 +16,9 @@
from __future__ import print_function
import json
+import os
import re
+import tempfile
import gdb
import sys
@@ -30,7 +32,199 @@
# we exit.
has_run_tests = False
-has_execute_mi = getattr(gdb, "execute_mi", None) is not None
+
+# Parser for GDB<14.2. Expected input formats:
+# ^done
+# ^done,numchild="1",children=[child={name="value.private",exp="private",numchild="1",value="",thread-id="1"}],has_more="0"
+# ^error,msg="Undefined MI command: rubbish"
+# See https://sourceware.org/gdb/current/onlinedocs/gdb.html/GDB_002fMI-Result-Records.html
+def _parse_mi_record_legacy(rec):
+ m = re.match(r"^\^([a-z]+)(?:,(.*))?$", rec)
+ if not m:
+ raise gdb.error("Failed to parse MI result line: " + rec)
+ code, rest = m.group(1), m.group(2)
+ if not rest:
+ return (code, {})
+ s = rest
+ idx, L = 0, len(s)
+
+ def skip():
+ nonlocal idx
+ while idx < L and s[idx].isspace():
+ idx += 1
+
+ def iden():
+ nonlocal idx
+ start = idx
+ while idx < L and re.match(r"[A-Za-z0-9_.-]", s[idx]):
+ idx += 1
+ return s[start:idx]
+
+ def parse_str():
+ nonlocal idx
+ idx += 1
+ out = []
+ while idx < L:
+ ch = s[idx]
+ if ch == '"':
+ idx += 1
+ return "".join(out)
+ if ch == "\\":
+ idx += 1
+ if idx >= L:
+ break
+ e = s[idx]
+ idx += 1
+ out.append(
+ {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\"}.get(e, e)
+ )
+ else:
+ out.append(ch)
+ idx += 1
+ return "".join(out)
+
+ def value():
+ nonlocal idx
+ skip()
+ if idx >= L:
+ return None
+ ch = s[idx]
+ if ch == '"':
+ return parse_str()
+ if ch == "{":
+ return parse_tuple()
+ if ch == "[":
+ return parse_array()
+ raise gdb.error(
+ "Unexpected characted {} while parsing MI result line: {}".format(ch, rec)
+ )
+
+ def parse_tuple():
+ nonlocal idx
+ idx += 1
+ res = {}
+ skip()
+ while idx < L and s[idx] != "}":
+ k = iden()
+ if idx < L and s[idx] == "=":
+ idx += 1
+ res[k] = value()
+ else:
+ v = value()
+ res[k or str(len(res))] = v
+ skip()
+ if idx < L and s[idx] == ",":
+ idx += 1
+ skip()
+ if idx < L and s[idx] == "}":
+ idx += 1
+ return res
+
+ def parse_array():
+ nonlocal idx
+ idx += 1
+ arr = []
+ skip()
+ while idx < L and s[idx] != "]":
+ save = idx
+ k = iden()
+ if k and idx < L and s[idx] == "=":
+ idx += 1
+ arr.append({k: value()})
+ else:
+ idx = save
+ arr.append(value())
+ skip()
+ if idx < L and s[idx] == ",":
+ idx += 1
+ skip()
+ if idx < L and s[idx] == "]":
+ idx += 1
+ if arr and all(isinstance(x, dict) and len(x) == 1 for x in arr):
+ keys = [next(iter(x)) for x in arr]
+ if all(k == keys[0] for k in keys):
+ arr = [x[keys[0]] for x in arr]
+ return arr
+
+ res = {}
+ while idx < L:
+ skip()
+ n = iden()
+ if not n:
+ break
+ if idx < L and s[idx] == "=":
+ idx += 1
+ res[n] = value()
+ else:
+ res[n] = value()
+ skip()
+ if idx < L and s[idx] == ",":
+ idx += 1
+ return (code, res)
+
+
+def execute_mi(*args, collect_output=False):
+ # gdb.execute_mi is available in GDB 14.2 or later
+ if hasattr(gdb, "execute_mi"):
+ r = gdb.execute_mi(*args)
+ return r if collect_output else None
+
+ # for older GDB: call `interpreter-exec mi2 "-cmd args..."` and parse result like:
+ # ^done,numchild="1",children=[child={name="value.private" ...
+ mi_command = " ".join(args)
+ gdb_command = 'interpreter-exec mi2 "{}"'.format(" ".join(args))
+
+ if not collect_output:
+ gdb.execute(gdb_command)
+ return
+
+ # gdb.execute("interpreter-exec mi2 ...") ignores flag to_string=True:
+ # see https://sourceware.org/bugzilla/show_bug.cgi?id=12886
+ # To get output of MI command, we use temporary file.
+ # "interpreter-exec mi2" also ignores "set logging file ...".
+ # It only prints to stdout, so we:
+ # 1) flush the existing stdout
+ # 2) redirect the stdout to our temporary file
+ # 3) execute the MI command and flush gdb output
+ # 4) restore the original stdout file descriptor
+ # 5) rewind and read our temporary file
+ result_line = ""
+ with tempfile.NamedTemporaryFile(mode="w+") as tmp_file:
+ stdout_fd = sys.__stdout__.fileno()
+ saved_stdout_fd = os.dup(stdout_fd)
+
+ try:
+ sys.__stdout__.flush()
+ os.dup2(tmp_file.fileno(), stdout_fd)
+ gdb.execute(gdb_command)
+ finally:
+ gdb.flush(gdb.STDOUT)
+ os.dup2(saved_stdout_fd, stdout_fd)
+ os.close(saved_stdout_fd)
+
+ tmp_file.seek(0)
+ result_line = tmp_file.read().splitlines()[0]
+
+ if not result_line:
+ raise gdb.error("No GDB/MI output for: " + mi_command)
+
+ match = re.match(r"\^([a-zA-Z-]+)(?:,(.*))?$", result_line)
+
+ if not match:
+ raise gdb.error("Failed to parse MI result line: " + result_line)
+
+ code, payload = _parse_mi_record_legacy(result_line)
+
+ if code == "error":
+ msg = payload.get("msg", "Unknown MI error")
+ raise gdb.error("GDB/MI Error: " + msg)
+
+ return payload
+
+
+def execute_expression(expression):
+ r = execute_mi("-data-evaluate-expression " + expression)
+ return r["value"]
class CheckResult(gdb.Command):
@@ -45,7 +239,8 @@ def invoke(self, arg, from_tty):
# Stack frame is:
# 0. StopForDebugger
- # 1. CompareListChildrenToChars, ComparePrettyPrintToChars or ComparePrettyPrintToRegex
+ # 1. CompareListChildrenToChars, CompareListChildrenSortedToChars,
+ # ComparePrettyPrintToChars or ComparePrettyPrintToRegex
# 2. TestCase
compare_frame = gdb.newest_frame().older()
testcase_frame = compare_frame.older()
@@ -56,11 +251,7 @@ def invoke(self, arg, from_tty):
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
+ value = self._get_children(compare_frame)
else:
value = self._get_value(compare_frame, testcase_frame)
@@ -93,9 +284,11 @@ def invoke(self, arg, from_tty):
def _get_children(self, compare_frame):
compare_frame.select()
- gdb.execute_mi("-var-create", "value", "*", "value")
- r = gdb.execute_mi("-var-list-children", "--simple-values", "value")
- gdb.execute_mi("-var-delete", "value")
+ execute_mi("-var-create", "value", "*", "value")
+ r = execute_mi(
+ "-var-list-children", "--simple-values", "value", collect_output=True
+ )
+ execute_mi("-var-delete", "value")
children = r["children"]
if r["displayhint"] == "map":
r = [
@@ -144,8 +337,7 @@ def exit_handler(event=None):
gdb.execute("set height 0")
gdb.execute("set python print-stack full")
-if has_execute_mi:
- gdb.execute_mi("-enable-pretty-printing")
+execute_mi("-enable-pretty-printing")
test_failures = 0
CheckResult()
diff --git a/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.sh.cpp b/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.sh.cpp
index f5a878582666b..62342823ceceb 100644
--- a/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.sh.cpp
+++ b/libcxx/test/libcxx/gdb/gdb_pretty_printer_test.sh.cpp
@@ -671,23 +671,74 @@ void streampos_test() {
ComparePrettyPrintToRegex(test1, "^std::fpos with stream offset:5( with state: {count:0 value:0})?$");
}
+template <typename T>
+void serialize_value(std::stringstream& ss, const T& value) {
+ // Primitive json-like serializer for integers and simple strings
+ if constexpr (std::is_arithmetic_v<T>) {
+ ss << value;
+ } else {
+ ss << '"' << value << '"';
+ }
+}
+
+template <typename, typename = void>
+struct is_map_like : std::false_type {};
+
+template <typename T>
+struct is_map_like<T, std::void_t<typename T::mapped_type>> : std::true_type {};
+
+template <typename T>
+std::string serialize_for_gdb(const T& container) {
+ // Primitive json-like serializer for containers
+ std::stringstream ss;
+ ss << '[';
+
+ bool first = true;
+ for (const auto& element : container) {
+ if (!first) {
+ ss << ", ";
+ }
+
+ if constexpr (is_map_like<T>::value) {
+ // Map-like container: {"key": K, "value": V}
+ ss << "{\"key\": ";
+ serialize_value(ss, element.first);
+ ss << ", \"value\": ";
+ serialize_value(ss, element.second);
+ ss << '}';
+ } else {
+ // Set-like container
+ serialize_value(ss, element);
+ }
+ first = false;
+ }
+
+ ss << ']';
+ return ss.str();
+}
+
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::string expected;
+
+ std::set<std::string> one_two_three_set{"3", "2", "1"};
+ expected = serialize_for_gdb(one_two_three_set);
+ CompareListChildrenToChars(one_two_three_set, expected.c_str());
+
+ std::unordered_set<std::string> one_two_three_uset{"3", "2", "1"};
+ expected = serialize_for_gdb(one_two_three_uset);
+ CompareListChildrenToChars(one_two_three_uset, expected.c_str());
+
+ std::map<int, std::string> one_two_three_map{{3, "three"}, {2, "two"}, {1, "one"}};
+ expected = serialize_for_gdb(one_two_three_map);
+ CompareListChildrenToChars(one_two_three_map, expected.c_str());
+
+ std::unordered_map<int, std::string> one_two_three_umap{{3, "three"}, {2, "two"}, {1, "one"}};
+ expected = serialize_for_gdb(one_two_three_umap);
+ CompareListChildrenToChars(one_two_three_umap, expected.c_str());
std::deque<int> one_two_three_deque{1, 2, 3};
- CompareListChildrenToChars(one_two_three_deque, "[1, 2, 3]");
+ expected = serialize_for_gdb(one_two_three_deque);
+ CompareListChildrenToChars(one_two_three_deque, expected.c_str());
}
int main(int, char**) {
More information about the libcxx-commits
mailing list