https://github.com/ashgti updated 
https://github.com/llvm/llvm-project/pull/176273

>From 1e0e815ca9fb34f401b87fbf9be09785c74847b2 Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Fri, 16 Jan 2026 11:33:33 -0800
Subject: [PATCH 1/3] [lldb-dap] Migrating 'stopped' to structured types.

Updates the 'stopped' event to use structure types. Additionally, I adjusted 
the description to include the full `GetStopDescription` that can have more 
details.
---
 .../tools/lldb-dap/threads/TestDAP_threads.py |   3 +-
 lldb/tools/lldb-dap/EventHelper.cpp           | 123 +++++++++-----
 lldb/tools/lldb-dap/JSONUtils.cpp             | 157 ------------------
 lldb/tools/lldb-dap/JSONUtils.h               |  30 ----
 .../lldb-dap/Protocol/ProtocolEvents.cpp      |  46 +++++
 lldb/tools/lldb-dap/Protocol/ProtocolEvents.h |  62 +++++++
 lldb/unittests/DAP/CMakeLists.txt             |   1 +
 lldb/unittests/DAP/ProtocolEventsTest.cpp     |  46 +++++
 8 files changed, 241 insertions(+), 227 deletions(-)
 create mode 100644 lldb/unittests/DAP/ProtocolEventsTest.cpp

diff --git a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py 
b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
index acd6108853787..be6dd84ec4d44 100644
--- a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
+++ b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
@@ -39,8 +39,7 @@ def test_correct_thread(self):
                 "breakpoint %s." % breakpoint_ids[0]
             )
         )
-        self.assertFalse(stopped_event[0]["body"]["preserveFocusHint"])
-        self.assertTrue(stopped_event[0]["body"]["threadCausedFocus"])
+        self.assertNotIn("preserveFocusHint", stopped_event[0]["body"])
         # All threads should be named Thread {index}
         threads = self.dap_server.get_threads()
         self.assertTrue(all(len(t["name"]) > 0 for t in threads))
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp 
b/lldb/tools/lldb-dap/EventHelper.cpp
index 6c5a9127f131b..2bc64fe8d2582 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -25,9 +25,12 @@
 #include "lldb/API/SBListener.h"
 #include "lldb/API/SBPlatform.h"
 #include "lldb/API/SBStream.h"
+#include "lldb/lldb-types.h"
 #include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Threading.h"
+#include "llvm/Support/raw_ostream.h"
 #include <mutex>
 #include <utility>
 
@@ -188,54 +191,98 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool 
on_entry) {
 
   llvm::DenseSet<lldb::tid_t> old_thread_ids;
   old_thread_ids.swap(dap.thread_ids);
-  uint32_t stop_id = on_entry ? 0 : process.GetStopID();
   const uint32_t num_threads = process.GetNumThreads();
 
-  // First make a pass through the threads to see if the focused thread
-  // has a stop reason. In case the focus thread doesn't have a stop
-  // reason, remember the first thread that has a stop reason so we can
-  // set it as the focus thread if below if needed.
-  lldb::tid_t first_tid_with_reason = LLDB_INVALID_THREAD_ID;
-  uint32_t num_threads_with_reason = 0;
-  bool focus_thread_exists = false;
+  lldb::tid_t stopped_thread_idx = 0;
   for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
     lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
-    const lldb::tid_t tid = thread.GetThreadID();
-    const bool has_reason = ThreadHasStopReason(thread);
-    // If the focus thread doesn't have a stop reason, clear the thread ID
-    if (tid == dap.focus_tid) {
-      focus_thread_exists = true;
-      if (!has_reason)
-        dap.focus_tid = LLDB_INVALID_THREAD_ID;
-    }
-    if (has_reason) {
-      ++num_threads_with_reason;
-      if (first_tid_with_reason == LLDB_INVALID_THREAD_ID)
-        first_tid_with_reason = tid;
-    }
+    dap.thread_ids.insert(thread.GetThreadID());
+
+    if (stopped_thread_idx || !ThreadHasStopReason(thread))
+      continue;
+
+    // Stop at the first thread with a stop reason.
+    stopped_thread_idx = thread_idx;
   }
 
-  // We will have cleared dap.focus_tid if the focus thread doesn't have
-  // a stop reason, so if it was cleared, or wasn't set, or doesn't exist,
-  // then set the focus thread to the first thread with a stop reason.
-  if (!focus_thread_exists || dap.focus_tid == LLDB_INVALID_THREAD_ID)
-    dap.focus_tid = first_tid_with_reason;
-
-  // If no threads stopped with a reason, then report the first one so
-  // we at least let the UI know we stopped.
-  if (num_threads_with_reason == 0) {
-    lldb::SBThread thread = process.GetThreadAtIndex(0);
-    dap.focus_tid = thread.GetThreadID();
-    dap.SendJSON(CreateThreadStopped(dap, thread, stop_id));
+  lldb::SBThread thread = process.GetThreadAtIndex(stopped_thread_idx);
+  assert(thread.IsValid() && "no valid thread found, process not stopped");
+
+  protocol::StoppedEventBody body;
+  if (on_entry) {
+    body.reason = protocol::eStopReasonEntry;
   } else {
-    for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
-      lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
-      dap.thread_ids.insert(thread.GetThreadID());
-      if (ThreadHasStopReason(thread)) {
-        dap.SendJSON(CreateThreadStopped(dap, thread, stop_id));
+    switch (thread.GetStopReason()) {
+    case lldb::eStopReasonTrace:
+    case lldb::eStopReasonPlanComplete:
+      body.reason = protocol::eStopReasonStep;
+      break;
+    case lldb::eStopReasonBreakpoint: {
+      ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread);
+      if (exc_bp) {
+        body.reason = protocol::eStopReasonException;
+        body.text = exc_bp->GetLabel();
+      } else {
+        InstructionBreakpoint *inst_bp =
+            dap.GetInstructionBPFromStopReason(thread);
+        body.reason = inst_bp ? protocol::eStopReasonInstructionBreakpoint
+                              : protocol::eStopReasonBreakpoint;
+
+        llvm::raw_string_ostream OS(body.text);
+        OS << "breakpoint";
+        for (size_t idx = 0; idx < thread.GetStopReasonDataCount(); idx += 2) {
+          lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(idx);
+          lldb::break_id_t bp_loc_id = thread.GetStopReasonDataAtIndex(idx + 
1);
+          body.hitBreakpointIds.push_back(bp_id);
+          OS << " " << bp_id << "." << bp_loc_id;
+        }
       }
+    } break;
+    case lldb::eStopReasonWatchpoint: {
+      body.reason = protocol::eStopReasonDataBreakpoint;
+      lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(0);
+      body.hitBreakpointIds.push_back(bp_id);
+      body.text = llvm::formatv("data breakpoint {0}", bp_id).str();
+    } break;
+    case lldb::eStopReasonProcessorTrace:
+      body.reason = protocol::eStopReasonStep; // fallback reason
+      break;
+    case lldb::eStopReasonHistoryBoundary:
+      body.reason = protocol::eStopReasonStep; // fallback reason
+      break;
+    case lldb::eStopReasonSignal:
+    case lldb::eStopReasonException:
+    case lldb::eStopReasonInstrumentation:
+      body.reason = protocol::eStopReasonException;
+      break;
+    case lldb::eStopReasonExec:
+    case lldb::eStopReasonFork:
+    case lldb::eStopReasonVFork:
+    case lldb::eStopReasonVForkDone:
+      body.reason = protocol::eStopReasonEntry;
+      break;
+    case lldb::eStopReasonInterrupt:
+      body.reason = protocol::eStopReasonPause;
+      break;
+    case lldb::eStopReasonThreadExiting:
+    case lldb::eStopReasonInvalid:
+    case lldb::eStopReasonNone:
+      llvm_unreachable("invalid stop reason, thread is not stopped");
+      break;
     }
   }
+  lldb::tid_t tid = thread.GetThreadID();
+  lldb::SBStream description;
+  thread.GetStopDescription(description);
+  body.description = {description.GetData(), description.GetSize()};
+  body.threadId = tid;
+  body.preserveFocusHint = tid == dap.focus_tid;
+  body.allThreadsStopped = true;
+
+  // Update focused thread.
+  dap.focus_tid = tid;
+
+  dap.Send(protocol::Event{"stopped", std::move(body)});
 
   for (const auto &tid : old_thread_ids) {
     auto end = dap.thread_ids.end();
diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp 
b/lldb/tools/lldb-dap/JSONUtils.cpp
index 5c33c6aa591a6..643ec5c6a685d 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -430,163 +430,6 @@ llvm::json::Object CreateEventObject(const 
llvm::StringRef event_name) {
   return event;
 }
 
-// "StoppedEvent": {
-//   "allOf": [ { "$ref": "#/definitions/Event" }, {
-//     "type": "object",
-//     "description": "Event message for 'stopped' event type. The event
-//                     indicates that the execution of the debuggee has stopped
-//                     due to some condition. This can be caused by a break
-//                     point previously set, a stepping action has completed,
-//                     by executing a debugger statement etc.",
-//     "properties": {
-//       "event": {
-//         "type": "string",
-//         "enum": [ "stopped" ]
-//       },
-//       "body": {
-//         "type": "object",
-//         "properties": {
-//           "reason": {
-//             "type": "string",
-//             "description": "The reason for the event. For backward
-//                             compatibility this string is shown in the UI if
-//                             the 'description' attribute is missing (but it
-//                             must not be translated).",
-//             "_enum": [ "step", "breakpoint", "exception", "pause", "entry" ]
-//           },
-//           "description": {
-//             "type": "string",
-//             "description": "The full reason for the event, e.g. 'Paused
-//                             on exception'. This string is shown in the UI
-//                             as is."
-//           },
-//           "threadId": {
-//             "type": "integer",
-//             "description": "The thread which was stopped."
-//           },
-//           "text": {
-//             "type": "string",
-//             "description": "Additional information. E.g. if reason is
-//                             'exception', text contains the exception name.
-//                             This string is shown in the UI."
-//           },
-//           "allThreadsStopped": {
-//             "type": "boolean",
-//             "description": "If allThreadsStopped is true, a debug adapter
-//                             can announce that all threads have stopped.
-//                             The client should use this information to
-//                             enable that all threads can be expanded to
-//                             access their stacktraces. If the attribute
-//                             is missing or false, only the thread with the
-//                             given threadId can be expanded."
-//           }
-//         },
-//         "required": [ "reason" ]
-//       }
-//     },
-//     "required": [ "event", "body" ]
-//   }]
-// }
-llvm::json::Value CreateThreadStopped(DAP &dap, lldb::SBThread &thread,
-                                      uint32_t stop_id) {
-  llvm::json::Object event(CreateEventObject("stopped"));
-  llvm::json::Object body;
-  switch (thread.GetStopReason()) {
-  case lldb::eStopReasonTrace:
-  case lldb::eStopReasonPlanComplete:
-    body.try_emplace("reason", "step");
-    break;
-  case lldb::eStopReasonBreakpoint: {
-    ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread);
-    if (exc_bp) {
-      body.try_emplace("reason", "exception");
-      EmplaceSafeString(body, "description", exc_bp->GetLabel());
-    } else {
-      InstructionBreakpoint *inst_bp =
-          dap.GetInstructionBPFromStopReason(thread);
-      if (inst_bp) {
-        body.try_emplace("reason", "instruction breakpoint");
-      } else {
-        body.try_emplace("reason", "breakpoint");
-      }
-      std::vector<lldb::break_id_t> bp_ids;
-      std::ostringstream desc_sstream;
-      desc_sstream << "breakpoint";
-      for (size_t idx = 0; idx < thread.GetStopReasonDataCount(); idx += 2) {
-        lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(idx);
-        lldb::break_id_t bp_loc_id = thread.GetStopReasonDataAtIndex(idx + 1);
-        bp_ids.push_back(bp_id);
-        desc_sstream << " " << bp_id << "." << bp_loc_id;
-      }
-      std::string desc_str = desc_sstream.str();
-      body.try_emplace("hitBreakpointIds", llvm::json::Array(bp_ids));
-      EmplaceSafeString(body, "description", desc_str);
-    }
-  } break;
-  case lldb::eStopReasonWatchpoint: {
-    body.try_emplace("reason", "data breakpoint");
-    lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(0);
-    body.try_emplace("hitBreakpointIds",
-                     llvm::json::Array{llvm::json::Value(bp_id)});
-    EmplaceSafeString(body, "description",
-                      llvm::formatv("data breakpoint {0}", bp_id).str());
-  } break;
-  case lldb::eStopReasonInstrumentation:
-    body.try_emplace("reason", "breakpoint");
-    break;
-  case lldb::eStopReasonProcessorTrace:
-    body.try_emplace("reason", "processor trace");
-    break;
-  case lldb::eStopReasonHistoryBoundary:
-    body.try_emplace("reason", "history boundary");
-    break;
-  case lldb::eStopReasonSignal:
-  case lldb::eStopReasonException:
-    body.try_emplace("reason", "exception");
-    break;
-  case lldb::eStopReasonExec:
-    body.try_emplace("reason", "entry");
-    break;
-  case lldb::eStopReasonFork:
-    body.try_emplace("reason", "fork");
-    break;
-  case lldb::eStopReasonVFork:
-    body.try_emplace("reason", "vfork");
-    break;
-  case lldb::eStopReasonVForkDone:
-    body.try_emplace("reason", "vforkdone");
-    break;
-  case lldb::eStopReasonInterrupt:
-    body.try_emplace("reason", "async interrupt");
-    break;
-  case lldb::eStopReasonThreadExiting:
-  case lldb::eStopReasonInvalid:
-  case lldb::eStopReasonNone:
-    break;
-  }
-  if (stop_id == 0)
-    body["reason"] = "entry";
-  const lldb::tid_t tid = thread.GetThreadID();
-  body.try_emplace("threadId", (int64_t)tid);
-  // If no description has been set, then set it to the default thread stopped
-  // description. If we have breakpoints that get hit and shouldn't be reported
-  // as breakpoints, then they will set the description above.
-  if (!ObjectContainsKey(body, "description")) {
-    char description[1024];
-    if (thread.GetStopDescription(description, sizeof(description))) {
-      EmplaceSafeString(body, "description", description);
-    }
-  }
-  // "threadCausedFocus" is used in tests to validate breaking behavior.
-  if (tid == dap.focus_tid) {
-    body.try_emplace("threadCausedFocus", true);
-  }
-  body.try_emplace("preserveFocusHint", tid != dap.focus_tid);
-  body.try_emplace("allThreadsStopped", true);
-  event.try_emplace("body", std::move(body));
-  return llvm::json::Value(std::move(event));
-}
-
 llvm::StringRef GetNonNullVariableName(lldb::SBValue &v) {
   const llvm::StringRef name = v.GetName();
   return !name.empty() ? name : "<null>";
diff --git a/lldb/tools/lldb-dap/JSONUtils.h b/lldb/tools/lldb-dap/JSONUtils.h
index 15449d6ece62a..c2ffa11eceb95 100644
--- a/lldb/tools/lldb-dap/JSONUtils.h
+++ b/lldb/tools/lldb-dap/JSONUtils.h
@@ -234,36 +234,6 @@ void FillResponse(const llvm::json::Object &request,
 ///     definition outlined by Microsoft.
 llvm::json::Object CreateEventObject(const llvm::StringRef event_name);
 
-/// Create a "StoppedEvent" object for a LLDB thread object.
-///
-/// This function will fill in the following keys in the returned
-/// object's "body" object:
-///   "reason" - With a valid stop reason enumeration string value
-///              that Microsoft specifies
-///   "threadId" - The thread ID as an integer
-///   "description" - a stop description (like "breakpoint 12.3") as a
-///                   string
-///   "preserveFocusHint" - a boolean value that states if this thread
-///                         should keep the focus in the GUI.
-///   "allThreadsStopped" - set to True to indicate that all threads
-///                         stop when any thread stops.
-///
-/// \param[in] dap
-///     The DAP session associated with the stopped thread.
-///
-/// \param[in] thread
-///     The LLDB thread to use when populating out the "StoppedEvent"
-///     object.
-///
-/// \param[in] stop_id
-///     The stop id for this event.
-///
-/// \return
-///     A "StoppedEvent" JSON object with that follows the formal JSON
-///     definition outlined by Microsoft.
-llvm::json::Value CreateThreadStopped(DAP &dap, lldb::SBThread &thread,
-                                      uint32_t stop_id);
-
 /// \return
 ///     The variable name of \a value or a default placeholder.
 llvm::StringRef GetNonNullVariableName(lldb::SBValue &value);
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp 
b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index df6be06637a13..1bc656b0458b2 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -8,6 +8,8 @@
 
 #include "Protocol/ProtocolEvents.h"
 #include "JSONUtils.h"
+#include "lldb/lldb-defines.h"
+#include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/JSON.h"
 
 using namespace llvm;
@@ -64,4 +66,48 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
       {"count", MEB.count}};
 }
 
+[[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) {
+  switch (SR) {
+  case eStopReasonStep:
+    return "step";
+  case eStopReasonBreakpoint:
+    return "breakpoint";
+  case eStopReasonException:
+    return "exception";
+  case eStopReasonPause:
+    return "pause";
+  case eStopReasonEntry:
+    return "entry";
+  case eStopReasonGoto:
+    return "goto";
+  case eStopReasonFunctionBreakpoint:
+    return "function breakpoint";
+  case eStopReasonDataBreakpoint:
+    return "data breakpoint";
+  case eStopReasonInstructionBreakpoint:
+    return "instruction breakpoint";
+  case eStopReasonInvalid:
+    return "";
+  }
+}
+
+llvm::json::Value toJSON(const StoppedEventBody &SEB) {
+  llvm::json::Object Result{{"reason", SEB.reason}};
+
+  if (!SEB.description.empty())
+    Result.insert({"description", SEB.description});
+  if (SEB.threadId != LLDB_INVALID_THREAD_ID)
+    Result.insert({"threadId", SEB.threadId});
+  if (SEB.preserveFocusHint)
+    Result.insert({"preserveFocusHint", SEB.preserveFocusHint});
+  if (!SEB.text.empty())
+    Result.insert({"text", SEB.text});
+  if (SEB.allThreadsStopped)
+    Result.insert({"allThreadsStopped", SEB.allThreadsStopped});
+  if (!SEB.hitBreakpointIds.empty())
+    Result.insert({"hitBreakpointIds", SEB.hitBreakpointIds});
+
+  return Result;
+}
+
 } // namespace lldb_dap::protocol
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h 
b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
index 5cd5a843d284e..230d28f7e2810 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
@@ -117,6 +117,68 @@ struct MemoryEventBody {
 };
 llvm::json::Value toJSON(const MemoryEventBody &);
 
+enum StopReason : unsigned {
+  eStopReasonInvalid,
+  eStopReasonStep,
+  eStopReasonBreakpoint,
+  eStopReasonException,
+  eStopReasonPause,
+  eStopReasonEntry,
+  eStopReasonGoto,
+  eStopReasonFunctionBreakpoint,
+  eStopReasonDataBreakpoint,
+  eStopReasonInstructionBreakpoint,
+};
+
+/// The event indicates that the execution of the debuggee has stopped due to
+/// some condition.
+///
+/// This can be caused by a breakpoint previously set, a stepping request has
+/// completed, by executing a debugger statement etc.
+struct StoppedEventBody {
+  /// The reason for the event.
+  ///
+  /// For backward compatibility this string is shown in the UI if the
+  /// `description` attribute is missing (but it must not be translated).
+  StopReason reason = eStopReasonInvalid;
+
+  /// The full reason for the event, e.g. 'Paused on exception'. This string is
+  /// shown in the UI as is and can be translated.
+  std::string description;
+
+  /// The thread which was stopped.
+  lldb::tid_t threadId = LLDB_INVALID_THREAD_ID;
+
+  /// A value of true hints to the client that this event should not change the
+  /// focus.
+  bool preserveFocusHint = false;
+
+  /// Additional information. E.g. if reason is `exception`, text contains the
+  /// exception name. This string is shown in the UI.
+  std::string text;
+
+  /// "If `allThreadsStopped` is true, a debug adapter can announce that all
+  /// threads have stopped.
+  ///
+  /// - The client should use this information to enable that all threads can 
be
+  /// expanded to access their stacktraces.
+  /// - If the attribute is missing or false, only the thread with the given
+  /// `threadId` can be expanded.
+  bool allThreadsStopped = false;
+
+  /// Ids of the breakpoints that triggered the event. In most cases there is
+  /// only a single breakpoint but here are some examples for multiple
+  /// breakpoints:
+  ///
+  /// - Different types of breakpoints map to the same location.
+  /// - Multiple source breakpoints get collapsed to the same instruction by 
the
+  /// compiler/runtime.
+  /// - Multiple function breakpoints with different function names map to the
+  /// same location.
+  std::vector<lldb::break_id_t> hitBreakpointIds;
+};
+llvm::json::Value toJSON(const StoppedEventBody &);
+
 } // end namespace lldb_dap::protocol
 
 #endif
diff --git a/lldb/unittests/DAP/CMakeLists.txt 
b/lldb/unittests/DAP/CMakeLists.txt
index 9fef37e00ed5d..97f9cad7477ed 100644
--- a/lldb/unittests/DAP/CMakeLists.txt
+++ b/lldb/unittests/DAP/CMakeLists.txt
@@ -10,6 +10,7 @@ add_lldb_unittest(DAPTests
   Handler/ContinueTest.cpp
   JSONUtilsTest.cpp
   LLDBUtilsTest.cpp
+  ProtocolEventsTest.cpp
   ProtocolRequestsTest.cpp
   ProtocolTypesTest.cpp
   ProtocolUtilsTest.cpp
diff --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp 
b/lldb/unittests/DAP/ProtocolEventsTest.cpp
new file mode 100644
index 0000000000000..bb7a1e9574fc8
--- /dev/null
+++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp
@@ -0,0 +1,46 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Protocol/ProtocolEvents.h"
+#include "TestingSupport/TestUtilities.h"
+#include "llvm/Testing/Support/Error.h"
+#include <gtest/gtest.h>
+
+using namespace llvm;
+using namespace lldb_dap::protocol;
+using llvm::json::parse;
+using llvm::json::Value;
+
+/// Returns a pretty printed json string of a `llvm::json::Value`.
+static std::string pp(const Value &E) { return formatv("{0:2}", E).str(); }
+
+TEST(ProtocolEventsTest, StoppedEventBody) {
+  StoppedEventBody body;
+  Expected<Value> expected_body = parse(R"({
+    "reason": ""
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(pp(*expected_body), pp(body));
+
+  body.reason = eStopReasonBreakpoint;
+  body.description = "desc";
+  body.text = "text";
+  body.preserveFocusHint = true;
+  body.allThreadsStopped = true;
+  body.hitBreakpointIds = {1, 2, 3};
+  expected_body = parse(R"({
+    "reason": "breakpoint",
+    "allThreadsStopped": true,
+    "description": "desc",
+    "text": "text",
+    "preserveFocusHint": true,
+    "hitBreakpointIds": [1, 2, 3]
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(pp(*expected_body), pp(body));
+}

>From 427f1c2c8052e370f195870df1bdcdfe0c243b7b Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Fri, 16 Jan 2026 12:04:56 -0800
Subject: [PATCH 2/3] Fixing tests after splitting up PR.

---
 .../test/tools/lldb-dap/lldbdap_testcase.py   | 50 ++++++++++++-------
 .../TestDAP_setExceptionBreakpoints.py        |  8 ++-
 .../lldb-dap/exception/TestDAP_exception.py   |  2 +-
 .../exception/cpp/TestDAP_exception_cpp.py    |  3 +-
 .../exception/objc/TestDAP_exception_objc.py  |  9 ++--
 5 files changed, 46 insertions(+), 26 deletions(-)

diff --git 
a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py 
b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
index e4a3e1c786ffe..015feb40096c0 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
@@ -208,25 +208,40 @@ def verify_all_breakpoints_hit(self, breakpoint_ids):
                     return
         self.assertTrue(False, f"breakpoints not hit, 
stopped_events={stopped_events}")
 
-    def verify_stop_exception_info(self, expected_description):
+    def verify_stop_exception_info(
+        self, expected_description: str, expected_text: Optional[str] = None
+    ):
         """Wait for the process we are debugging to stop, and verify the stop
         reason is 'exception' and that the description matches
         'expected_description'
         """
         stopped_events = self.dap_server.wait_for_stopped()
+        self.assertIsNotNone(stopped_events, "No stopped events detected")
         for stopped_event in stopped_events:
-            if "body" in stopped_event:
-                body = stopped_event["body"]
-                if "reason" not in body:
-                    continue
-                if body["reason"] != "exception":
-                    continue
-                if "description" not in body:
-                    continue
-                description = body["description"]
-                if expected_description == description:
-                    return True
-        return False
+            if (
+                "body" not in stopped_event
+                or stopped_event["body"]["reason"] != "exception"
+            ):
+                continue
+            self.assertIn(
+                "description",
+                stopped_event["body"],
+                f"stopped event missing description {stopped_event}",
+            )
+            description = stopped_event["body"]["description"]
+            self.assertRegex(
+                description,
+                expected_description,
+                f"for 'stopped' event {stopped_event!r}",
+            )
+            if expected_text:
+                self.assertRegex(
+                    stopped_event["body"]["text"],
+                    expected_text,
+                    f"for stopped event {stopped_event!r}",
+                )
+            return
+        self.fail(f"No valid stop exception info detected in {stopped_events}")
 
     def verify_stop_on_entry(self) -> None:
         """Waits for the process to be stopped and then verifies at least one
@@ -437,12 +452,11 @@ def continue_to_breakpoints(self, breakpoint_ids):
         self.do_continue()
         self.verify_breakpoint_hit(breakpoint_ids)
 
-    def continue_to_exception_breakpoint(self, filter_label):
+    def continue_to_exception_breakpoint(
+        self, expected_description, expected_text=None
+    ):
         self.do_continue()
-        self.assertTrue(
-            self.verify_stop_exception_info(filter_label),
-            'verify we got "%s"' % (filter_label),
-        )
+        self.verify_stop_exception_info(expected_description, expected_text)
 
     def continue_to_exit(self, exitCode=0):
         self.do_continue()
diff --git 
a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py 
b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
index 4ca733a9a59ca..2aac9310cb133 100644
--- a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
+++ b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
@@ -35,5 +35,9 @@ def test_functionality(self):
         if response:
             self.assertTrue(response["success"])
 
-        self.continue_to_exception_breakpoint("C++ Throw")
-        self.continue_to_exception_breakpoint("C++ Catch")
+        self.continue_to_exception_breakpoint(
+            expected_description=r"breakpoint 1\.1", expected_text=r"C\+\+ 
Throw"
+        )
+        self.continue_to_exception_breakpoint(
+            expected_description=r"breakpoint 2\.1", expected_text=r"C\+\+ 
Catch"
+        )
diff --git a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py 
b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
index f044bcae41892..b92c3290ceb4c 100644
--- a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
+++ b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
@@ -18,7 +18,7 @@ def test_stopped_description(self):
         self.build_and_launch(program)
         self.do_continue()
 
-        self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+        self.verify_stop_exception_info("signal SIGABRT")
         exceptionInfo = self.get_exceptionInfo()
         self.assertEqual(exceptionInfo["breakMode"], "always")
         self.assertEqual(exceptionInfo["description"], "signal SIGABRT")
diff --git 
a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py 
b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
index 6471e2b87251a..96fcc2cae2c04 100644
--- a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
+++ b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
@@ -2,7 +2,6 @@
 Test exception behavior in DAP with c++ throw.
 """
 
-
 from lldbsuite.test.decorators import *
 from lldbsuite.test.lldbtest import *
 import lldbdap_testcase
@@ -18,7 +17,7 @@ def test_stopped_description(self):
         program = self.getBuildArtifact("a.out")
         self.build_and_launch(program)
         self.dap_server.request_continue()
-        self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+        self.verify_stop_exception_info("signal SIGABRT")
         exceptionInfo = self.get_exceptionInfo()
         self.assertEqual(exceptionInfo["breakMode"], "always")
         self.assertEqual(exceptionInfo["description"], "signal SIGABRT")
diff --git 
a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py 
b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
index ddedf7a6de8c6..3eb8d7885dc35 100644
--- a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
+++ b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
@@ -16,7 +16,7 @@ def test_stopped_description(self):
         program = self.getBuildArtifact("a.out")
         self.build_and_launch(program)
         self.dap_server.request_continue()
-        self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+        self.verify_stop_exception_info("signal SIGABRT")
         exception_info = self.get_exceptionInfo()
         self.assertEqual(exception_info["breakMode"], "always")
         self.assertEqual(exception_info["description"], "signal SIGABRT")
@@ -44,7 +44,10 @@ def test_break_on_throw_and_catch(self):
         if response:
             self.assertTrue(response["success"])
 
-        self.continue_to_exception_breakpoint("Objective-C Throw")
+        self.continue_to_exception_breakpoint(
+            expected_description="hit Objective-C exception",
+            expected_text="Objective-C Throw",
+        )
 
         # FIXME: Catching objc exceptions do not appear to be working.
         # Xcode appears to set a breakpoint on '__cxa_begin_catch' for objc
@@ -54,7 +57,7 @@ def test_break_on_throw_and_catch(self):
 
         self.do_continue()
 
-        self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+        self.verify_stop_exception_info("signal SIGABRT")
         exception_info = self.get_exceptionInfo()
         self.assertEqual(exception_info["breakMode"], "always")
         self.assertEqual(exception_info["description"], "signal SIGABRT")

>From 910b2510a94bdd6430873e079e63f7502dbee647 Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Fri, 16 Jan 2026 16:30:28 -0800
Subject: [PATCH 3/3] Adding additional tests to ensure we can have multiple
 threads stop but only produce a single 'stopped' event.

---
 .../API/tools/lldb-dap/stop-events/Makefile   |  3 +
 .../stop-events/TestDAP_stop_events.py        | 94 +++++++++++++++++++
 .../API/tools/lldb-dap/stop-events/main.cpp   | 27 ++++++
 .../lldb-dap/Protocol/ProtocolEvents.cpp      |  4 +-
 lldb/tools/lldb-dap/Protocol/ProtocolEvents.h |  4 +-
 5 files changed, 128 insertions(+), 4 deletions(-)
 create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/Makefile
 create mode 100644 
lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
 create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/main.cpp

diff --git a/lldb/test/API/tools/lldb-dap/stop-events/Makefile 
b/lldb/test/API/tools/lldb-dap/stop-events/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py 
b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
new file mode 100644
index 0000000000000..e766db2e47f59
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
@@ -0,0 +1,94 @@
+"""
+Test lldb-dap stop events.
+"""
+
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import lldbdap_testcase
+
+
+class TestDAP_stop_events(lldbdap_testcase.DAPTestCaseBase):
+    """
+    Test validates different operations that produce 'stopped' events.
+    """
+
+    def evaluate(self, command: str) -> str:
+        result = self.dap_server.request_evaluate(command, context="repl")
+        self.assertTrue(result["success"])
+        return result["body"]["result"]
+
+    def test_multiple_threads_sample_breakpoint(self):
+        """
+        Test that multiple threads being stopped on the same breakpoint only 
produces a single 'stopped' event.
+        """
+        program = self.getBuildArtifact("a.out")
+        self.build_and_launch(program)
+        line_1 = line_number("main.cpp", "breakpoint 1")
+        [bp1] = self.set_source_breakpoints("main.cpp", [line_1])
+
+        events = self.continue_to_next_stop()
+        self.assertEqual(len(events), 1, "Expected a single stopped event")
+        body = events[0]["body"]
+        self.assertEqual(body["reason"], "breakpoint")
+        self.assertEqual(body["text"], "breakpoint 1.1")
+        self.assertEqual(body["description"], "breakpoint 1.1")
+        self.assertEqual(body["hitBreakpointIds"], [int(bp1)])
+        self.assertEqual(body["allThreadsStopped"], True)
+        self.assertNotIn("preserveFocusHint", body)
+        self.assertIsNotNone(body["threadId"])
+
+        # Should return something like:
+        # Process 1234 stopped
+        #   thread #1: tid = 0x01, 0x0a 
libsystem_pthread.dylib`pthread_mutex_lock + 12, queue = 'com.apple.main-thread'
+        # * thread #2: tid = 0x02, 0x0b a.out`add(a=1, b=2) at main.cpp:10:32, 
stop reason = breakpoint 1.1
+        #   thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, 
stop reason = breakpoint 1.1
+        result = self.evaluate("thread list")
+
+        # Ensure we have 2 threads stopped at the same breakpoint.
+        threads_with_stop_reason = [
+            l for l in result.split("\n") if "stop reason = breakpoint" in l
+        ]
+        self.assertTrue(
+            len(threads_with_stop_reason) == 2,
+            f"Failed to stop at the same breakpoint: {result}",
+        )
+
+        self.continue_to_exit()
+
+    def test_multiple_breakpoints_same_location(self):
+        """
+        Test stopping at a location that reports multiple overlapping 
breakpoints.
+        """
+        program = self.getBuildArtifact("a.out")
+        self.build_and_launch(program)
+        line_1 = line_number("main.cpp", "breakpoint 1")
+        [bp1] = self.set_source_breakpoints("main.cpp", [line_1])
+        [bp2] = self.set_function_breakpoints(["my_add"])
+
+        events = self.continue_to_next_stop()
+        self.assertEqual(len(events), 1, "Expected a single stopped event")
+        body = events[0]["body"]
+        self.assertEqual(body["reason"], "breakpoint")
+        self.assertEqual(body["text"], "breakpoint 1.1 2.1")
+        self.assertEqual(body["description"], "breakpoint 1.1 2.1")
+        self.assertEqual(body["hitBreakpointIds"], [int(bp1), int(bp2)])
+        self.assertEqual(body["allThreadsStopped"], True)
+        self.assertNotIn("preserveFocusHint", body)
+        self.assertIsNotNone(body["threadId"])
+
+        # Should return something like:
+        # Process 1234 stopped
+        #   thread #1: tid = 0x01, 0x0a 
libsystem_pthread.dylib`pthread_mutex_lock + 12, queue = 'com.apple.main-thread'
+        # * thread #2: tid = 0x02, 0x0b a.out`add(a=1, b=2) at main.cpp:10:32, 
stop reason = breakpoint 1.1 2.1
+        #   thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, 
stop reason = breakpoint 1.1 2.1
+        result = self.evaluate("thread list")
+
+        # Ensure we have 2 threads at the same location with overlapping 
breakpoints.
+        threads_with_stop_reason = [
+            l for l in result.split("\n") if "stop reason = breakpoint" in l
+        ]
+        self.assertTrue(
+            len(threads_with_stop_reason) == 2,
+            f"Failed to stop at the same breakpoint: {result}",
+        )
+        self.continue_to_exit()
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp 
b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
new file mode 100644
index 0000000000000..195dbeda5c2f1
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
@@ -0,0 +1,27 @@
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+std::mutex mux;
+std::condition_variable cv;
+bool ready = false;
+
+static int my_add(int a, int b) { // breakpoint 1
+  std::unique_lock<std::mutex> lk(mux);
+  cv.wait(lk, [] { return ready; });
+  return a + b;
+}
+
+int main(int argc, char const *argv[]) {
+  std::thread t1(my_add, 1, 2);
+  std::thread t2(my_add, 4, 5);
+
+  {
+    std::lock_guard<std::mutex> lk(mux);
+    ready = true;
+    cv.notify_all();
+  }
+  t1.join();
+  t2.join();
+  return 0;
+}
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp 
b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index 1bc656b0458b2..70722bc4ea0ca 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -68,6 +68,8 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
 
 [[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) {
   switch (SR) {
+  case eStopReasonEmpty:
+    return "";
   case eStopReasonStep:
     return "step";
   case eStopReasonBreakpoint:
@@ -86,8 +88,6 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
     return "data breakpoint";
   case eStopReasonInstructionBreakpoint:
     return "instruction breakpoint";
-  case eStopReasonInvalid:
-    return "";
   }
 }
 
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h 
b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
index 230d28f7e2810..c305499084021 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
@@ -118,7 +118,7 @@ struct MemoryEventBody {
 llvm::json::Value toJSON(const MemoryEventBody &);
 
 enum StopReason : unsigned {
-  eStopReasonInvalid,
+  eStopReasonEmpty,
   eStopReasonStep,
   eStopReasonBreakpoint,
   eStopReasonException,
@@ -140,7 +140,7 @@ struct StoppedEventBody {
   ///
   /// For backward compatibility this string is shown in the UI if the
   /// `description` attribute is missing (but it must not be translated).
-  StopReason reason = eStopReasonInvalid;
+  StopReason reason = eStopReasonEmpty;
 
   /// The full reason for the event, e.g. 'Paused on exception'. This string is
   /// shown in the UI as is and can be translated.

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to