From ee6524a76ba47f97ebea6d7b4620a2cea94997d5 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Tue, 14 Oct 2025 13:56:05 -0700 Subject: [PATCH 1/4] [lldb-dap] Remove timings from TestDAP_attach This PR split TestDAP_attach into two tests and removes any reliance on timing from the simple attach tests. Instead, it uses stdin and stdout to synchronize between the test and the inferior, and to check that we're attached to the correct inferior. Fixes #163295 --- .../tools/lldb-dap/attach-commands/Makefile | 3 + .../attach-commands/TestDAP_attachCommands.py | 146 +++++++++++++ .../API/tools/lldb-dap/attach-commands/main.c | 30 +++ .../tools/lldb-dap/attach/TestDAP_attach.py | 202 +++--------------- lldb/test/API/tools/lldb-dap/attach/main.c | 11 +- 5 files changed, 211 insertions(+), 181 deletions(-) create mode 100644 lldb/test/API/tools/lldb-dap/attach-commands/Makefile create mode 100644 lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py create mode 100644 lldb/test/API/tools/lldb-dap/attach-commands/main.c diff --git a/lldb/test/API/tools/lldb-dap/attach-commands/Makefile b/lldb/test/API/tools/lldb-dap/attach-commands/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/attach-commands/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules 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 new file mode 100644 index 0000000000000..09f22ca5eff30 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py @@ -0,0 +1,146 @@ +""" +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 + +class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): + @skipIfNetBSD # Hangs on NetBSD as well + def test_commands(self): + """ + Tests the "initCommands", "preRunCommands", "stopCommands", + "exitCommands", "terminateCommands" and "attachCommands" + that can be passed during attach. + + "initCommands" are a list of LLDB commands that get executed + before the target is created. + "preRunCommands" are a list of LLDB commands that get executed + after the target has been created and before the launch. + "stopCommands" are a list of LLDB commands that get executed each + time the program stops. + "exitCommands" are a list of LLDB commands that get executed when + the process exits + "attachCommands" are a list of LLDB commands that get executed and + must have a valid process in the selected target in LLDB after + they are done executing. This allows custom commands to create any + kind of debug session. + "terminateCommands" are a list of LLDB commands that get executed when + the debugger session terminates. + """ + program = self.build_and_create_debug_adapter_for_attach() + + # 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", + ] + 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, + ) + # Get output from the console. This should contain both the + # "initCommands" and the "preRunCommands". + output = self.get_console() + # Verify all "initCommands" were found in console output + self.verify_commands("initCommands", output, initCommands) + # Verify all "preRunCommands" were found in console output + self.verify_commands("preRunCommands", output, preRunCommands) + # Verify all "postRunCommands" were found in console output + self.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(timeout=10, pattern=stopCommands[-1]) + self.verify_commands("stopCommands", output, 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(timeout=10, pattern=stopCommands[-1]) + self.verify_commands("stopCommands", output, stopCommands) + + # Continue until the program exits + self.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( + timeout=10.0, + pattern=terminateCommands[0], + ) + self.verify_commands("exitCommands", output, exitCommands) + self.verify_commands("terminateCommands", output, 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( + program=program, + attachCommands=attachCommands, + expectFailure=True, + ) + self.assertFalse(resp["success"]) + self.assertIn( + "attachCommands failed to attach to a process", + resp["body"]["error"]["format"], + ) + + @skipIfNetBSD # Hangs on NetBSD as well + 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() + + # 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", + ] + terminateCommands = ["expr 4+2"] + self.attach( + program=program, + attachCommands=attachCommands, + terminateCommands=terminateCommands, + disconnectAutomatically=False, + ) + self.get_console() + # Once it's disconnected the console should contain the + # "terminateCommands" + self.dap_server.request_disconnect(terminateDebuggee=True) + output = self.collect_console( + timeout=1.0, + pattern=terminateCommands[0], + ) + 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 new file mode 100644 index 0000000000000..f56d5d53afa05 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/attach-commands/main.c @@ -0,0 +1,30 @@ +#include "attach.h" +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +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); + } + + printf("pid = %i\n", getpid()); +#ifdef _WIN32 + Sleep(10 * 1000); +#else + sleep(10); +#endif + return 0; // breakpoint 1 +} 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 c54e21c1b973a..17f670b3c190d 100644 --- a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py +++ b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py @@ -11,35 +11,23 @@ import time -def spawn_and_wait(program, delay): - if delay: - time.sleep(delay) - process = subprocess.Popen( - [program], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - process.wait() - - class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): - def set_and_hit_breakpoint(self, continueToExit=True): - 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" - ) - # Test binary will sleep for 10s, offset the breakpoint timeout - # accordingly. - timeout_offset = 10 - self.continue_to_breakpoints( - breakpoint_ids, timeout=timeout_offset + self.DEFAULT_TIMEOUT + def spawn_and_wait(self, program, delay): + time.sleep(delay) + self.process = subprocess.Popen( + [program], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, ) - if continueToExit: - self.continue_to_exit() + self.process.wait() + + def continue_and_verify_pid(self): + self.do_continue() + out, _ = self.process.communicate("foo") + self.assertIn(f"pid = {self.process.pid}", out) - @skipIfNetBSD # Hangs on NetBSD as well def test_by_pid(self): """ Tests attaching to a process by process ID. @@ -50,11 +38,11 @@ def test_by_pid(self): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True, ) self.attach(pid=self.process.pid) - self.set_and_hit_breakpoint(continueToExit=True) + self.continue_and_verify_pid() - @skipIfNetBSD # Hangs on NetBSD as well def test_by_name(self): """ Tests attaching to a process by process name. @@ -65,24 +53,26 @@ def test_by_name(self): pid_file_path = lldbutil.append_to_process_working_directory( self, "pid_file_%d" % (int(time.time())) ) - - popen = self.spawnSubprocess(program, [pid_file_path]) + self.process = subprocess.Popen( + [program, pid_file_path], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) lldbutil.wait_for_file_on_target(self, pid_file_path) self.attach(program=program) - self.set_and_hit_breakpoint(continueToExit=True) + self.continue_and_verify_pid() - @skipUnlessDarwin - @skipIfNetBSD # Hangs on NetBSD as well def test_by_name_waitFor(self): """ - Tests attaching to a process by process name and waiting for the - next instance of a process to be launched, ignoring all current - ones. + 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_thread = threading.Thread( - target=spawn_and_wait, + target=self.spawn_and_wait, args=( program, 1.0, @@ -90,140 +80,4 @@ def test_by_name_waitFor(self): ) self.spawn_thread.start() self.attach(program=program, waitFor=True) - self.set_and_hit_breakpoint(continueToExit=True) - - @skipIfNetBSD # Hangs on NetBSD as well - def test_commands(self): - """ - Tests the "initCommands", "preRunCommands", "stopCommands", - "exitCommands", "terminateCommands" and "attachCommands" - that can be passed during attach. - - "initCommands" are a list of LLDB commands that get executed - before the target is created. - "preRunCommands" are a list of LLDB commands that get executed - after the target has been created and before the launch. - "stopCommands" are a list of LLDB commands that get executed each - time the program stops. - "exitCommands" are a list of LLDB commands that get executed when - the process exits - "attachCommands" are a list of LLDB commands that get executed and - must have a valid process in the selected target in LLDB after - they are done executing. This allows custom commands to create any - kind of debug session. - "terminateCommands" are a list of LLDB commands that get executed when - the debugger session terminates. - """ - program = self.build_and_create_debug_adapter_for_attach() - - # 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", - ] - 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, - ) - # Get output from the console. This should contain both the - # "initCommands" and the "preRunCommands". - output = self.get_console() - # Verify all "initCommands" were found in console output - self.verify_commands("initCommands", output, initCommands) - # Verify all "preRunCommands" were found in console output - self.verify_commands("preRunCommands", output, preRunCommands) - # Verify all "postRunCommands" were found in console output - self.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(timeout=10, pattern=stopCommands[-1]) - self.verify_commands("stopCommands", output, 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(timeout=10, pattern=stopCommands[-1]) - self.verify_commands("stopCommands", output, stopCommands) - - # Continue until the program exits - self.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( - timeout=10.0, - pattern=terminateCommands[0], - ) - self.verify_commands("exitCommands", output, exitCommands) - self.verify_commands("terminateCommands", output, 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( - program=program, - attachCommands=attachCommands, - expectFailure=True, - ) - self.assertFalse(resp["success"]) - self.assertIn( - "attachCommands failed to attach to a process", - resp["body"]["error"]["format"], - ) - - @skipIfNetBSD # Hangs on NetBSD as well - 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() - - # 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", - ] - terminateCommands = ["expr 4+2"] - self.attach( - program=program, - attachCommands=attachCommands, - terminateCommands=terminateCommands, - disconnectAutomatically=False, - ) - self.get_console() - # Once it's disconnected the console should contain the - # "terminateCommands" - self.dap_server.request_disconnect(terminateDebuggee=True) - output = self.collect_console( - timeout=1.0, - pattern=terminateCommands[0], - ) - self.verify_commands("terminateCommands", output, terminateCommands) + self.continue_and_verify_pid() diff --git a/lldb/test/API/tools/lldb-dap/attach/main.c b/lldb/test/API/tools/lldb-dap/attach/main.c index f56d5d53afa05..e14cf71a7044c 100644 --- a/lldb/test/API/tools/lldb-dap/attach/main.c +++ b/lldb/test/API/tools/lldb-dap/attach/main.c @@ -2,7 +2,6 @@ #include #ifdef _WIN32 #include -#include #else #include #endif @@ -20,11 +19,9 @@ int main(int argc, char const *argv[]) { fclose(f); } + // Wait on input from stdin. + getchar(); + printf("pid = %i\n", getpid()); -#ifdef _WIN32 - Sleep(10 * 1000); -#else - sleep(10); -#endif - return 0; // breakpoint 1 + return 0; } From 3aed18b11ab9f774f23e78829b2b5e4cc87fa2c8 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Tue, 14 Oct 2025 14:01:31 -0700 Subject: [PATCH 2/4] Update test name --- .../tools/lldb-dap/attach-commands/TestDAP_attachCommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 09f22ca5eff30..872fe47ac3f3b 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 @@ -8,7 +8,7 @@ import lldbdap_testcase import time -class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): +class TestDAP_attachCommands(lldbdap_testcase.DAPTestCaseBase): @skipIfNetBSD # Hangs on NetBSD as well def test_commands(self): """ From bbcb58a92bd3c265a87d0393c65c62f69d942190 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Tue, 14 Oct 2025 14:02:54 -0700 Subject: [PATCH 3/4] Formatting --- .../API/tools/lldb-dap/attach-commands/TestDAP_attachCommands.py | 1 + 1 file changed, 1 insertion(+) 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 872fe47ac3f3b..ed373f2c427a9 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 @@ -8,6 +8,7 @@ import lldbdap_testcase import time + class TestDAP_attachCommands(lldbdap_testcase.DAPTestCaseBase): @skipIfNetBSD # Hangs on NetBSD as well def test_commands(self): From b99f29ab324c103e0cc212c176bb7a068330f1b4 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Tue, 14 Oct 2025 14:28:21 -0700 Subject: [PATCH 4/4] Extract spawn helper --- .../tools/lldb-dap/attach/TestDAP_attach.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) 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 17f670b3c190d..5331a9f94ef1f 100644 --- a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py +++ b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py @@ -12,15 +12,18 @@ class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): - def spawn_and_wait(self, program, delay): - time.sleep(delay) + def spawn(self, args): self.process = subprocess.Popen( - [program], + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) + + def spawn_and_wait(self, program, delay): + time.sleep(delay) + self.spawn([program]) self.process.wait() def continue_and_verify_pid(self): @@ -33,13 +36,7 @@ def test_by_pid(self): Tests attaching to a process by process ID. """ program = self.build_and_create_debug_adapter_for_attach() - self.process = subprocess.Popen( - [program], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) + self.spawn([program]) self.attach(pid=self.process.pid) self.continue_and_verify_pid() @@ -53,13 +50,7 @@ def test_by_name(self): pid_file_path = lldbutil.append_to_process_working_directory( self, "pid_file_%d" % (int(time.time())) ) - self.process = subprocess.Popen( - [program, pid_file_path], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) + self.spawn([program, pid_file_path]) lldbutil.wait_for_file_on_target(self, pid_file_path) self.attach(program=program)