diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index 150dc4d5f16f..919739dbcd1b 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -44,6 +44,7 @@ import signal from subprocess import * import sys import time +import datetime import traceback from typing import Optional, Union @@ -966,6 +967,9 @@ class Base(unittest.TestCase): self.lib_lldb = lib self.darwinWithFramework = self.platformIsDarwin() + # As the last operation, mark the setup completed for dumpSessionInfo. + self.__setup_done__ = True + def setAsync(self, value): """Sets async mode to True/False and ensures it is reset after the testcase completes.""" old_async = self.dbg.GetAsync() @@ -1247,16 +1251,18 @@ class Base(unittest.TestCase): Dump the debugger interactions leading to a test error/failure. This allows for more convenient postmortem analysis. - See also LLDBTestResult (dotest.py) which is a singlton class derived + See also LLDBTestResult (dotest.py) which is a singleton class derived from TextTestResult and overwrites addError, addFailure, and addExpectedFailure methods to allow us to to mark the test instance as such. """ + # Ensure 'setUp' has completed. + if not getattr(self, "__setup_done__", False): + return - # We are here because self.tearDown() detected that this test instance - # either errored or failed. The lldb.test_result singleton contains - # two lists (errors and failures) which get populated by the unittest - # framework. Look over there for stack trace information. + # The lldb.test_result singleton contains two lists (errors and + # failures) which get populated by the unittest framework. Look over + # there for stack trace information. # # The lists contain 2-tuples of TestCase instances and strings holding # formatted tracebacks. @@ -1286,16 +1292,15 @@ class Base(unittest.TestCase): session_file = self.getLogBasenameForCurrentTest() + ".log" + lldbutil.mkdir_p(os.path.dirname(session_file)) # Python 3 doesn't support unbuffered I/O in text mode. Open buffered. - session = encoded_file.open(session_file, "utf-8", mode="w") + session = encoded_file.open(session_file, "utf-8", mode="a") if not self.__unexpected__ and not self.__skipped__: for test, traceback in pairs: if test is self: print(traceback, file=session) - import datetime - print( "Session info generated @", datetime.datetime.now().ctime(), @@ -1307,8 +1312,12 @@ class Base(unittest.TestCase): # process the log files if prefix != "Success" or lldbtest_config.log_success: # keep all log files, rename them to include prefix + # e.g. .../TestDAP_module/Incomplete.log > Failure_.log src_log_basename = self.getLogBasenameForCurrentTest() - dst_log_basename = self.getLogBasenameForCurrentTest(prefix) + dst_log_basename = ( + f"{self.getLogBasenameForCurrentTest(prefix)}_{self.testMethodName}" + ) + files = [] for src in self.log_files: if os.path.isfile(src): dst = src.replace(src_log_basename, dst_log_basename) @@ -1323,6 +1332,12 @@ class Base(unittest.TestCase): lldbutil.mkdir_p(os.path.dirname(dst)) os.rename(src, dst) + files.append(dst) + if files: + print( + "Log Files:\n - %s" % ("\n - ".join(files)), + file=sys.stderr, + ) else: # success! (and we don't want log files) delete log files for log_file in self.log_files: diff --git a/lldb/packages/Python/lldbsuite/test/test_result.py b/lldb/packages/Python/lldbsuite/test/test_result.py index 2d574b343b41..383135d5f67c 100644 --- a/lldb/packages/Python/lldbsuite/test/test_result.py +++ b/lldb/packages/Python/lldbsuite/test/test_result.py @@ -296,3 +296,10 @@ class LLDBTestResult(unittest.TextTestResult): self.stream.write( "XPASS: LLDB (%s) :: %s\n" % (self._config_string(test), str(test)) ) + + def stopTest(self, test): + """Dump the session info for debugging.""" + dumpSessionInfo = getattr(test, "dumpSessionInfo", None) + if dumpSessionInfo: + dumpSessionInfo() + super().stopTest(test) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index b8aee69bbd28..5b23b921d295 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Protocol import argparse -import binascii import dataclasses import enum import json @@ -15,7 +14,6 @@ import pathlib import re import signal import socket -import string import subprocess import sys import threading @@ -126,16 +124,6 @@ class Breakpoint(TypedDict, total=False): return src.get("verified", False) -def dump_dap_log(log_file: Optional[str]) -> None: - print("========= DEBUG ADAPTER PROTOCOL LOGS =========", file=sys.stderr) - if log_file is None: - print("no log file available", file=sys.stderr) - else: - with open(log_file, "r") as file: - print(file.read(), file=sys.stderr) - print("========= END =========", file=sys.stderr) - - class NotSupportedError(KeyError): """Raised if a feature is not supported due to its capabilities.""" @@ -216,11 +204,9 @@ class DebugCommunication(object): recv: BinaryIO, send: BinaryIO, init_commands: Optional[List[str]] = None, - log_file: Optional[str] = None, spawn_helper: Optional[SpawnHelperCallback] = None, ): self._log = Log() - self.log_file = log_file self.send = send self.recv = recv self.spawn_helper = spawn_helper @@ -1656,8 +1642,6 @@ class DebugCommunication(object): self.send.close() if self._recv_thread.is_alive(): self._recv_thread.join() - if self.log_file: - dump_dap_log(self.log_file) def request_setInstructionBreakpoints(self, memory_reference=[]): breakpoints = [] @@ -1681,6 +1665,7 @@ class DebugAdapterServer(DebugCommunication): *, executable: Optional[str] = None, connection: Optional[str] = None, + connection_timeout: Optional[stintr] = None, init_commands: Optional[list[str]] = None, log_file: Optional[str] = None, env: Optional[Dict[str, str]] = None, @@ -1693,6 +1678,7 @@ class DebugAdapterServer(DebugCommunication): process, connection = DebugAdapterServer.launch( executable=executable, connection=connection, + connection_timeout=connection_timeout, env=env, log_file=log_file, additional_args=additional_args, @@ -1715,7 +1701,6 @@ class DebugAdapterServer(DebugCommunication): s.makefile("rb"), s.makefile("wb"), init_commands, - log_file, spawn_helper, ) self.connection = connection @@ -1724,7 +1709,6 @@ class DebugAdapterServer(DebugCommunication): self.process.stdout, self.process.stdin, init_commands, - log_file, spawn_helper, ) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py index 8edda1f5a7bd..714a8c228a76 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py @@ -24,26 +24,40 @@ class DAPTestCaseBase(TestBase): DEFAULT_TIMEOUT: Final[float] = dap_server.DEFAULT_TIMEOUT NO_DEBUG_INFO_TESTCASE = True + def setUp(self): + self.dap_server_count = 0 + super().setUp() + def create_debug_adapter( self, lldbDAPEnv: Optional[dict[str, str]] = None, connection: Optional[str] = None, + connection_timeout: Optional[int] = None, additional_args: Optional[list[str]] = None, ): """Create the Visual Studio Code debug adapter""" self.assertTrue( is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" ) - log_file_path = self.getBuildArtifact("dap.log") + if self.dap_server_count: + log_file_path = ( + self.getLogBasenameForCurrentTest() + + f"-dap-{self.dap_server_count}.log" + ) + else: + log_file_path = self.getLogBasenameForCurrentTest() + "-dap.log" + self.dap_server_count += 1 self.dap_server = dap_server.DebugAdapterServer( executable=self.lldbDAPExec, connection=connection, + connection_timeout=connection_timeout, init_commands=self.setUpCommands(), log_file=log_file_path, env=lldbDAPEnv, - additional_args=additional_args or [], + additional_args=additional_args, spawn_helper=self.spawnSubprocess, ) + self.log_files.append(log_file_path) def build_and_create_debug_adapter( self, diff --git a/lldb/test/API/tools/lldb-dap/io/TestDAP_io.py b/lldb/test/API/tools/lldb-dap/io/TestDAP_io.py index ffd860733ce5..19b2da1dfd6c 100644 --- a/lldb/test/API/tools/lldb-dap/io/TestDAP_io.py +++ b/lldb/test/API/tools/lldb-dap/io/TestDAP_io.py @@ -14,24 +14,15 @@ EXIT_SUCCESS = 0 class TestDAP_io(lldbdap_testcase.DAPTestCaseBase): def launch(self): - log_file_path = self.getBuildArtifact("dap.log") - process, _ = dap_server.DebugAdapterServer.launch( - executable=self.lldbDAPExec, log_file=log_file_path - ) + self.create_debug_adapter() + self.assertIsNotNone(self.dap_server.process) + process = self.dap_server.process def cleanup(): - # If the process is still alive, terminate it. + # If the process is still alive, kill it. if process.poll() is None: - process.terminate() + process.kill() process.wait() - stdout_data = process.stdout.read().decode() - print("========= STDOUT =========", file=sys.stderr) - print(stdout_data, file=sys.stderr) - print("========= END =========", file=sys.stderr) - print("========= DEBUG ADAPTER PROTOCOL LOGS =========", file=sys.stderr) - with open(log_file_path, "r") as file: - print(file.read(), file=sys.stderr) - print("========= END =========", file=sys.stderr) # Execute the cleanup function during test case tear down. self.addTearDownHook(cleanup) 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 1e5b23f71662..1cbda50e07dd 100644 --- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -11,31 +11,38 @@ import dap_server from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * import lldbdap_testcase +from subprocess import Popen +from typing import Tuple class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): def start_server( - self, connection, connection_timeout=None, wait_seconds_for_termination=None - ): - log_file_path = self.getBuildArtifact("dap.log") - (process, connection) = dap_server.DebugAdapterServer.launch( - executable=self.lldbDAPExec, - connection=connection, - connection_timeout=connection_timeout, - log_file=log_file_path, + 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(): - if wait_seconds_for_termination is not None: - process.wait(wait_seconds_for_termination) - else: - process.terminate() + try: + process.stdin.close() + process.wait(timeout=5) + except TimeoutExpired: + process.kill() self.addTearDownHook(cleanup) - return (process, connection) + return (process, self.dap_server.connection) - def run_debug_session(self, connection, name, sleep_seconds_in_middle=None): + def run_debug_session( + self, connection: str, name: str, *, sleep_seconds_in_middle: float = 0 + ): self.dap_server = dap_server.DebugAdapterServer( connection=connection, spawn_helper=self.spawnSubprocess ) @@ -48,7 +55,7 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): args=[name], disconnectAutomatically=False, ) - if sleep_seconds_in_middle is not None: + if sleep_seconds_in_middle: time.sleep(sleep_seconds_in_middle) self.set_source_breakpoints(source, [breakpoint_line]) self.continue_to_next_stop() @@ -88,14 +95,11 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): @skipIfWindows def test_server_interrupt(self): """ - Test launching a binary with lldb-dap in server mode and shutting down the server while the debug session is still active. + Test launching a binary with lldb-dap in server mode and shutting down + the server while the debug session is still active. """ self.build() - (process, connection) = self.start_server(connection="listen://localhost:0") - self.dap_server = dap_server.DebugAdapterServer( - connection=connection, - spawn_helper=self.spawnSubprocess, - ) + (process, _) = self.start_server(connection="listen://localhost:0") program = self.getBuildArtifact("a.out") source = "main.c" breakpoint_line = line_number(source, "// breakpoint") @@ -122,39 +126,39 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): @skipIfWindows def test_connection_timeout_at_server_start(self): """ - Test launching lldb-dap in server mode with connection timeout and waiting for it to terminate automatically when no client connects. + Test launching lldb-dap in server mode with connection timeout and + waiting for it to terminate automatically when no client connects. """ self.build() self.start_server( connection="listen://localhost:0", connection_timeout=1, - wait_seconds_for_termination=5, ) @skipIfWindows def test_connection_timeout_long_debug_session(self): """ - Test launching lldb-dap in server mode with connection timeout and terminating the server after the a long debug session. + Test launching lldb-dap in server mode with connection timeout and + terminating the server after the a long debug session. """ self.build() (_, connection) = self.start_server( connection="listen://localhost:0", connection_timeout=1, - wait_seconds_for_termination=5, ) # The connection timeout should not cut off the debug session - self.run_debug_session(connection, "Alice", 1.5) + self.run_debug_session(connection, "Alice", sleep_seconds_in_middle=1.5) @skipIfWindows def test_connection_timeout_multiple_sessions(self): """ - Test launching lldb-dap in server mode with connection timeout and terminating the server after the last debug session. + Test launching lldb-dap in server mode with connection timeout and + terminating the server after the last debug session. """ self.build() (_, connection) = self.start_server( connection="listen://localhost:0", connection_timeout=1, - wait_seconds_for_termination=5, ) time.sleep(0.5) # Should be able to connect to the server.