Skip to content

Commit 8e6fa15

Browse files
authored
[lldb-dap] Support column breakpoints (llvm#125347)
This commit adds support for column breakpoints to lldb-dap To do so, support for the `breakpointLocations` request was added. To find all available breakpoint positions, we iterate over the line table. The `setBreakpoints` request already forwarded the column correctly to `SBTarget::BreakpointCreateByLocation`. However, `SourceBreakpointMap` did not keep track of multiple breakpoints in the same line. To do so, the `SourceBreakpointMap` is now indexed by line+column instead of by line only. This was previously submitted as llvm#113787, but got reverted due to failures on ARM and macOS. This second attempt has less strict test case expectations. Also, I added a release note.
1 parent ea9e174 commit 8e6fa15

File tree

8 files changed

+425
-82
lines changed

8 files changed

+425
-82
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,28 @@ def request_attach(
612612
command_dict = {"command": "attach", "type": "request", "arguments": args_dict}
613613
return self.send_recv(command_dict)
614614

615+
def request_breakpointLocations(
616+
self, file_path, line, end_line=None, column=None, end_column=None
617+
):
618+
(dir, base) = os.path.split(file_path)
619+
source_dict = {"name": base, "path": file_path}
620+
args_dict = {}
621+
args_dict["source"] = source_dict
622+
if line is not None:
623+
args_dict["line"] = line
624+
if end_line is not None:
625+
args_dict["endLine"] = end_line
626+
if column is not None:
627+
args_dict["column"] = column
628+
if end_column is not None:
629+
args_dict["endColumn"] = end_column
630+
command_dict = {
631+
"command": "breakpointLocations",
632+
"type": "request",
633+
"arguments": args_dict,
634+
}
635+
return self.send_recv(command_dict)
636+
615637
def request_configurationDone(self):
616638
command_dict = {
617639
"command": "configurationDone",
@@ -851,6 +873,8 @@ def request_next(self, threadId, granularity="statement"):
851873
def request_stepIn(self, threadId, targetId, granularity="statement"):
852874
if self.exit_status is not None:
853875
raise ValueError("request_stepIn called after process exited")
876+
if threadId is None:
877+
threadId = self.get_thread_id()
854878
args_dict = {
855879
"threadId": threadId,
856880
"targetId": targetId,
@@ -911,18 +935,14 @@ def request_setBreakpoints(self, file_path, line_array, data=None):
911935
breakpoint_data = data[i]
912936
bp = {"line": line}
913937
if breakpoint_data is not None:
914-
if "condition" in breakpoint_data and breakpoint_data["condition"]:
938+
if breakpoint_data.get("condition"):
915939
bp["condition"] = breakpoint_data["condition"]
916-
if (
917-
"hitCondition" in breakpoint_data
918-
and breakpoint_data["hitCondition"]
919-
):
940+
if breakpoint_data.get("hitCondition"):
920941
bp["hitCondition"] = breakpoint_data["hitCondition"]
921-
if (
922-
"logMessage" in breakpoint_data
923-
and breakpoint_data["logMessage"]
924-
):
942+
if breakpoint_data.get("logMessage"):
925943
bp["logMessage"] = breakpoint_data["logMessage"]
944+
if breakpoint_data.get("column"):
945+
bp["column"] = breakpoint_data["column"]
926946
breakpoints.append(bp)
927947
args_dict["breakpoints"] = breakpoints
928948

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ def set_global(self, name, value, id=None):
238238
def stepIn(
239239
self, threadId=None, targetId=None, waitForStop=True, granularity="statement"
240240
):
241-
self.dap_server.request_stepIn(
241+
response = self.dap_server.request_stepIn(
242242
threadId=threadId, targetId=targetId, granularity=granularity
243243
)
244+
self.assertTrue(response["success"])
244245
if waitForStop:
245246
return self.dap_server.wait_for_stopped()
246247
return None

lldb/test/API/tools/lldb-dap/breakpoint/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ main-copy.cpp: main.cpp
1616
# The following shared library will be used to test breakpoints under dynamic loading
1717
libother: other-copy.c
1818
"$(MAKE)" -f $(MAKEFILE_RULES) \
19-
DYLIB_ONLY=YES DYLIB_C_SOURCES=other-copy.c DYLIB_NAME=other
19+
DYLIB_ONLY=YES DYLIB_C_SOURCES=other-copy.c DYLIB_NAME=other
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
Test lldb-dap breakpointLocations request
3+
"""
4+
5+
6+
import dap_server
7+
import shutil
8+
from lldbsuite.test.decorators import *
9+
from lldbsuite.test.lldbtest import *
10+
from lldbsuite.test import lldbutil
11+
import lldbdap_testcase
12+
import os
13+
14+
15+
class TestDAP_breakpointLocations(lldbdap_testcase.DAPTestCaseBase):
16+
def setUp(self):
17+
lldbdap_testcase.DAPTestCaseBase.setUp(self)
18+
19+
self.main_basename = "main-copy.cpp"
20+
self.main_path = os.path.realpath(self.getBuildArtifact(self.main_basename))
21+
22+
@skipIfWindows
23+
def test_column_breakpoints(self):
24+
"""Test retrieving the available breakpoint locations."""
25+
program = self.getBuildArtifact("a.out")
26+
self.build_and_launch(program, stopOnEntry=True)
27+
loop_line = line_number(self.main_path, "// break loop")
28+
self.dap_server.request_continue()
29+
30+
# Ask for the breakpoint locations based only on the line number
31+
response = self.dap_server.request_breakpointLocations(
32+
self.main_path, loop_line
33+
)
34+
self.assertTrue(response["success"])
35+
self.assertEqual(
36+
response["body"]["breakpoints"],
37+
[
38+
{"line": loop_line, "column": 9},
39+
{"line": loop_line, "column": 13},
40+
{"line": loop_line, "column": 20},
41+
{"line": loop_line, "column": 23},
42+
{"line": loop_line, "column": 25},
43+
{"line": loop_line, "column": 34},
44+
{"line": loop_line, "column": 37},
45+
{"line": loop_line, "column": 39},
46+
{"line": loop_line, "column": 51},
47+
],
48+
)
49+
50+
# Ask for the breakpoint locations for a column range
51+
response = self.dap_server.request_breakpointLocations(
52+
self.main_path,
53+
loop_line,
54+
column=24,
55+
end_column=46,
56+
)
57+
self.assertTrue(response["success"])
58+
self.assertEqual(
59+
response["body"]["breakpoints"],
60+
[
61+
{"line": loop_line, "column": 25},
62+
{"line": loop_line, "column": 34},
63+
{"line": loop_line, "column": 37},
64+
{"line": loop_line, "column": 39},
65+
],
66+
)
67+
68+
# Ask for the breakpoint locations for a range of line numbers
69+
response = self.dap_server.request_breakpointLocations(
70+
self.main_path,
71+
line=loop_line,
72+
end_line=loop_line + 2,
73+
column=39,
74+
)
75+
self.maxDiff = None
76+
self.assertTrue(response["success"])
77+
# On some systems, there is an additional breakpoint available
78+
# at line 41, column 3, i.e. at the end of the loop. To make this
79+
# test more portable, only check that all expected breakpoints are
80+
# presented, but also accept additional breakpoints.
81+
expected_breakpoints = [
82+
{"column": 39, "line": 40},
83+
{"column": 51, "line": 40},
84+
{"column": 3, "line": 42},
85+
{"column": 18, "line": 42},
86+
]
87+
for bp in expected_breakpoints:
88+
self.assertIn(bp, response["body"]["breakpoints"])

lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setBreakpoints.py

Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,18 @@ def test_set_and_clear(self):
125125
# Set 3 breakpoints and verify that they got set correctly
126126
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
127127
line_to_id = {}
128-
if response:
129-
breakpoints = response["body"]["breakpoints"]
130-
self.assertEqual(
131-
len(breakpoints),
132-
len(lines),
133-
"expect %u source breakpoints" % (len(lines)),
134-
)
135-
for breakpoint, index in zip(breakpoints, range(len(lines))):
136-
line = breakpoint["line"]
137-
self.assertTrue(line, lines[index])
138-
# Store the "id" of the breakpoint that was set for later
139-
line_to_id[line] = breakpoint["id"]
140-
self.assertIn(line, lines, "line expected in lines array")
141-
self.assertTrue(breakpoint["verified"], "expect breakpoint verified")
128+
breakpoints = response["body"]["breakpoints"]
129+
self.assertEqual(
130+
len(breakpoints),
131+
len(lines),
132+
"expect %u source breakpoints" % (len(lines)),
133+
)
134+
for index, breakpoint in enumerate(breakpoints):
135+
line = breakpoint["line"]
136+
self.assertEqual(line, lines[index])
137+
# Store the "id" of the breakpoint that was set for later
138+
line_to_id[line] = breakpoint["id"]
139+
self.assertTrue(breakpoint["verified"], "expect breakpoint verified")
142140

143141
# There is no breakpoint delete packet, clients just send another
144142
# setBreakpoints packet with the same source file with fewer lines.
@@ -151,75 +149,66 @@ def test_set_and_clear(self):
151149
# Set 2 breakpoints and verify that the previous breakpoints that were
152150
# set above are still set.
153151
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
154-
if response:
155-
breakpoints = response["body"]["breakpoints"]
152+
breakpoints = response["body"]["breakpoints"]
153+
self.assertEqual(
154+
len(breakpoints),
155+
len(lines),
156+
"expect %u source breakpoints" % (len(lines)),
157+
)
158+
for index, breakpoint in enumerate(breakpoints):
159+
line = breakpoint["line"]
160+
self.assertEqual(line, lines[index])
161+
# Verify the same breakpoints are still set within LLDB by
162+
# making sure the breakpoint ID didn't change
156163
self.assertEqual(
157-
len(breakpoints),
158-
len(lines),
159-
"expect %u source breakpoints" % (len(lines)),
164+
line_to_id[line],
165+
breakpoint["id"],
166+
"verify previous breakpoints stayed the same",
160167
)
161-
for breakpoint, index in zip(breakpoints, range(len(lines))):
162-
line = breakpoint["line"]
163-
self.assertTrue(line, lines[index])
164-
# Verify the same breakpoints are still set within LLDB by
165-
# making sure the breakpoint ID didn't change
166-
self.assertEqual(
167-
line_to_id[line],
168-
breakpoint["id"],
169-
"verify previous breakpoints stayed the same",
170-
)
171-
self.assertIn(line, lines, "line expected in lines array")
172-
self.assertTrue(
173-
breakpoint["verified"], "expect breakpoint still verified"
174-
)
168+
self.assertTrue(breakpoint["verified"], "expect breakpoint still verified")
175169

176170
# Now get the full list of breakpoints set in the target and verify
177171
# we have only 2 breakpoints set. The response above could have told
178172
# us about 2 breakpoints, but we want to make sure we don't have the
179173
# third one still set in the target
180174
response = self.dap_server.request_testGetTargetBreakpoints()
181-
if response:
182-
breakpoints = response["body"]["breakpoints"]
175+
breakpoints = response["body"]["breakpoints"]
176+
self.assertEqual(
177+
len(breakpoints),
178+
len(lines),
179+
"expect %u source breakpoints" % (len(lines)),
180+
)
181+
for breakpoint in breakpoints:
182+
line = breakpoint["line"]
183+
# Verify the same breakpoints are still set within LLDB by
184+
# making sure the breakpoint ID didn't change
183185
self.assertEqual(
184-
len(breakpoints),
185-
len(lines),
186-
"expect %u source breakpoints" % (len(lines)),
186+
line_to_id[line],
187+
breakpoint["id"],
188+
"verify previous breakpoints stayed the same",
187189
)
188-
for breakpoint in breakpoints:
189-
line = breakpoint["line"]
190-
# Verify the same breakpoints are still set within LLDB by
191-
# making sure the breakpoint ID didn't change
192-
self.assertEqual(
193-
line_to_id[line],
194-
breakpoint["id"],
195-
"verify previous breakpoints stayed the same",
196-
)
197-
self.assertIn(line, lines, "line expected in lines array")
198-
self.assertTrue(
199-
breakpoint["verified"], "expect breakpoint still verified"
200-
)
190+
self.assertIn(line, lines, "line expected in lines array")
191+
self.assertTrue(breakpoint["verified"], "expect breakpoint still verified")
201192

202193
# Now clear all breakpoints for the source file by passing down an
203194
# empty lines array
204195
lines = []
205196
response = self.dap_server.request_setBreakpoints(self.main_path, lines)
206-
if response:
207-
breakpoints = response["body"]["breakpoints"]
208-
self.assertEqual(
209-
len(breakpoints),
210-
len(lines),
211-
"expect %u source breakpoints" % (len(lines)),
212-
)
197+
breakpoints = response["body"]["breakpoints"]
198+
self.assertEqual(
199+
len(breakpoints),
200+
len(lines),
201+
"expect %u source breakpoints" % (len(lines)),
202+
)
213203

214204
# Verify with the target that all breakpoints have been cleared
215205
response = self.dap_server.request_testGetTargetBreakpoints()
216-
if response:
217-
breakpoints = response["body"]["breakpoints"]
218-
self.assertEqual(
219-
len(breakpoints),
220-
len(lines),
221-
"expect %u source breakpoints" % (len(lines)),
222-
)
206+
breakpoints = response["body"]["breakpoints"]
207+
self.assertEqual(
208+
len(breakpoints),
209+
len(lines),
210+
"expect %u source breakpoints" % (len(lines)),
211+
)
223212

224213
# Now set a breakpoint again in the same source file and verify it
225214
# was added.
@@ -281,12 +270,11 @@ def test_clear_breakpoints_unset_breakpoints(self):
281270
self.assertEqual(
282271
len(breakpoints), len(lines), "expect %u source breakpoints" % (len(lines))
283272
)
284-
for breakpoint, index in zip(breakpoints, range(len(lines))):
273+
for index, breakpoint in enumerate(breakpoints):
285274
line = breakpoint["line"]
286-
self.assertTrue(line, lines[index])
275+
self.assertEqual(line, lines[index])
287276
# Store the "id" of the breakpoint that was set for later
288277
line_to_id[line] = breakpoint["id"]
289-
self.assertIn(line, lines, "line expected in lines array")
290278
self.assertTrue(breakpoint["verified"], "expect breakpoint verified")
291279

292280
# Now clear all breakpoints for the source file by not setting the
@@ -356,3 +344,49 @@ def test_functionality(self):
356344
self.continue_to_breakpoints(breakpoint_ids)
357345
i = int(self.dap_server.get_local_variable_value("i"))
358346
self.assertEqual(i, 7, "i != 7 showing post hitCondition hits every time")
347+
348+
@skipIfWindows
349+
def test_column_breakpoints(self):
350+
"""Test setting multiple breakpoints in the same line at different columns."""
351+
loop_line = line_number("main.cpp", "// break loop")
352+
353+
program = self.getBuildArtifact("a.out")
354+
self.build_and_launch(program)
355+
356+
# Set two breakpoints on the loop line at different columns.
357+
columns = [13, 39]
358+
response = self.dap_server.request_setBreakpoints(
359+
self.main_path, [loop_line, loop_line], list({"column": c} for c in columns)
360+
)
361+
362+
# Verify the breakpoints were set correctly
363+
breakpoints = response["body"]["breakpoints"]
364+
breakpoint_ids = []
365+
self.assertEqual(
366+
len(breakpoints),
367+
len(columns),
368+
"expect %u source breakpoints" % (len(columns)),
369+
)
370+
for index, breakpoint in enumerate(breakpoints):
371+
self.assertEqual(breakpoint["line"], loop_line)
372+
self.assertEqual(breakpoint["column"], columns[index])
373+
self.assertTrue(breakpoint["verified"], "expect breakpoint verified")
374+
breakpoint_ids.append(breakpoint["id"])
375+
376+
# Continue to the first breakpoint,
377+
self.continue_to_breakpoints([breakpoint_ids[0]])
378+
379+
# We should have stopped right before the call to `twelve`.
380+
# Step into and check we are inside `twelve`.
381+
self.stepIn()
382+
func_name = self.get_stackFrames()[0]["name"]
383+
self.assertEqual(func_name, "twelve(int)")
384+
385+
# Continue to the second breakpoint.
386+
self.continue_to_breakpoints([breakpoint_ids[1]])
387+
388+
# We should have stopped right before the call to `fourteen`.
389+
# Step into and check we are inside `fourteen`.
390+
self.stepIn()
391+
func_name = self.get_stackFrames()[0]["name"]
392+
self.assertEqual(func_name, "a::fourteen(int)")

lldb/tools/lldb-dap/DAP.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050

5151
namespace lldb_dap {
5252

53-
typedef llvm::DenseMap<uint32_t, SourceBreakpoint> SourceBreakpointMap;
53+
typedef llvm::DenseMap<std::pair<uint32_t, uint32_t>, SourceBreakpoint>
54+
SourceBreakpointMap;
5455
typedef llvm::StringMap<FunctionBreakpoint> FunctionBreakpointMap;
5556
typedef llvm::DenseMap<lldb::addr_t, InstructionBreakpoint>
5657
InstructionBreakpointMap;

0 commit comments

Comments
 (0)