[Lldb-commits] [lldb] [lldb-dap][test] Migrate server mode tests (PR #227804)
Ebuka Ezike via lldb-commits
lldb-commits at lists.llvm.org
Wed Sep 30 16:55:56 PDT 2026
https://github.com/da-viper updated https://github.com/llvm/llvm-project/pull/227804
>From ea274c48f7602636ca25c1524bb60749e5a7f655 Mon Sep 17 00:00:00 2001
From: Ebuka Ezike <e_ezike at apple.com>
Date: Wed, 30 Sep 2026 18:42:58 +0100
Subject: [PATCH] [lldb-dap] Migrate server tests
Migrate TestDAP_server.py to the new lldb-dap test infrastructure.
test_server_port and test_server_unix_socket now run two sessions
concurrently.
Add test_breakpoints_in_multiple_sessions to check that a breakpoint set
in one session must not stop another.
Fix test_connection_timeout_at_server_start. It now waits for lldb-dap to
exit on its own before the teardown kills it.
---
.../tools/lldb-dap/server/TestDAP_server.py | 237 +++++++++++-------
1 file changed, 142 insertions(+), 95 deletions(-)
diff --git a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py
index d5225fd627022..1a8e1ead10eec 100644
--- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py
+++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py
@@ -6,66 +6,18 @@
import signal
import tempfile
import time
+from concurrent import futures
+from typing import List
-import dap_server
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
-import lldbdap_testcase
-from subprocess import Popen
-from typing import Tuple
+from lldbsuite.test.tools.lldb_dap import *
+from lldbsuite.test.tools.lldb_dap.types import *
@requireSocketPermission # every test starts lldb-dap in listening server mode
-class TestDAP_server(lldbdap_testcase.DAPTestCaseBase):
- def start_server(
- self, connection, connection_timeout=30
- ) -> Tuple[Popen[bytes], str]:
- self.create_debug_adapter(
- connection=connection, connection_timeout=connection_timeout
- )
- assert self.dap_server.process
- assert self.dap_server.connection
-
- # Save the process instance for the cleanup in case a new server is
- # created.
- process: Popen[bytes] = self.dap_server.process
-
- def cleanup():
- try:
- process.stdin.close()
- process.wait(timeout=5)
- except subprocess.TimeoutExpired:
- process.kill()
-
- self.addTearDownHook(cleanup)
-
- return (process, self.dap_server.connection)
-
- def run_debug_session(
- self, connection: str, name: str, *, sleep_seconds_in_middle: float = 0
- ):
- self.dap_server = dap_server.DebugAdapterServer(
- connection=connection,
- init_commands=self.setUpCommands(),
- spawn_helper=self.spawnSubprocess,
- )
- program = self.getBuildArtifact("a.out")
- source = "main.c"
- breakpoint_line = line_number(source, "// breakpoint")
-
- self.launch(
- program,
- args=[name],
- disconnectAutomatically=False,
- )
- if sleep_seconds_in_middle:
- time.sleep(sleep_seconds_in_middle)
- self.set_source_breakpoints(source, [breakpoint_line])
- self.continue_to_next_stop()
- self.continue_to_exit()
- output = self.get_stdout()
- self.assertEqual(output, f"Hello {name}!\r\n")
- self.dap_server.request_disconnect()
+class TestDAP_server(DAPTestCaseBase):
+ USE_DEFAULT_DEBUG_ADAPTER = False
@skipIfWindows
def test_server_port(self):
@@ -73,27 +25,27 @@ def test_server_port(self):
Test launching a binary with a lldb-dap in server mode on a specific port.
"""
self.build()
- (_, connection) = self.start_server(connection="listen://localhost:0")
- self.run_debug_session(connection, "Alice")
- self.run_debug_session(connection, "Bob")
+ adapter = self.start_server(connection="listen://localhost:0")
+ self.run_concurrent_debug_sessions(adapter, ["Alice", "Bob"])
@requirePOSIX
def test_server_unix_socket(self):
"""
Test launching a binary with a lldb-dap in server mode on a unix socket.
"""
- dir = tempfile.gettempdir()
- name = dir + "/dap-connection-" + str(os.getpid())
+ self.build()
+ # Unix socket paths are limited to about 104 bytes, and the default temp
+ # directory on macOS can be long, so use /tmp when there is one.
+ # The temp directory makes it unique "/tmp/tmp-dir9909ilskj32ewr/dap.sock"
+ base_dir = "/tmp" if os.path.isdir("/tmp") else None
+ temp_dir = tempfile.TemporaryDirectory(dir=base_dir)
+ socket_path = os.path.join(temp_dir.name, "dap.sock")
- def cleanup():
- os.unlink(name)
+ self.logger.info("adapter server socket path: %s", socket_path)
+ self.addTearDownHook(temp_dir.cleanup)
- self.addTearDownHook(cleanup)
-
- self.build()
- (_, connection) = self.start_server(connection="accept://" + name)
- self.run_debug_session(connection, "Alice")
- self.run_debug_session(connection, "Bob")
+ adapter = self.start_server(connection="accept://" + socket_path)
+ self.run_concurrent_debug_sessions(adapter, ["Alice", "Bob"])
@skipIfWindows
def test_server_interrupt(self):
@@ -102,29 +54,23 @@ def test_server_interrupt(self):
the server while the debug session is still active.
"""
self.build()
- (process, _) = self.start_server(connection="listen://localhost:0")
- program = self.getBuildArtifact("a.out")
- source = "main.c"
- breakpoint_line = line_number(source, "// breakpoint")
-
- self.launch(
- program,
- args=["Alice"],
- disconnectAutomatically=False,
- )
- self.set_source_breakpoints(source, [breakpoint_line])
- self.continue_to_next_stop()
+ adapter = self.start_server(connection="listen://localhost:0")
+ session = self.create_session(adapter, disconnect_automatically=False)
+ stop_event = self.launch_to_breakpoint(session, "Alice")
# Interrupt the server which should disconnect all clients.
- process.send_signal(signal.SIGINT)
+ adapter.process.send_signal(signal.SIGINT)
# Wait for both events since they can happen in any order.
- self.dap_server.wait_for_event(["terminated", "exited"])
- self.dap_server.wait_for_event(["terminated", "exited"])
- self.assertIsNotNone(
- self.dap_server.exit_status,
- "Process exited before interrupting lldb-dap server",
- )
+ ending_events = (TerminatedEvent, ExitedEvent)
+ first = session.wait_for_any_event(ending_events, after=stop_event)
+ session.wait_for_any_event(ending_events, after=first)
+
+ exit_code = int(signal.SIGKILL)
+ session.verify_process_exited(exitCode=exit_code, after=stop_event)
+
+ # The server shuts down cleanly once its clients are disconnected.
+ self.assertEqual(adapter.process.wait(timeout=self.DEFAULT_TIMEOUT), 0)
@skipIfWindows
def test_connection_timeout_at_server_start(self):
@@ -133,10 +79,12 @@ def test_connection_timeout_at_server_start(self):
waiting for it to terminate automatically when no client connects.
"""
self.build()
- self.start_server(
+ adapter = self.start_server(
connection="listen://localhost:0",
connection_timeout=1,
)
+ # The server must exit on its own, before the teardown hook kills it.
+ self.assertEqual(adapter.process.wait(timeout=self.DEFAULT_TIMEOUT), 0)
@skipIfWindows
def test_connection_timeout_long_debug_session(self):
@@ -145,12 +93,14 @@ def test_connection_timeout_long_debug_session(self):
terminating the server after the a long debug session.
"""
self.build()
- (_, connection) = self.start_server(
+ adapter = self.start_server(
connection="listen://localhost:0",
connection_timeout=1,
)
- # The connection timeout should not cut off the debug session
- self.run_debug_session(connection, "Alice", sleep_seconds_in_middle=1.5)
+ # The connection timeout should not cut off the debug session.
+ session = self.create_session(adapter, disconnect_automatically=False)
+ self.run_debug_session(session, "Alice", sleep_seconds_in_middle=1.2)
+ self.assertTrue(adapter.is_alive, "expected the server to be running")
@skipIfWindows
def test_connection_timeout_multiple_sessions(self):
@@ -159,13 +109,110 @@ def test_connection_timeout_multiple_sessions(self):
terminating the server after the last debug session.
"""
self.build()
- (_, connection) = self.start_server(
+ adapter = self.start_server(
connection="listen://localhost:0",
connection_timeout=1,
)
- time.sleep(0.5)
+ time.sleep(0.25)
# Should be able to connect to the server.
- self.run_debug_session(connection, "Alice")
- time.sleep(0.5)
+ session1 = self.create_session(adapter, disconnect_automatically=False)
+ self.run_debug_session(session1, "Alice")
+ time.sleep(0.25)
# Should be able to connect to the server, because it's still within the connection timeout.
- self.run_debug_session(connection, "Bob")
+ session2 = self.create_session(adapter, disconnect_automatically=False)
+ self.run_debug_session(session2, "Bob")
+
+ # The server shuts down once the connection timeout passes after the
+ # last session ends.
+ self.assertEqual(adapter.process.wait(timeout=self.DEFAULT_TIMEOUT), 0)
+
+ @skipIfWindows
+ def test_breakpoints_in_multiple_sessions(self):
+ """
+ Test in server mode setting a breakpoint in one session does not activate
+ in another session.
+ """
+ self.build()
+ program = self.getBuildArtifact("a.out")
+ adapter = self.start_server(connection="listen://localhost:0")
+ # With first breakpoint.
+ session1 = self.create_session(adapter, disconnect_automatically=False)
+ # With second breakpoint.
+ session2 = self.create_session(adapter, disconnect_automatically=False)
+
+ source = "main.c"
+ bp1_line = line_number(source, "// breakpoint 1")
+ launch_args = LaunchArgs(program)
+
+ # Start the first session and stop at breakpoint 1.
+ with session1.configure(launch_args) as ctx1:
+ [breakpoint1] = session1.resolve_source_breakpoints(source, [bp1_line])
+
+ session1.verify_stopped_on_breakpoint(breakpoint1, after=ctx1.process_event)
+
+ # Start the second session and stop at breakpoint 2.
+ bp2_line = line_number(source, "// breakpoint 2")
+ with session2.configure(launch_args) as ctx2:
+ [breakpoint2] = session2.resolve_source_breakpoints(source, [bp2_line])
+
+ session2.verify_stopped_on_breakpoint(breakpoint2, after=ctx2.process_event)
+
+ # Start and finish the third session with no breakpoint.
+ session3 = self.create_session(adapter, disconnect_automatically=False)
+ process_event3 = session3.launch(launch_args)
+ session3.verify_process_exited(after=process_event3)
+
+ # Finishing session1 and session2 should not hit any breakpoint.
+ session1.continue_to_exit()
+ session2.continue_to_exit()
+
+ def start_server(self, connection: str, connection_timeout: int = 30):
+ adapter = self.create_server_debug_adapter(
+ connection=connection, connection_timeout=connection_timeout
+ )
+ return adapter
+
+ def launch_to_breakpoint(self, session: DAPTestSession, name: str):
+ program = self.getBuildArtifact("a.out")
+ source = "main.c"
+ breakpoint_line = line_number(source, "// breakpoint 1")
+
+ with session.configure(LaunchArgs(program, args=[name])) as ctx:
+ session.resolve_source_breakpoints(source, [breakpoint_line])
+
+ return session.verify_stopped_on_breakpoint(after=ctx.process_event)
+
+ def continue_to_exit_and_disconnect(self, session: DAPTestSession, name: str):
+ session.continue_to_exit()
+ output = session.get_stdout()
+ self.assertEqual(output, f"Hello {name}!\r\n")
+ disconnect_resp = session.disconnect()
+
+ with self.assertRaises(DAPError):
+ # We should not receive a second terminated event. The check doesn't
+ # stall, since the event history is closed.
+ session.wait_for_terminated_event(after=disconnect_resp)
+
+ def run_debug_session(
+ self, session: DAPTestSession, name: str, *, sleep_seconds_in_middle: float = 0
+ ):
+ self.launch_to_breakpoint(session, name)
+ if sleep_seconds_in_middle:
+ time.sleep(sleep_seconds_in_middle)
+ self.continue_to_exit_and_disconnect(session, name)
+
+ def run_concurrent_debug_sessions(self, adapter: DebugAdapter, names: List[str]):
+ # create_session() is not called in the executor since it is not thread-safe
+ # because it calls addTearDownHook() and create_connection().
+ sessions = [
+ self.create_session(adapter, disconnect_automatically=False) for _ in names
+ ]
+ with futures.ThreadPoolExecutor(max_workers=len(names)) as executor:
+ session_futures = {
+ executor.submit(self.run_debug_session, session, name): name
+ for session, name in zip(sessions, names)
+ }
+
+ for fut in futures.as_completed(session_futures):
+ with self.subTest(session=session_futures[fut]):
+ fut.result()
More information about the lldb-commits
mailing list