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
