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 01/12] [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 02/12] 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 03/12] 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. >From 0eab16ea51c3ba3fc96e37c875e280ca3fca501f Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Fri, 16 Jan 2026 16:52:56 -0800 Subject: [PATCH 04/12] Moved creating the event body into its own function again. --- lldb/tools/lldb-dap/EventHelper.cpp | 81 +++++++++++++++-------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index 2bc64fe8d2582..888c959414fef 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -175,39 +175,7 @@ void SendProcessEvent(DAP &dap, LaunchMethod launch_method) { dap.SendJSON(llvm::json::Value(std::move(event))); } -// Send a thread stopped event for all threads as long as the process -// is stopped. -llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { - lldb::SBMutex lock = dap.GetAPIMutex(); - std::lock_guard<lldb::SBMutex> guard(lock); - - lldb::SBProcess process = dap.target.GetProcess(); - if (!process.IsValid()) - return make_error<DAPError>("invalid process"); - - lldb::StateType state = process.GetState(); - if (!lldb::SBDebugger::StateIsStoppedState(state)) - return make_error<NotStoppedError>(); - - llvm::DenseSet<lldb::tid_t> old_thread_ids; - old_thread_ids.swap(dap.thread_ids); - const uint32_t num_threads = process.GetNumThreads(); - - 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); - 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; - } - - lldb::SBThread thread = process.GetThreadAtIndex(stopped_thread_idx); - assert(thread.IsValid() && "no valid thread found, process not stopped"); - +static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) { protocol::StoppedEventBody body; if (on_entry) { body.reason = protocol::eStopReasonEntry; @@ -267,8 +235,7 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { case lldb::eStopReasonThreadExiting: case lldb::eStopReasonInvalid: case lldb::eStopReasonNone: - llvm_unreachable("invalid stop reason, thread is not stopped"); - break; + return; } } lldb::tid_t tid = thread.GetThreadID(); @@ -279,10 +246,48 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { body.preserveFocusHint = tid == dap.focus_tid; body.allThreadsStopped = true; - // Update focused thread. - dap.focus_tid = tid; - dap.Send(protocol::Event{"stopped", std::move(body)}); +} + +// Send a thread stopped event for the first stopped thread as the process is +// stopped. +llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { + lldb::SBMutex lock = dap.GetAPIMutex(); + std::lock_guard<lldb::SBMutex> guard(lock); + + lldb::SBProcess process = dap.target.GetProcess(); + if (!process.IsValid()) + return make_error<DAPError>("invalid process"); + + lldb::StateType state = process.GetState(); + if (!lldb::SBDebugger::StateIsStoppedState(state)) + return make_error<NotStoppedError>(); + + llvm::DenseSet<lldb::tid_t> old_thread_ids; + old_thread_ids.swap(dap.thread_ids); + const uint32_t num_threads = process.GetNumThreads(); + + lldb::SBThread stopped_thread; + for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { + lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); + // Collect all known thread ids for sending thread events. + dap.thread_ids.insert(thread.GetThreadID()); + + if (stopped_thread || !ThreadHasStopReason(thread)) + continue; + + // Stop at the first thread with a stop reason. + stopped_thread = thread; + } + + if (!stopped_thread) + return make_error<DAPError>( + "no valid thread found, cannot determine why the process is stopped"); + + SendStoppedEvent(dap, stopped_thread, on_entry); + + // Update focused thread. + dap.focus_tid = stopped_thread.GetThreadID(); for (const auto &tid : old_thread_ids) { auto end = dap.thread_ids.end(); >From 63d9c6081e886bcd76f546e27d530a6aa5618bb1 Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Fri, 16 Jan 2026 17:03:55 -0800 Subject: [PATCH 05/12] Addressing feedback. --- lldb/tools/lldb-dap/EventHelper.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index 888c959414fef..bd8fb9767f57b 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -183,6 +183,8 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) { switch (thread.GetStopReason()) { case lldb::eStopReasonTrace: case lldb::eStopReasonPlanComplete: + case lldb::eStopReasonProcessorTrace: + case lldb::eStopReasonHistoryBoundary: body.reason = protocol::eStopReasonStep; break; case lldb::eStopReasonBreakpoint: { @@ -212,12 +214,6 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) { 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: >From 4784f64d2a3ae4462be4bcc84246fa0955588d89 Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Fri, 16 Jan 2026 17:27:24 -0800 Subject: [PATCH 06/12] Adjusting the multiple thread stop event to more closely match other tests stopping at the same location and marking the test with the same skipped decorators. --- .../stop-events/TestDAP_stop_events.py | 28 +++++++++++++--- .../API/tools/lldb-dap/stop-events/main.cpp | 32 ++++++++++--------- .../lldb-dap/Protocol/ProtocolEvents.cpp | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) 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 index e766db2e47f59..8fa57fab4cf1c 100644 --- 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 @@ -17,14 +17,24 @@ def evaluate(self, command: str) -> str: self.assertTrue(result["success"]) return result["body"]["result"] + @expectedFailureAll( + oslist=["linux"], + bugnumber="llvm.org/pr15824 thread states not properly maintained", + ) + @expectedFailureAll( + oslist=["freebsd"], + bugnumber="llvm.org/pr18190 thread states not properly maintained", + ) + @expectedFailureNetBSD + @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 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]) + line = line_number("main.cpp", "breakpoint") + [bp] = self.set_source_breakpoints("main.cpp", [line]) events = self.continue_to_next_stop() self.assertEqual(len(events), 1, "Expected a single stopped event") @@ -32,7 +42,7 @@ def test_multiple_threads_sample_breakpoint(self): 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["hitBreakpointIds"], [int(bp)]) self.assertEqual(body["allThreadsStopped"], True) self.assertNotIn("preserveFocusHint", body) self.assertIsNotNone(body["threadId"]) @@ -55,13 +65,23 @@ def test_multiple_threads_sample_breakpoint(self): self.continue_to_exit() + @expectedFailureAll( + oslist=["linux"], + bugnumber="llvm.org/pr15824 thread states not properly maintained", + ) + @expectedFailureAll( + oslist=["freebsd"], + bugnumber="llvm.org/pr18190 thread states not properly maintained", + ) + @expectedFailureNetBSD + @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 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") + line_1 = line_number("main.cpp", "breakpoint") [bp1] = self.set_source_breakpoints("main.cpp", [line_1]) [bp2] = self.set_function_breakpoints(["my_add"]) diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp index 195dbeda5c2f1..4ad66cac33b08 100644 --- a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp +++ b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp @@ -1,27 +1,29 @@ -#include <condition_variable> -#include <mutex> +#include "pseudo_barrier.h" #include <thread> -std::mutex mux; -std::condition_variable cv; -bool ready = false; +pseudo_barrier_t g_barrier; -static int my_add(int a, int b) { // breakpoint 1 - std::unique_lock<std::mutex> lk(mux); - cv.wait(lk, [] { return ready; }); +static int my_add(int a, int b) { // breakpoint return a + b; } int main(int argc, char const *argv[]) { - std::thread t1(my_add, 1, 2); - std::thread t2(my_add, 4, 5); + // Don't let either thread do anything until they're both ready. + pseudo_barrier_init(g_barrier, 2); + + std::thread t1([] { + // Wait until both threads are running + pseudo_barrier_wait(g_barrier); + my_add(1, 2); + }); + std::thread t2([] { + // Wait until both threads are running + pseudo_barrier_wait(g_barrier); + 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 70722bc4ea0ca..f2455098f5ccf 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp @@ -66,7 +66,7 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) { {"count", MEB.count}}; } -[[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) { +static llvm::json::Value toJSON(const StopReason &SR) { switch (SR) { case eStopReasonEmpty: return ""; >From 9c6db170caac9606027e12338d244b439a84cf8a Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Tue, 20 Jan 2026 15:51:01 -0800 Subject: [PATCH 07/12] When the process is stopped, sand a stopped event for each stopped thread, but correctly mark 'preserveFocusedHint' and 'allThreadsStopped' to ensure the client correctly updates the event state. --- .../test/tools/lldb-dap/dap_server.py | 6 +- .../stop-events/TestDAP_stop_events.py | 114 -------------- .../{stop-events => stopped-events}/Makefile | 0 .../stopped-events/TestDAP_stopped_events.py | 149 ++++++++++++++++++ .../{stop-events => stopped-events}/main.cpp | 0 lldb/tools/lldb-dap/EventHelper.cpp | 33 ++-- 6 files changed, 174 insertions(+), 128 deletions(-) delete mode 100644 lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py rename lldb/test/API/tools/lldb-dap/{stop-events => stopped-events}/Makefile (100%) create mode 100644 lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py rename lldb/test/API/tools/lldb-dap/{stop-events => stopped-events}/main.cpp (100%) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index 875cfb6c6b197..031e6a24e0fe4 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -278,6 +278,7 @@ def __init__( self.stopped_thread: Optional[dict] = None self.thread_stacks: Optional[Dict[int, List[dict]]] self.thread_stop_reasons: Dict[str, Any] = {} + self.focused_tid = None self.frame_scopes: Dict[str, Any] = {} # keyed by breakpoint id self.resolved_breakpoints: dict[str, Breakpoint] = {} @@ -507,6 +508,8 @@ def _handle_event(self, packet: Event) -> None: self._process_stopped() tid = body["threadId"] self.thread_stop_reasons[tid] = body + if "preserveFocusHint" not in body or not body["preserveFocusHint"]: + self.focused_tid = tid elif event.startswith("progress"): # Progress events come in as 'progressStart', 'progressUpdate', # and 'progressEnd' events. Keep these around in case test @@ -567,6 +570,7 @@ def _process_continued(self, all_threads_continued: bool): self.frame_scopes = {} if all_threads_continued: self.thread_stop_reasons = {} + self.focused_tid = None def _update_verified_breakpoints(self, breakpoints: List[Breakpoint]): for bp in breakpoints: @@ -1530,7 +1534,7 @@ def request_threads(self): tid = thread["id"] if tid in self.thread_stop_reasons: thread_stop_info = self.thread_stop_reasons[tid] - copy_keys = ["reason", "description", "text"] + copy_keys = ["reason", "description", "text", "hitBreakpointIds"] for key in copy_keys: if key in thread_stop_info: thread[key] = thread_stop_info[key] 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 deleted file mode 100644 index 8fa57fab4cf1c..0000000000000 --- a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -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"] - - @expectedFailureAll( - oslist=["linux"], - bugnumber="llvm.org/pr15824 thread states not properly maintained", - ) - @expectedFailureAll( - oslist=["freebsd"], - bugnumber="llvm.org/pr18190 thread states not properly maintained", - ) - @expectedFailureNetBSD - @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 - 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 = line_number("main.cpp", "breakpoint") - [bp] = self.set_source_breakpoints("main.cpp", [line]) - - 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(bp)]) - 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() - - @expectedFailureAll( - oslist=["linux"], - bugnumber="llvm.org/pr15824 thread states not properly maintained", - ) - @expectedFailureAll( - oslist=["freebsd"], - bugnumber="llvm.org/pr18190 thread states not properly maintained", - ) - @expectedFailureNetBSD - @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 - 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") - [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/Makefile b/lldb/test/API/tools/lldb-dap/stopped-events/Makefile similarity index 100% rename from lldb/test/API/tools/lldb-dap/stop-events/Makefile rename to lldb/test/API/tools/lldb-dap/stopped-events/Makefile diff --git a/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py new file mode 100644 index 0000000000000..94dfa6f69c852 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py @@ -0,0 +1,149 @@ +""" +Test lldb-dap 'stopped' events. +""" + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase + + +class TestDAP_stopped_events(lldbdap_testcase.DAPTestCaseBase): + """ + Test validates different operations that produce 'stopped' events. + """ + + ANY_THREAD = {} + + def matches(self, a: dict, b: dict) -> bool: + """Returns true if 'a' is a subset of 'b', otherwise false.""" + return a | b == a + + def verify_threads(self, expected_threads): + threads_resp = self.dap_server.request_threads() + self.assertTrue(threads_resp["success"]) + threads = threads_resp["body"]["threads"] + self.assertEqual(len(threads), len(expected_threads)) + for idx, expected_thread in enumerate(expected_threads): + thread = threads[idx] + self.assertTrue( + self.matches(thread, expected_thread), + f"Invalid thread state in {threads_resp}", + ) + + @expectedFailureAll( + oslist=["linux"], + bugnumber="llvm.org/pr15824 thread states not properly maintained", + ) + @expectedFailureAll( + oslist=["freebsd"], + bugnumber="llvm.org/pr18190 thread states not properly maintained", + ) + @expectedFailureNetBSD + @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 + 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 = line_number("main.cpp", "breakpoint") + [bp] = self.set_source_breakpoints("main.cpp", [line]) + + events = self.continue_to_next_stop() + self.assertEqual(len(events), 2, "Expected exactly two 'stopped' events") + for event in events: + body = event["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(bp)]) + self.assertIsNotNone(body["threadId"]) + + # We should have three threads, something along the lines of: + # + # 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 + self.verify_threads( + [ + {}, + { + "reason": "breakpoint", + "text": "breakpoint 1.1", + "description": "breakpoint 1.1", + }, + { + "reason": "breakpoint", + "text": "breakpoint 1.1", + "description": "breakpoint 1.1", + }, + ] + ) + + self.assertEqual( + self.dap_server.threads[1]["id"], + self.dap_server.focused_tid, + "Expected thread#2 to be focused", + ) + + self.continue_to_exit() + + @expectedFailureAll( + oslist=["linux"], + bugnumber="llvm.org/pr15824 thread states not properly maintained", + ) + @expectedFailureAll( + oslist=["freebsd"], + bugnumber="llvm.org/pr18190 thread states not properly maintained", + ) + @expectedFailureNetBSD + @skipIfWindows # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373 + 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") + [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), 2, "Expected two stopped events") + for event in events: + body = event["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.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 + self.verify_threads( + [ + self.ANY_THREAD, + { + "reason": "breakpoint", + "description": "breakpoint 1.1 2.1", + "text": "breakpoint 1.1 2.1", + }, + { + "reason": "breakpoint", + "description": "breakpoint 1.1 2.1", + "text": "breakpoint 1.1 2.1", + }, + ] + ) + + self.assertEqual( + self.dap_server.threads[1]["id"], + self.dap_server.focused_tid, + "Expected thread#2 to be focused", + ) + + self.continue_to_exit() diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stopped-events/main.cpp similarity index 100% rename from lldb/test/API/tools/lldb-dap/stop-events/main.cpp rename to lldb/test/API/tools/lldb-dap/stopped-events/main.cpp diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index bd8fb9767f57b..166536125b1c3 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -25,6 +25,8 @@ #include "lldb/API/SBListener.h" #include "lldb/API/SBPlatform.h" #include "lldb/API/SBStream.h" +#include "lldb/API/SBThread.h" +#include "lldb/lldb-defines.h" #include "lldb/lldb-types.h" #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" @@ -175,7 +177,8 @@ void SendProcessEvent(DAP &dap, LaunchMethod launch_method) { dap.SendJSON(llvm::json::Value(std::move(event))); } -static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) { +static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry, + bool all_threads_stopped, bool preserve_focus) { protocol::StoppedEventBody body; if (on_entry) { body.reason = protocol::eStopReasonEntry; @@ -239,8 +242,8 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) { thread.GetStopDescription(description); body.description = {description.GetData(), description.GetSize()}; body.threadId = tid; - body.preserveFocusHint = tid == dap.focus_tid; - body.allThreadsStopped = true; + body.allThreadsStopped = all_threads_stopped; + body.preserveFocusHint = preserve_focus; dap.Send(protocol::Event{"stopped", std::move(body)}); } @@ -263,27 +266,31 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { old_thread_ids.swap(dap.thread_ids); const uint32_t num_threads = process.GetNumThreads(); - lldb::SBThread stopped_thread; + lldb::tid_t focused_tid = LLDB_INVALID_THREAD_ID; for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); // Collect all known thread ids for sending thread events. dap.thread_ids.insert(thread.GetThreadID()); - if (stopped_thread || !ThreadHasStopReason(thread)) + if (!ThreadHasStopReason(thread)) continue; - // Stop at the first thread with a stop reason. - stopped_thread = thread; - } + // When we stop, report allThreadsStopped for the *first* stopped thread to + // ensure the list of stopped threads is up to date. + bool first_stop = focused_tid == LLDB_INVALID_THREAD_ID; + SendStoppedEvent(dap, thread, on_entry, /*all_threads_stopped=*/first_stop, + /*preserve_focus=*/!first_stop); - if (!stopped_thread) - return make_error<DAPError>( - "no valid thread found, cannot determine why the process is stopped"); + // Default focus to the first stopped thread. + if (focused_tid == LLDB_INVALID_THREAD_ID) + focused_tid = thread.GetThreadID(); + } - SendStoppedEvent(dap, stopped_thread, on_entry); + if (focused_tid == LLDB_INVALID_THREAD_ID) + return make_error<DAPError>("no stopped threads"); // Update focused thread. - dap.focus_tid = stopped_thread.GetThreadID(); + dap.focus_tid = focused_tid; for (const auto &tid : old_thread_ids) { auto end = dap.thread_ids.end(); >From cf3842b7755f8e46ba57e5cee0811e638342a4ed Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Wed, 21 Jan 2026 10:14:20 -0800 Subject: [PATCH 08/12] Update unit tests to use TestUtilities PrettyPrint. --- lldb/unittests/DAP/ProtocolEventsTest.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp b/lldb/unittests/DAP/ProtocolEventsTest.cpp index bb7a1e9574fc8..91f999fa60bce 100644 --- a/lldb/unittests/DAP/ProtocolEventsTest.cpp +++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp @@ -13,19 +13,17 @@ using namespace llvm; using namespace lldb_dap::protocol; +using lldb_private::PrettyPrint; 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)); + EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body)); body.reason = eStopReasonBreakpoint; body.description = "desc"; @@ -42,5 +40,5 @@ TEST(ProtocolEventsTest, StoppedEventBody) { "hitBreakpointIds": [1, 2, 3] })"); ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded()); - EXPECT_EQ(pp(*expected_body), pp(body)); + EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body)); } >From 6182be04f26dd5b253eed922ee8b329532b71b7a Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Wed, 21 Jan 2026 10:16:28 -0800 Subject: [PATCH 09/12] Removing expected failure on linux, the tests are passing in CI. --- .../lldb-dap/stopped-events/TestDAP_stopped_events.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py index 94dfa6f69c852..03dff39c82ed9 100644 --- a/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py +++ b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py @@ -30,10 +30,6 @@ def verify_threads(self, expected_threads): f"Invalid thread state in {threads_resp}", ) - @expectedFailureAll( - oslist=["linux"], - bugnumber="llvm.org/pr15824 thread states not properly maintained", - ) @expectedFailureAll( oslist=["freebsd"], bugnumber="llvm.org/pr18190 thread states not properly maintained", @@ -89,10 +85,6 @@ def test_multiple_threads_sample_breakpoint(self): self.continue_to_exit() - @expectedFailureAll( - oslist=["linux"], - bugnumber="llvm.org/pr15824 thread states not properly maintained", - ) @expectedFailureAll( oslist=["freebsd"], bugnumber="llvm.org/pr18190 thread states not properly maintained", >From 5f8339d4de472c7585b3d56d77d99394c050a585 Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Wed, 21 Jan 2026 13:58:31 -0800 Subject: [PATCH 10/12] Updating verify_stop_exception_info. --- .../test/tools/lldb-dap/lldbdap_testcase.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 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 015feb40096c0..498e0f88c2346 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 @@ -210,7 +210,7 @@ def verify_all_breakpoints_hit(self, breakpoint_ids): def verify_stop_exception_info( self, expected_description: str, expected_text: Optional[str] = None - ): + ) -> None: """Wait for the process we are debugging to stop, and verify the stop reason is 'exception' and that the description matches 'expected_description' @@ -223,12 +223,13 @@ def verify_stop_exception_info( or stopped_event["body"]["reason"] != "exception" ): continue + body = stopped_event["body"] self.assertIn( "description", - stopped_event["body"], - f"stopped event missing description {stopped_event}", + body, + f"stopped event missing description {stopped_event!r}", ) - description = stopped_event["body"]["description"] + description = body["description"] self.assertRegex( description, expected_description, @@ -236,12 +237,12 @@ def verify_stop_exception_info( ) if expected_text: self.assertRegex( - stopped_event["body"]["text"], + body["text"], expected_text, f"for stopped event {stopped_event!r}", ) return - self.fail(f"No valid stop exception info detected in {stopped_events}") + self.fail(f"No valid stop exception info detected in {stopped_events!r}") def verify_stop_on_entry(self) -> None: """Waits for the process to be stopped and then verifies at least one >From fb40ab5d1e93aaa83d36ab52959f41333681566f Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Thu, 22 Jan 2026 14:24:26 -0800 Subject: [PATCH 11/12] Renaming `eStopReasonEmpty` > `eStopReasonUninitialized` and adding an assert to to the `toJSON` for validating we filled out the reason correctly. --- lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp | 3 ++- lldb/tools/lldb-dap/Protocol/ProtocolEvents.h | 4 ++-- lldb/unittests/DAP/ProtocolEventsTest.cpp | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp index f2455098f5ccf..1ced5f1095326 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp @@ -67,8 +67,9 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) { } static llvm::json::Value toJSON(const StopReason &SR) { + assert(SR != eStopReasonUninitialized && "StopReason Uninitialized"); switch (SR) { - case eStopReasonEmpty: + case eStopReasonUninitialized: return ""; case eStopReasonStep: return "step"; diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h index c305499084021..1f448bc1c317f 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 { - eStopReasonEmpty, + eStopReasonUninitialized, 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 = eStopReasonEmpty; + StopReason reason = eStopReasonUninitialized; /// The full reason for the event, e.g. 'Paused on exception'. This string is /// shown in the UI as is and can be translated. diff --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp b/lldb/unittests/DAP/ProtocolEventsTest.cpp index 91f999fa60bce..30c457b72e14c 100644 --- a/lldb/unittests/DAP/ProtocolEventsTest.cpp +++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp @@ -19,8 +19,9 @@ using llvm::json::Value; TEST(ProtocolEventsTest, StoppedEventBody) { StoppedEventBody body; + body.reason = lldb_dap::protocol::eStopReasonBreakpoint; Expected<Value> expected_body = parse(R"({ - "reason": "" + "reason": "breakpoint" })"); ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded()); EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body)); >From 3afa93204f597a3dcad1ce46b4807d5bdde1753c Mon Sep 17 00:00:00 2001 From: John Harrison <[email protected]> Date: Fri, 23 Jan 2026 09:17:35 -0800 Subject: [PATCH 12/12] Renaming StopReason > StoppedReason in lldb_dap::protocol --- lldb/tools/lldb-dap/EventHelper.cpp | 18 +++++++------- .../lldb-dap/Protocol/ProtocolEvents.cpp | 24 +++++++++---------- lldb/tools/lldb-dap/Protocol/ProtocolEvents.h | 24 +++++++++---------- lldb/unittests/DAP/ProtocolEventsTest.cpp | 4 ++-- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index bf16d246e817a..5a4275873e93b 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -181,25 +181,25 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry, bool all_threads_stopped, bool preserve_focus) { protocol::StoppedEventBody body; if (on_entry) { - body.reason = protocol::eStopReasonEntry; + body.reason = protocol::eStoppedReasonEntry; } else { switch (thread.GetStopReason()) { case lldb::eStopReasonTrace: case lldb::eStopReasonPlanComplete: case lldb::eStopReasonProcessorTrace: case lldb::eStopReasonHistoryBoundary: - body.reason = protocol::eStopReasonStep; + body.reason = protocol::eStoppedReasonStep; break; case lldb::eStopReasonBreakpoint: { ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread); if (exc_bp) { - body.reason = protocol::eStopReasonException; + body.reason = protocol::eStoppedReasonException; body.text = exc_bp->GetLabel(); } else { InstructionBreakpoint *inst_bp = dap.GetInstructionBPFromStopReason(thread); - body.reason = inst_bp ? protocol::eStopReasonInstructionBreakpoint - : protocol::eStopReasonBreakpoint; + body.reason = inst_bp ? protocol::eStoppedReasonInstructionBreakpoint + : protocol::eStoppedReasonBreakpoint; llvm::raw_string_ostream OS(body.text); OS << "breakpoint"; @@ -212,7 +212,7 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry, } } break; case lldb::eStopReasonWatchpoint: { - body.reason = protocol::eStopReasonDataBreakpoint; + body.reason = protocol::eStoppedReasonDataBreakpoint; 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(); @@ -220,16 +220,16 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry, case lldb::eStopReasonSignal: case lldb::eStopReasonException: case lldb::eStopReasonInstrumentation: - body.reason = protocol::eStopReasonException; + body.reason = protocol::eStoppedReasonException; break; case lldb::eStopReasonExec: case lldb::eStopReasonFork: case lldb::eStopReasonVFork: case lldb::eStopReasonVForkDone: - body.reason = protocol::eStopReasonEntry; + body.reason = protocol::eStoppedReasonEntry; break; case lldb::eStopReasonInterrupt: - body.reason = protocol::eStopReasonPause; + body.reason = protocol::eStoppedReasonPause; break; case lldb::eStopReasonThreadExiting: case lldb::eStopReasonInvalid: diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp index 1ced5f1095326..b1985cbb7d053 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp @@ -66,28 +66,28 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) { {"count", MEB.count}}; } -static llvm::json::Value toJSON(const StopReason &SR) { - assert(SR != eStopReasonUninitialized && "StopReason Uninitialized"); +static llvm::json::Value toJSON(const StoppedReason &SR) { + assert(SR != eStoppedReasonUninitialized && "StopReason Uninitialized"); switch (SR) { - case eStopReasonUninitialized: + case eStoppedReasonUninitialized: return ""; - case eStopReasonStep: + case eStoppedReasonStep: return "step"; - case eStopReasonBreakpoint: + case eStoppedReasonBreakpoint: return "breakpoint"; - case eStopReasonException: + case eStoppedReasonException: return "exception"; - case eStopReasonPause: + case eStoppedReasonPause: return "pause"; - case eStopReasonEntry: + case eStoppedReasonEntry: return "entry"; - case eStopReasonGoto: + case eStoppedReasonGoto: return "goto"; - case eStopReasonFunctionBreakpoint: + case eStoppedReasonFunctionBreakpoint: return "function breakpoint"; - case eStopReasonDataBreakpoint: + case eStoppedReasonDataBreakpoint: return "data breakpoint"; - case eStopReasonInstructionBreakpoint: + case eStoppedReasonInstructionBreakpoint: return "instruction breakpoint"; } } diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h index 1f448bc1c317f..5c415f76c37fd 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h @@ -117,17 +117,17 @@ struct MemoryEventBody { }; llvm::json::Value toJSON(const MemoryEventBody &); -enum StopReason : unsigned { - eStopReasonUninitialized, - eStopReasonStep, - eStopReasonBreakpoint, - eStopReasonException, - eStopReasonPause, - eStopReasonEntry, - eStopReasonGoto, - eStopReasonFunctionBreakpoint, - eStopReasonDataBreakpoint, - eStopReasonInstructionBreakpoint, +enum StoppedReason : unsigned { + eStoppedReasonUninitialized, + eStoppedReasonStep, + eStoppedReasonBreakpoint, + eStoppedReasonException, + eStoppedReasonPause, + eStoppedReasonEntry, + eStoppedReasonGoto, + eStoppedReasonFunctionBreakpoint, + eStoppedReasonDataBreakpoint, + eStoppedReasonInstructionBreakpoint, }; /// The event indicates that the execution of the debuggee has stopped due to @@ -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 = eStopReasonUninitialized; + StoppedReason reason = eStoppedReasonUninitialized; /// The full reason for the event, e.g. 'Paused on exception'. This string is /// shown in the UI as is and can be translated. diff --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp b/lldb/unittests/DAP/ProtocolEventsTest.cpp index 30c457b72e14c..b6efc2791e578 100644 --- a/lldb/unittests/DAP/ProtocolEventsTest.cpp +++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp @@ -19,14 +19,14 @@ using llvm::json::Value; TEST(ProtocolEventsTest, StoppedEventBody) { StoppedEventBody body; - body.reason = lldb_dap::protocol::eStopReasonBreakpoint; + body.reason = lldb_dap::protocol::eStoppedReasonBreakpoint; Expected<Value> expected_body = parse(R"({ "reason": "breakpoint" })"); ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded()); EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body)); - body.reason = eStopReasonBreakpoint; + body.reason = eStoppedReasonBreakpoint; body.description = "desc"; body.text = "text"; body.preserveFocusHint = true; _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
