[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