[lldb-dap] Correctly report breakpoints as resolved on macOS (#129589)

On macOS, breakpoints are briefly unresolved between process launch and
when the dynamic loader has informed us about the loaded libraries. This
information was being forwarded by lldb-dap, but only partially. In the
event handler, we were listening for the `LocationsAdded` and
`LocationsRemoved` breakpoint events. For the scenario described above,
the latter would trigger and we'd send an event reporting the breakpoint
as unresolved. The problem is that when the breakpoint location is
resolved again, you receive a `LocationsResolved` event, not a
`LocationsAdded` event. As a result, the breakpoint would continue to
show up as unresolved in the DAP client.

I found a test that tried to test part of this behavior, but the test
was broken and disabled. I revived the test and added coverage for the
situation described above.

Fixes #112629
rdar://137968318
This commit is contained in:
Jonas Devlieghere 2025-03-03 18:56:21 -06:00 committed by GitHub
parent 6041c745f3
commit d654d37c86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 68 deletions

View File

@ -2,7 +2,6 @@
Test lldb-dap setBreakpoints request Test lldb-dap setBreakpoints request
""" """
import dap_server import dap_server
from lldbsuite.test.decorators import * from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import * from lldbsuite.test.lldbtest import *
@ -12,9 +11,7 @@ import os
class TestDAP_breakpointEvents(lldbdap_testcase.DAPTestCaseBase): class TestDAP_breakpointEvents(lldbdap_testcase.DAPTestCaseBase):
@skipIfWindows
@skipUnlessDarwin @skipUnlessDarwin
@expectedFailureAll(macos_version=[">=", "13.0"])
def test_breakpoint_events(self): def test_breakpoint_events(self):
""" """
This test sets a breakpoint in a shared library and runs and stops This test sets a breakpoint in a shared library and runs and stops
@ -63,70 +60,73 @@ class TestDAP_breakpointEvents(lldbdap_testcase.DAPTestCaseBase):
response = self.dap_server.request_setBreakpoints( response = self.dap_server.request_setBreakpoints(
main_source_path, [main_bp_line] main_source_path, [main_bp_line]
) )
if response: self.assertTrue(response)
breakpoints = response["body"]["breakpoints"] breakpoints = response["body"]["breakpoints"]
for breakpoint in breakpoints: for breakpoint in breakpoints:
main_bp_id = breakpoint["id"] main_bp_id = breakpoint["id"]
dap_breakpoint_ids.append("%i" % (main_bp_id)) dap_breakpoint_ids.append("%i" % (main_bp_id))
# line = breakpoint['line'] self.assertTrue(
self.assertTrue( breakpoint["verified"], "expect main breakpoint to be verified"
breakpoint["verified"], "expect main breakpoint to be verified" )
)
response = self.dap_server.request_setBreakpoints( response = self.dap_server.request_setBreakpoints(
foo_source_path, [foo_bp1_line] foo_source_path, [foo_bp1_line]
) )
if response: self.assertTrue(response)
breakpoints = response["body"]["breakpoints"] breakpoints = response["body"]["breakpoints"]
for breakpoint in breakpoints: for breakpoint in breakpoints:
foo_bp_id = breakpoint["id"] foo_bp_id = breakpoint["id"]
dap_breakpoint_ids.append("%i" % (foo_bp_id)) dap_breakpoint_ids.append("%i" % (foo_bp_id))
self.assertFalse( self.assertFalse(
breakpoint["verified"], "expect foo breakpoint to not be verified" breakpoint["verified"], "expect foo breakpoint to not be verified"
) )
# Get the stop at the entry point # Get the stop at the entry point
self.continue_to_next_stop() self.continue_to_next_stop()
# We are now stopped at the entry point to the program. Shared # We are now stopped at the entry point to the program. Shared
# libraries are not loaded yet (at least on macOS they aren't) and any # libraries are not loaded yet (at least on macOS they aren't) and only
# breakpoints set in foo.cpp should not be resolved. # the breakpoint in the main executable should be resolved.
self.assertEqual(len(self.dap_server.breakpoint_events), 1)
event = self.dap_server.breakpoint_events[0]
body = event["body"]
self.assertEqual( self.assertEqual(
len(self.dap_server.breakpoint_events), body["reason"], "changed", "breakpoint event should say changed"
0,
"no breakpoint events when stopped at entry point",
) )
breakpoint = body["breakpoint"]
self.assertEqual(breakpoint["id"], main_bp_id)
self.assertTrue(breakpoint["verified"], "main breakpoint should be resolved")
# Clear the list of breakpoint events so we don't see this one again.
self.dap_server.breakpoint_events.clear()
# Continue to the breakpoint # Continue to the breakpoint
self.continue_to_breakpoints(dap_breakpoint_ids) self.continue_to_breakpoints(dap_breakpoint_ids)
# Make sure we only get an event for the breakpoint we set via a call # When the process launches, we first expect to see both the main and
# to self.dap_server.request_setBreakpoints(...), not the breakpoint # foo breakpoint as unresolved.
# we set with with a LLDB command in preRunCommands. for event in self.dap_server.breakpoint_events[:2]:
self.assertEqual( body = event["body"]
len(self.dap_server.breakpoint_events), self.assertEqual(
1, body["reason"], "changed", "breakpoint event should say changed"
"make sure we got a breakpoint event", )
) breakpoint = body["breakpoint"]
event = self.dap_server.breakpoint_events[0] self.assertIn(str(breakpoint["id"]), dap_breakpoint_ids)
# Verify the details of the breakpoint changed notification. self.assertFalse(breakpoint["verified"], "breakpoint should be unresolved")
body = event["body"]
self.assertEqual( # Then, once the dynamic loader has given us a load address, they
body["reason"], "changed", "breakpoint event is says breakpoint is changed" # should show up as resolved again.
) for event in self.dap_server.breakpoint_events[3:]:
breakpoint = body["breakpoint"] body = event["body"]
self.assertTrue( self.assertEqual(
breakpoint["verified"], "breakpoint event is says it is verified" body["reason"], "changed", "breakpoint event should say changed"
) )
self.assertEqual( breakpoint = body["breakpoint"]
breakpoint["id"], self.assertIn(str(breakpoint["id"]), dap_breakpoint_ids)
foo_bp_id, self.assertTrue(breakpoint["verified"], "breakpoint should be resolved")
"breakpoint event is for breakpoint %i" % (foo_bp_id), self.assertNotIn(
) "source",
self.assertTrue( breakpoint,
"line" in breakpoint and breakpoint["line"] > 0, "breakpoint event should not return a source object",
"breakpoint event is has a line number", )
) self.assertIn("line", breakpoint, "breakpoint event should have line")
self.assertNotIn(
"source", breakpoint, "breakpoint event should not return a source object"
)

View File

@ -137,27 +137,29 @@ static void EventThreadFunction(DAP &dap) {
lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event); lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event);
auto bp = Breakpoint( auto bp = Breakpoint(
dap, lldb::SBBreakpoint::GetBreakpointFromEvent(event)); dap, lldb::SBBreakpoint::GetBreakpointFromEvent(event));
// If the breakpoint was originated from the IDE, it will have the // If the breakpoint was set through DAP, it will have the
// BreakpointBase::GetBreakpointLabel() label attached. Regardless // BreakpointBase::GetBreakpointLabel() label. Regardless
// of wether the locations were added or removed, the breakpoint // of whether locations were added, removed, or resolved, the
// ins't going away, so we the reason is always "changed". // breakpoint isn't going away and the reason is always "changed".
if ((event_type & lldb::eBreakpointEventTypeLocationsAdded || if ((event_type & lldb::eBreakpointEventTypeLocationsAdded ||
event_type & lldb::eBreakpointEventTypeLocationsRemoved) && event_type & lldb::eBreakpointEventTypeLocationsRemoved ||
event_type & lldb::eBreakpointEventTypeLocationsResolved) &&
bp.MatchesName(BreakpointBase::GetBreakpointLabel())) { bp.MatchesName(BreakpointBase::GetBreakpointLabel())) {
auto bp_event = CreateEventObject("breakpoint"); // As the DAP client already knows the path of this breakpoint, we
llvm::json::Object body; // don't need to send it back as part of the "changed" event. This
// As VSCode already knows the path of this breakpoint, we don't // avoids sending paths that should be source mapped. Note that
// need to send it back as part of a "changed" event. This // CreateBreakpoint doesn't apply source mapping and certain
// prevent us from sending to VSCode paths that should be source // implementation ignore the source part of this event anyway.
// mapped. Note that CreateBreakpoint doesn't apply source mapping.
// Besides, the current implementation of VSCode ignores the
// "source" element of breakpoint events.
llvm::json::Value source_bp = CreateBreakpoint(&bp); llvm::json::Value source_bp = CreateBreakpoint(&bp);
source_bp.getAsObject()->erase("source"); source_bp.getAsObject()->erase("source");
llvm::json::Object body;
body.try_emplace("breakpoint", source_bp); body.try_emplace("breakpoint", source_bp);
body.try_emplace("reason", "changed"); body.try_emplace("reason", "changed");
llvm::json::Object bp_event = CreateEventObject("breakpoint");
bp_event.try_emplace("body", std::move(body)); bp_event.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(bp_event))); dap.SendJSON(llvm::json::Value(std::move(bp_event)));
} }
} }