Author: Ebuka Ezike Date: 2026-07-23T13:52:25+01:00 New Revision: bd3546eaedaa0bc89f6706c619074d59ba00bafe
URL: https://github.com/llvm/llvm-project/commit/bd3546eaedaa0bc89f6706c619074d59ba00bafe DIFF: https://github.com/llvm/llvm-project/commit/bd3546eaedaa0bc89f6706c619074d59ba00bafe.diff LOG: [lldb-dap] Migrate DAP attach tests. (#210814) Address some issues with the previous tests. - Always wait for the continued event after sending a continue request. since the continue response is just an acknowlegement that we send a continue packet. - Retry reading stdin if it has an error when the debugger attaches. - Update and enable the attachByPortNum test, this may now run on NetBSD and Windows. will try to enable in a different PR. Added: Modified: lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session_helpers.py lldb/packages/Python/lldbsuite/test/tools/lldb_dap/testcase.py lldb/packages/Python/lldbsuite/test/tools/lldb_dap/types.py lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py lldb/test/API/tools/lldb-dap/attach-commands/main.c lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py lldb/test/API/tools/lldb-dap/attach/TestDAP_attachByPortNum.py lldb/test/API/tools/lldb-dap/attach/main.c Removed: ################################################################################ diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session_helpers.py b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session_helpers.py index 613bf70d53479..32937ebdc4861 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session_helpers.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session_helpers.py @@ -27,6 +27,7 @@ CompletionsArgs, ConfigurationDoneArgs, ContinueArgs, + ContinuedEvent, DataBreakpoint, DataBreakpointInfoArgs, DisassembleArgs, @@ -1103,8 +1104,17 @@ def wait_for_memory_event(self, *, after: Event | Response): return self.wait_for_event(MemoryEvent, after=after) def do_continue(self): + """Send a 'continueRequest' and wait for a 'ContinuedEvent', + + Receiving the continue response does not mean the process continued, + It means we successfully sent the continue packet. + LLDB can continue asynchronously, so wait for the 'Continued' event. + """ self.ensure_initialized() - return self.send_request(ContinueArgs()).result() + prior_event = self.last_event() + response = self.send_request(ContinueArgs()).result() + self.wait_for_event(ContinuedEvent, after=prior_event) + return response def continue_to_exit(self, exitCode: int = 0) -> ExitedEvent: continue_response = self.do_continue() diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/testcase.py index 18085e5156aef..9e43a100cbd56 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/testcase.py @@ -1,12 +1,14 @@ import logging import os +import uuid from pathlib import Path from typing import Any, Final, Optional, TypeVar, Union, cast from lldbsuite.test.lldbtest import Base, LLDBTestCaseFactory, is_exe +import lldbgdbserverutils -from .types import AnyResponse, ErrorResponse, Response from .session_helpers import DAPTestSession +from .types import AnyResponse, ErrorResponse, Response from .utils import DebugAdapter, DebugAdapterOptions @@ -116,6 +118,11 @@ def cleanup_session(): self.addTearDownHook(cleanup_session) return session + def build_for_attach(self) -> str: + unique_name = str(uuid.uuid4()) + self.build(dictionary={"EXE": unique_name}) + return self.getBuildArtifact(unique_name) + def build_and_create_session( self, adapter: Optional[DebugAdapter] = None, @@ -152,6 +159,7 @@ def create_debug_adapter( def cleanup_adapter(): if adapter.is_alive: + self.logger.info("Manually killing debug adapter.") adapter.kill() # The debug adapter may have a reason the test failed. if stderr := adapter.process.stderr: @@ -194,6 +202,24 @@ def create_server_debug_adapter( self.assertTrue(adapter.is_server, "adapter should run as a server.") return adapter + def get_debug_server_path(self) -> Optional[Path]: + # Tries to find simulation/lldb-server/gdbserver tool path. + platform = self.getPlatform() + if platform == "windows": + # TODO: Update this for windows as it now supports lldb-server. + return None + + if self.platformIsDarwin(): + if platform != "macosx": + return None + server_exe = lldbgdbserverutils.get_debugserver_exe() + else: + server_exe = lldbgdbserverutils.get_lldb_server_exe() + + server_path = Path(server_exe) + self.assertTrue(server_path.exists(), f"{server_path.stem!r} not found") + return server_path + def expect_not_none(self, value: Optional[T], msg: Any = None) -> T: """Convenience function to narrow fields that are optional, as most DAP types are.""" self.assertIsNotNone(value, msg=msg) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/types.py b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/types.py index cfd14f480e2a6..19bd0d9dc26e5 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/types.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/types.py @@ -1150,15 +1150,24 @@ class LaunchArgs: @dataclass(frozen=True) @args_protocol class AttachArgs: - restart: Optional[Any] = field(metadata={"alias": "__restart"}, default=None) + @dataclass(frozen=True) + class Session: + targetId: int + debuggerId: Optional[int] = None + program: Optional[str] = None attachCommands: Optional[List[str]] = None pid: Optional[int] = None waitFor: Optional[bool] = None - gdbRemotePort: Optional[int] = None - gdbRemoteHostname: Optional[str] = None + gdbRemotePort: Optional[int] = field( + metadata={"alias": "gdb-remote-port"}, default=None + ) + gdbRemoteHostname: Optional[str] = field( + metadata={"alias": "gdb-remote-hostname"}, default=None + ) coreFile: Optional[str] = None - program: Optional[str] = None + session: Optional[Session] = None + restart: Optional[Any] = field(metadata={"alias": "__restart"}, default=None) # Configurations. debuggerRoot: Optional[str] = None diff --git a/lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py b/lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py index f24a32edbaa7b..0cfbb30f02c31 100644 --- a/lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py +++ b/lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py @@ -2,16 +2,12 @@ Test lldb-dap attach commands """ -from lldbsuite.test.decorators import * -from lldbsuite.test.lldbtest import * -from lldbsuite.test import lldbutil -import lldbdap_testcase -import time +from lldbsuite.test.decorators import skipIfNetBSD +from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase +from lldbsuite.test.tools.lldb_dap.types import AttachArgs, PauseArgs -class TestDAP_attachCommands(lldbdap_testcase.DAPTestCaseBase): - SHARED_BUILD_TESTCASE = False - +class TestDAP_attachCommands(DAPTestCaseBase): @skipIfNetBSD # Hangs on NetBSD as well def test_commands(self): """ @@ -34,89 +30,97 @@ def test_commands(self): "terminateCommands" are a list of LLDB commands that get executed when the debugger session terminates. """ - program = self.build_and_create_debug_adapter_for_attach() + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session() # Here we just create a target and launch the process as a way to test # if we are able to use attach commands to create any kind of a target - # and use it for debugging + # and use it for debugging. attachCommands = [ - 'target create -d "%s"' % (program), - "process launch --stop-at-entry", + f'target create -d "{program}"', + "process launch --stop-at-user-entry", ] initCommands = ["target list", "platform list"] preRunCommands = ["image list a.out", "image dump sections a.out"] postRunCommands = ["help trace", "help process trace"] stopCommands = ["frame variable", "thread backtrace"] - exitCommands = ["expr 2+3", "expr 3+4"] - terminateCommands = ["expr 4+2"] - self.attach( - program=program, - attachCommands=attachCommands, - initCommands=initCommands, - preRunCommands=preRunCommands, - stopCommands=stopCommands, - exitCommands=exitCommands, - terminateCommands=terminateCommands, - postRunCommands=postRunCommands, + exitCommands = ["history -c 2"] + terminateCommands = ["platform status"] + + process_event = session.attach( + AttachArgs( + program=program, + attachCommands=attachCommands, + stopOnEntry=True, + initCommands=initCommands, + preRunCommands=preRunCommands, + stopCommands=stopCommands, + exitCommands=exitCommands, + terminateCommands=terminateCommands, + postRunCommands=postRunCommands, + ) ) - self.dap_server.wait_for_initialized() + # Get output from the console. This should contain both the # "initCommands" and the "preRunCommands". - output = self.get_console() + output = session.get_console() # Verify all "initCommands" were found in console output - self.verify_commands("initCommands", output, initCommands) + session.verify_commands("initCommands", output, initCommands) # Verify all "preRunCommands" were found in console output - self.verify_commands("preRunCommands", output, preRunCommands) + session.verify_commands("preRunCommands", output, preRunCommands) # Verify all "postRunCommands" were found in console output - self.verify_commands("postRunCommands", output, postRunCommands) + session.verify_commands("postRunCommands", output, postRunCommands) - functions = ["main"] - breakpoint_ids = self.set_function_breakpoints(functions) - self.assertEqual(len(breakpoint_ids), len(functions), "expect one breakpoint") - self.continue_to_breakpoints(breakpoint_ids) - output = self.collect_console(pattern=stopCommands[-1]) - self.verify_commands("stopCommands", output, stopCommands) + stopped_event = session.verify_stopped_on_entry(after=process_event) + stopped_thread_id = self.expect_not_none(stopped_event.body.threadId) - # Check that we got module events from target - modules = self.dap_server.wait_for_module_events() - self.assertGreater(len(modules), 0) + output = session.collect_console(after=stopped_event, until=stopCommands[-1]) + session.verify_commands("stopCommands", output.seen_texts, stopCommands) # Continue after launch and hit the "pause()" call and stop the target. # Get output from the console. This should contain both the # "stopCommands" that were run after we stop. - self.do_continue() - time.sleep(0.5) - self.dap_server.request_pause() - self.dap_server.wait_for_stopped() - output = self.collect_console(pattern=stopCommands[-1]) - self.verify_commands("stopCommands", output, stopCommands) - - # Continue until the program exits - self.continue_to_exit() + session.do_continue() + + before_pause = session.last_event() + session.send_request(PauseArgs(stopped_thread_id)).result() + session.wait_for_stopped_event(after=before_pause) + + output = session.collect_console(after=before_pause, until=stopCommands[-1]) + session.verify_commands("stopCommands", output.seen_texts, stopCommands) + + # Set is_ready and continue until the program exits. + session.evaluate("`expression is_ready = 1", context="repl") + session.continue_to_exit() + # Get output from the console. This should contain both the # "exitCommands" that were run after the second breakpoint was hit # and the "terminateCommands" due to the debugging session ending - output = self.collect_console( - pattern=terminateCommands[0], - ) - self.verify_commands("exitCommands", output, exitCommands) - self.verify_commands("terminateCommands", output, terminateCommands) + output = session.collect_console(after=output.event, until=terminateCommands[0]) + output_texts = output.seen_texts + session.verify_commands("exitCommands", output_texts, exitCommands) + session.verify_commands("terminateCommands", output_texts, terminateCommands) def test_attach_command_process_failures(self): """ Tests that a 'attachCommands' is expected to leave the debugger's selected target with a valid process. """ - program = self.build_and_create_debug_adapter_for_attach() - attachCommands = ['script print("oops, forgot to attach to a process...")'] - resp = self.attach_and_configurationDone( + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session() + + attach_args = AttachArgs( program=program, - attachCommands=attachCommands, + attachCommands=['script print("oops, forgot to attach to a process...")'], ) - self.assertFalse(resp["success"]) + pending_attach = session.send_request(attach_args) + session.verify_configuration_done(expected_success=False) + + attach_response = pending_attach.error() + response_body = self.expect_not_none(attach_response.body) + response_error = self.expect_not_none(response_body.error) self.assertIn( - "attachCommands failed to attach to a process", - resp["body"]["error"]["format"], + "attachCommands failed to attach to a process", response_error.format ) @skipIfNetBSD # Hangs on NetBSD as well @@ -125,27 +129,29 @@ def test_terminate_commands(self): Tests that the "terminateCommands", that can be passed during attach, are run when the debugger is disconnected. """ - program = self.build_and_create_debug_adapter_for_attach() + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session(disconnect_automatically=False) # Here we just create a target and launch the process as a way to test # if we are able to use attach commands to create any kind of a target # and use it for debugging attachCommands = [ - 'target create -d "%s"' % (program), - "process launch --stop-at-entry", + f"target create -d '{program}'", + "process launch --stop-at-user-entry", ] - terminateCommands = ["expr 4+2"] - self.attach( - program=program, - attachCommands=attachCommands, - terminateCommands=terminateCommands, - disconnectAutomatically=False, + terminateCommands = ["history -c 1"] + process_event = session.attach( + AttachArgs( + program=program, + attachCommands=attachCommands, + terminateCommands=terminateCommands, + ) + ) + # Once it's disconnected the console should contain the "terminateCommands". + session.disconnect(terminateDebuggee=True) + output = session.collect_console( + after=process_event, until=terminateCommands[0] ) - self.get_console() - # Once it's disconnected the console should contain the - # "terminateCommands" - self.dap_server.request_disconnect(terminateDebuggee=True) - output = self.collect_console( - pattern=terminateCommands[0], + session.verify_commands( + "terminateCommands", output.seen_texts, terminateCommands ) - self.verify_commands("terminateCommands", output, terminateCommands) diff --git a/lldb/test/API/tools/lldb-dap/attach-commands/main.c b/lldb/test/API/tools/lldb-dap/attach-commands/main.c index f56d5d53afa05..26bb99c93cc84 100644 --- a/lldb/test/API/tools/lldb-dap/attach-commands/main.c +++ b/lldb/test/API/tools/lldb-dap/attach-commands/main.c @@ -1,30 +1,21 @@ #include "attach.h" -#include <stdio.h> + #ifdef _WIN32 -#include <process.h> #include <windows.h> +#define sleep_ms(ms) Sleep(ms) #else #include <unistd.h> +#define sleep_ms(ms) usleep((ms) * 1000) #endif +static volatile int is_ready = 0; + int main(int argc, char const *argv[]) { lldb_enable_attach(); - if (argc >= 2) { - // Create the synchronization token. - FILE *f = fopen(argv[1], "wx"); - if (!f) - return 1; - fputs("\n", f); - fflush(f); - fclose(f); + while (!is_ready) { + sleep_ms(50); } - printf("pid = %i\n", getpid()); -#ifdef _WIN32 - Sleep(10 * 1000); -#else - sleep(10); -#endif - return 0; // breakpoint 1 + return 0; } diff --git a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py index 8e8fea5f65b4b..96c5b193df39f 100644 --- a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py +++ b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py @@ -2,77 +2,79 @@ Test lldb-dap attach request """ -from lldbsuite.test.decorators import * -from lldbsuite.test.lldbtest import * -from lldbsuite.test import lldbutil -import lldbdap_testcase -import os import subprocess -import threading -import time +import uuid +from pathlib import Path + +from lldbsuite.test import lldbutil +from lldbsuite.test.decorators import ( + expectedFailureWindows, + expectedFailureWindowsAndNoLLDBServer, + skipIf, + skipIfWasm, + skipIfWindowsAndLLDBServer, +) +from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase +from lldbsuite.test.tools.lldb_dap.types import ( + AttachArgs, + ProcessEvent, + ProgressStartEvent, +) # Often fails on Arm Linux, but not specifically because it's Arm, something in # process scheduling can cause a massive (minutes) delay during this test. @skipIf(oslist=["linux"], archs=["arm$"]) -@skipIfWasm # no attach support -class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): +@skipIfWasm # No attach support +class TestDAP_attach(DAPTestCaseBase): SHARED_BUILD_TESTCASE = False - def spawn(self, program, args=None): - return self.spawnSubprocess( + def spawn(self, program: str, *, wait_for_sync: bool = True): + """Spawn the target and (by default) block until it has called `lldb_enable_attach`.""" + sync_token = lldbutil.append_to_process_working_directory( + self, f"sync_{uuid.uuid4().hex}" + ) + proc = self.spawnSubprocess( executable=program, - args=args, + args=[sync_token], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) + if wait_for_sync: + lldbutil.wait_for_file_on_target(self, sync_token) - def spawn_and_wait(self, program, delay): - time.sleep(delay) - proc = self.spawn(program=program) - start_time = time.time() - # Wait for either the process to exit or the event to be set. - while proc.poll() is None and not self.spawn_event.is_set(): - elapsed = time.time() - start_time - if elapsed >= self.DEFAULT_TIMEOUT: - break - time.sleep(0.1) - proc.kill() - proc.wait() - - def continue_and_verify_pid(self): - self.do_continue() - proc = self.lastSubprocess - if proc is None: - self.fail(f"lastSubprocess is None") - out, _ = proc.communicate("foo") + self.subprocesses.append(proc) + return proc + + def verify_pid(self, proc): + out, _ = proc.communicate("f") + + self.assertIn(f"char = f", out) self.assertIn(f"pid = {proc.pid}", out) def test_by_pid(self): - """ - Tests attaching to a process by process ID. - """ - program = self.build_and_create_debug_adapter_for_attach() + """Tests attaching to a process by process ID.""" + program = self.build_for_attach() + session = self.create_session() + proc = self.spawn(program=program) - self.attach(pid=proc.pid) - self.continue_and_verify_pid() + self.assertIsNone(proc.poll(), "process should be running") + + process_event = session.attach(AttachArgs(pid=proc.pid)) + self.assertEqual(process_event.body.systemProcessId, proc.pid) + self.verify_pid(proc) def test_by_name(self): - """ - Tests attaching to a process by process name. - """ - program = self.build_and_create_debug_adapter_for_attach() + """Tests attaching to a process by process name.""" + program = self.build_for_attach() + session = self.create_session() - # Use a file as a synchronization point between test and inferior. - pid_file_path = lldbutil.append_to_process_working_directory( - self, "pid_file_%d" % (int(time.time())) - ) - self.spawn(program=program, args=[pid_file_path]) - lldbutil.wait_for_file_on_target(self, pid_file_path) + proc = self.spawn(program=program) - self.attach(program=program) - self.continue_and_verify_pid() + process_event = session.attach(AttachArgs(program=program)) + self.assertEqual(process_event.body.systemProcessId, proc.pid) + self.verify_pid(proc) @expectedFailureWindowsAndNoLLDBServer def test_by_name_waitFor(self): @@ -80,64 +82,63 @@ def test_by_name_waitFor(self): Tests waiting for, and attaching to a process by process name that doesn't exist yet. """ - program = self.build_and_create_debug_adapter_for_attach() - self.spawn_event = threading.Event() - self.spawn_thread = threading.Thread( - target=self.spawn_and_wait, - args=( - program, - 1.0, - ), - ) - self.spawn_thread.start() - try: - self.attach(program=program, waitFor=True) - self.continue_and_verify_pid() - finally: - self.spawn_event.set() - if self.spawn_thread.is_alive(): - self.spawn_thread.join(timeout=10) + self.do_attach_waitFor(use_basename=False) @expectedFailureWindows @skipIfWindowsAndLLDBServer - def test_by_partial_name_waitFor(self): + def test_by_basename_waitFor(self): """ - Tests waiting for and attaching to a process by partial process name + Tests waiting for and attaching to a process by the process base name that doesn't exist yet. """ - program = self.build_and_create_debug_adapter_for_attach() - self.spawn_event = threading.Event() - self.spawn_thread = threading.Thread( - target=self.spawn_and_wait, - args=( - program, - 1.0, - ), + self.do_attach_waitFor(use_basename=True) + + def do_attach_waitFor(self, use_basename: bool): + """Kick off attach with waitFor=True; spawn the target once lldb-dap + signals it has entered the wait-for-process polling loop.""" + session = self.create_session() + program = self.build_for_attach() + attach_name = Path(program).name if use_basename else program + + init_response = session.initialize_sequence(session.initialize_args) + pending = session.send_request(AttachArgs(program=attach_name, waitFor=True)) + + # Wait until lldb-dap is actually polling for the process before we + # spawn it, so we don't race the poll setup. + session.wait_for_event( + ProgressStartEvent, + until=lambda e: "Waiting to attach" in e.body.title, + after=init_response, + timeout_msg="Waiting for attach progress event.", ) - self.spawn_thread.start() - try: - self.attach(program=os.path.basename(program), waitFor=True) - self.continue_and_verify_pid() - finally: - self.spawn_event.set() - if self.spawn_thread.is_alive(): - self.spawn_thread.join(timeout=10) + + proc = self.spawn(program=program, wait_for_sync=False) + + session.ensure_initialized() + session.verify_configuration_done() + + process_event = session.wait_for_event(ProcessEvent, after=init_response) + pending.result("expects attach response.") + + self.assertEqual(process_event.body.systemProcessId, proc.pid) + self.verify_pid(proc) def test_attach_with_missing_session_debugger(self): """ Test that attaching with only one of debuggerId/targetId specified fails with the expected error message. """ - self.build_and_create_debug_adapter() + session = self.create_session() # Test with only targetId specified (no debuggerId) - session = {"targetId": 99999} - attach_seq = self.attach(session=session) - resp = self.dap_server.receive_response(attach_seq) - self.assertFalse(resp["success"]) + resp = session.initialize_and_launch( + AttachArgs(session=AttachArgs.Session(targetId=99999)) + ).error() + + message = self.expect_not_none(resp.body and resp.body.error) self.assertIn( "missing value at arguments.session.debuggerId", - resp["body"]["error"]["format"], + message.format, ) def test_attach_with_invalid_session(self): @@ -145,15 +146,19 @@ def test_attach_with_invalid_session(self): Test that attaching with both debuggerId and targetId specified but invalid fails with an appropriate error message. """ - self.build_and_create_debug_adapter() + session = self.create_session() - # Attach with both debuggerId=9999 and targetId=99999 (both invalid). + # Attach with both debuggerId=9999 and targetId=9999 (both invalid). # Since debugger ID 9999 likely doesn't exist in the global registry, # we expect a validation error. - session = {"debuggerId": 9999, "targetId": 9999} - resp = self.attach_and_configurationDone(session=session) - self.assertFalse(resp["success"]) - error_msg = resp["body"]["error"]["format"] + pending = session.initialize_and_launch( + AttachArgs(session=AttachArgs.Session(targetId=9999, debuggerId=9999)) + ) + session.configuration_done().result_or_error() + + resp = pending.error() + message = self.expect_not_none(resp.body and resp.body.error) + error_msg = message.format # Either error is acceptable - both indicate the debugger reuse # validation is working correctly self.assertTrue( diff --git a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attachByPortNum.py b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attachByPortNum.py index e8f3f0fbc3fd3..fe4f240c404e2 100644 --- a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attachByPortNum.py +++ b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attachByPortNum.py @@ -2,151 +2,118 @@ Test lldb-dap "port" configuration to "attach" request """ -from lldbsuite.test.decorators import * -from lldbsuite.test.lldbtest import * -from lldbsuite.test import lldbplatformutil +from typing import List + from lldbgdbserverutils import Pipe -import lldbdap_testcase +from lldbsuite.test import lldbplatformutil +from lldbsuite.test.decorators import skipIfNetBSD, skipIfWindows +from lldbsuite.test.lldbtest import line_number +from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase +from lldbsuite.test.tools.lldb_dap.types import AttachArgs + import lldb -@skip(bugnumber="https://github.com/llvm/llvm-project/issues/138803") -class TestDAP_attachByPortNum(lldbdap_testcase.DAPTestCaseBase): - def set_and_hit_breakpoint(self, continueToExit=True): - self.dap_server.wait_for_stopped() +def debug_server_start_args() -> List[str]: + args: List[str] = [] + if not lldbplatformutil.platformIsDarwin(): + args = ["gdbserver"] - source = "main.c" - breakpoint1_line = line_number(source, "// breakpoint 1") - lines = [breakpoint1_line] - # Set breakpoint in the thread function so we can step the threads - breakpoint_ids = self.set_source_breakpoints(source, lines) - self.assertEqual( - len(breakpoint_ids), len(lines), "expect correct number of breakpoints" - ) - self.continue_to_breakpoints(breakpoint_ids) - if continueToExit: - self.continue_to_exit() - - def get_debug_server_command_line_args(self): - args = [] - if lldbplatformutil.getPlatform() == "linux": - args = ["gdbserver"] - if lldb.remote_platform: - args += ["*:0"] - else: - args += ["localhost:0"] - return args - - def get_debug_server_pipe(self): + if lldb.remote_platform: + args += ["*:0"] + else: + args += ["localhost:0"] + return args + + +class TestDAP_attachByPortNum(DAPTestCaseBase): + SHARED_BUILD_TESTCASE = False + + def create_debug_server_pipe(self): pipe = Pipe(self.getBuildDir()) - self.addTearDownHook(lambda: pipe.close()) + self.addTearDownHook(pipe.close) pipe.finish_connection(self.DEFAULT_TIMEOUT) return pipe @skipIfWindows - @skipIfNetBSD + @skipIfNetBSD # Try enable, get_debug_server_path previously returned None. def test_by_port(self): - """ - Tests attaching to a process by port. - """ - program = self.build_and_create_debug_adapter_for_attach() - - debug_server_tool = self.getBuiltinDebugServerTool() - - pipe = self.get_debug_server_pipe() - args = self.get_debug_server_command_line_args() - args += [program] - args += ["--named-pipe", pipe.name] - - self.process = self.spawnSubprocess( - debug_server_tool, args, install_remote=False + """Tests attaching to a process by port.""" + program_path = self.build_for_attach() + session = self.create_session() + + pipe = self.create_debug_server_pipe() + debug_server_args = debug_server_start_args() + debug_server_args.extend(["--named-pipe", pipe.name, "--", program_path]) + + self.spawnSubprocess( + str(self.get_debug_server_path()), + debug_server_args, + install_remote=False, ) # Read the port number from the debug server pipe. - port = pipe.read(10, self.DEFAULT_TIMEOUT) - # Trim null byte, convert to int - port = int(port[:-1]) - self.assertIsNotNone( - port, " Failed to read the port number from debug server pipe" - ) + pipe_data = pipe.read(10, self.DEFAULT_TIMEOUT) + port = int(pipe_data.rstrip(b"\0")) - self.attach( - program=program, - gdbRemotePort=port, - sourceInitFile=True, - stopOnEntry=True, - ) - self.set_and_hit_breakpoint(continueToExit=True) + args = AttachArgs(program=program_path, gdbRemotePort=port, stopOnEntry=True) + with session.configure(args) as ctx: + bp_line = line_number("main.c", "// breakpoint 1") + [bp1] = session.resolve_source_breakpoints("main.c", [bp_line]) + + session.verify_stopped_on_entry(after=ctx.process_event) + session.continue_to_breakpoint(bp1) + session.continue_to_exit() @skipIfWindows @skipIfNetBSD def test_fails_if_both_port_and_pid_are_set(self): - """ - Tests attaching to a process by process ID and port number. - """ - program = self.build_and_create_debug_adapter_for_attach() - + """Tests attaching to a process by process ID and port number.""" # It is not necessary to launch "lldb-server" to obtain the actual port # and pid for attaching. However, when providing the port number and pid # directly, "lldb-dap" throws an error message, which is expected. So, # used random pid and port numbers here. + program = self.build_for_attach() + session = self.create_session() - pid = 1354 - port = 1234 - - response = self.attach( - program=program, - pid=pid, - gdbRemotePort=port, - sourceInitFile=True, - waitForResponse=True, - ) - self.assertFalse( - response["success"], "The user can't specify both pid and port" + pending = session.send_request( + AttachArgs(program, pid=1354, gdbRemotePort=1234) ) + pending.error("The user can't specify both pid and port") @skipIfWindows @skipIfNetBSD def test_by_invalid_port(self): - """ - Tests attaching to a process by invalid port number 0. - """ - program = self.build_and_create_debug_adapter_for_attach() - - port = 0 - response = self.attach( - program=program, - gdbRemotePort=port, - sourceInitFile=True, - waitForResponse=True, - ) - self.assertFalse( - response["success"], - "The user can't attach with invalid port (%s)" % port, - ) + """Tests attaching to a process by invalid port number 0.""" + program = self.build_for_attach() + session = self.create_session() + + port = -1 + attach_args = AttachArgs(program, gdbRemotePort=port) + pending = session.initialize_and_launch(attach_args) + session.configuration_done().result_or_error() + pending.error(f"The user can't attach to invalid port {port}") @skipIfWindows @skipIfNetBSD def test_by_illegal_port(self): - """ - Tests attaching to a process by illegal/greater port number 65536 - """ - program = self.build_and_create_debug_adapter_for_attach() + """Tests attaching to a process by illegal/greater port number 65536""" + program = self.build_for_attach() + session = self.create_session() port = 65536 - args = [program] - debug_server_tool = self.getBuiltinDebugServerTool() - self.process = self.spawnSubprocess( - debug_server_tool, args, install_remote=False - ) - - response = self.attach( - program=program, - gdbRemotePort=port, - sourceInitFile=True, - waitForResponse=True, - ) - self.assertFalse( - response["success"], - "The user can't attach with illegal port (%s)" % port, + debug_server = self.expect_not_none(self.get_debug_server_path()) + server_args = [f"localhost:{port}", "--", program] + if debug_server.stem == "lldb-server": + server_args = ["gdbserver", *server_args] + + self.spawnSubprocess(str(debug_server), server_args, install_remote=False) + + pending = session.initialize_and_launch( + AttachArgs( + program=program, + gdbRemotePort=port, + ) ) + session.configuration_done().result_or_error() + pending.error(f"The user can't attach with illegal port ({port})") diff --git a/lldb/test/API/tools/lldb-dap/attach/main.c b/lldb/test/API/tools/lldb-dap/attach/main.c index e14cf71a7044c..23e358de55747 100644 --- a/lldb/test/API/tools/lldb-dap/attach/main.c +++ b/lldb/test/API/tools/lldb-dap/attach/main.c @@ -17,11 +17,22 @@ int main(int argc, char const *argv[]) { fputs("\n", f); fflush(f); fclose(f); - } - // Wait on input from stdin. - getchar(); + // Wait on input from stdin. + // when lldb connects to the process, on MacOS getchar() is interupted + // and sets the stream's error indicator (EINTR). + // ignore that and keep waiting until we actually receive a character. + while (1) { + int c = getchar(); + if (c == EOF && ferror(stdin)) { + clearerr(stdin); + continue; + } + printf("char = %c\n", c); + break; + } + } printf("pid = %i\n", getpid()); - return 0; + return 0; // breakpoint 1 } _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
