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 73f7b0e91d57a..d3589e78b6bc7 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 @@ -12,6 +12,13 @@ import sys import threading import time +from typing import Any, Optional, Union, BinaryIO, TextIO + +## DAP type references +Event = dict[str, Any] +Request = dict[str, Any] +Response = dict[str, Any] +ProtocolMessage = Union[Event, Request, Response] def dump_memory(base_addr, data, num_per_line, outfile): @@ -98,55 +105,40 @@ def dump_dap_log(log_file): print("========= END =========", file=sys.stderr) -def read_packet_thread(vs_comm, log_file): - done = False - try: - while not done: - packet = read_packet(vs_comm.recv, trace_file=vs_comm.trace_file) - # `packet` will be `None` on EOF. We want to pass it down to - # handle_recv_packet anyway so the main thread can handle unexpected - # termination of lldb-dap and stop waiting for new packets. - done = not vs_comm.handle_recv_packet(packet) - finally: - # Wait for the process to fully exit before dumping the log file to - # ensure we have the entire log contents. - if vs_comm.process is not None: - try: - # Do not wait forever, some logs are better than none. - vs_comm.process.wait(timeout=20) - except subprocess.TimeoutExpired: - pass - dump_dap_log(log_file) - - class DebugCommunication(object): - def __init__(self, recv, send, init_commands, log_file=None): - self.trace_file = None + def __init__( + self, + recv: BinaryIO, + send: BinaryIO, + init_commands: list[str], + log_file: Optional[TextIO] = None, + ): + # For debugging test failures, try setting `trace_file = sys.stderr`. + self.trace_file: Optional[TextIO] = None + self.log_file = log_file self.send = send self.recv = recv - self.recv_packets = [] + self.recv_packets: list[Optional[ProtocolMessage]] = [] self.recv_condition = threading.Condition() - self.recv_thread = threading.Thread( - target=read_packet_thread, args=(self, log_file) - ) + self.recv_thread = threading.Thread(target=self._read_packet_thread) self.process_event_body = None - self.exit_status = None + self.exit_status: Optional[int] = None self.initialize_body = None - self.thread_stop_reasons = {} - self.progress_events = [] + self.progress_events: list[Event] = [] self.reverse_requests = [] self.sequence = 1 self.threads = None + self.thread_stop_reasons = {} self.recv_thread.start() self.output_condition = threading.Condition() - self.output = {} + self.output: dict[str, list[str]] = {} self.configuration_done_sent = False self.frame_scopes = {} self.init_commands = init_commands self.disassembled_instructions = {} @classmethod - def encode_content(cls, s): + def encode_content(cls, s: str) -> bytes: return ("Content-Length: %u\r\n\r\n%s" % (len(s), s)).encode("utf-8") @classmethod @@ -156,6 +148,18 @@ def validate_response(cls, command, response): if command["seq"] != response["request_seq"]: raise ValueError("seq mismatch in response") + def _read_packet_thread(self): + done = False + try: + while not done: + packet = read_packet(self.recv, trace_file=self.trace_file) + # `packet` will be `None` on EOF. We want to pass it down to + # handle_recv_packet anyway so the main thread can handle unexpected + # termination of lldb-dap and stop waiting for new packets. + done = not self._handle_recv_packet(packet) + finally: + dump_dap_log(self.log_file) + def get_modules(self): module_list = self.request_modules()["body"]["modules"] modules = {} @@ -190,13 +194,13 @@ def collect_output(self, category, timeout_secs, pattern, clear=True): break return collected_output if collected_output else None - def enqueue_recv_packet(self, packet): + def _enqueue_recv_packet(self, packet: Optional[ProtocolMessage]): self.recv_condition.acquire() self.recv_packets.append(packet) self.recv_condition.notify() self.recv_condition.release() - def handle_recv_packet(self, packet): + def _handle_recv_packet(self, packet: Optional[ProtocolMessage]) -> bool: """Called by the read thread that is waiting for all incoming packets to store the incoming packet in "self.recv_packets" in a thread safe way. This function will then signal the "self.recv_condition" to @@ -205,7 +209,7 @@ def handle_recv_packet(self, packet): """ # If EOF, notify the read thread by enqueuing a None. if not packet: - self.enqueue_recv_packet(None) + self._enqueue_recv_packet(None) return False # Check the packet to see if is an event packet @@ -235,6 +239,18 @@ def handle_recv_packet(self, packet): # When a new process is attached or launched, remember the # details that are available in the body of the event self.process_event_body = body + elif event == "exited": + # Process exited, mark the status to indicate the process is not + # alive. + self.exit_status = body["exitCode"] + elif event == "continued": + # When the process continues, clear the known threads and + # thread_stop_reasons. + all_threads_continued = body.get("allThreadsContinued", True) + tid = body["threadId"] + if tid in self.thread_stop_reasons: + del self.thread_stop_reasons[tid] + self._process_continued(all_threads_continued) elif event == "stopped": # Each thread that stops with a reason will send a # 'stopped' event. We need to remember the thread stop @@ -252,10 +268,16 @@ def handle_recv_packet(self, packet): elif packet_type == "response": if packet["command"] == "disconnect": keepGoing = False - self.enqueue_recv_packet(packet) + self._enqueue_recv_packet(packet) return keepGoing - def send_packet(self, command_dict, set_sequence=True): + def _process_continued(self, all_threads_continued: bool): + self.threads = None + self.frame_scopes = {} + if all_threads_continued: + self.thread_stop_reasons = {} + + def send_packet(self, command_dict: Request, set_sequence=True): """Take the "command_dict" python dictionary and encode it as a JSON string and send the contents as a packet to the VSCode debug adapter""" @@ -273,7 +295,12 @@ def send_packet(self, command_dict, set_sequence=True): self.send.write(self.encode_content(json_str)) self.send.flush() - def recv_packet(self, filter_type=None, filter_event=None, timeout=None): + def recv_packet( + self, + filter_type: Optional[str] = None, + filter_event: Optional[Union[str, list[str]]] = None, + timeout: Optional[float] = None, + ) -> Optional[ProtocolMessage]: """Get a JSON packet from the VSCode debug adapter. This function assumes a thread that reads packets is running and will deliver any received packets by calling handle_recv_packet(...). This @@ -309,8 +336,6 @@ def recv_packet(self, filter_type=None, filter_event=None, timeout=None): finally: self.recv_condition.release() - return None - def send_recv(self, command): """Send a command python dictionary as JSON and receive the JSON response. Validates that the response is the correct sequence and @@ -360,47 +385,36 @@ def send_recv(self, command): return None - def wait_for_event(self, filter=None, timeout=None): - while True: - return self.recv_packet( - filter_type="event", filter_event=filter, timeout=timeout - ) - return None - - def wait_for_events(self, events, timeout=None): - """Wait for a list of events in `events` in any order. - Return the events not hit before the timeout expired""" - events = events[:] # Make a copy to avoid modifying the input - while events: - event_dict = self.wait_for_event(filter=events, timeout=timeout) - if event_dict is None: - break - events.remove(event_dict["event"]) - return events + def wait_for_event( + self, filter: Union[str, list[str]], timeout: Optional[float] = None + ) -> Optional[Event]: + """Wait for the first event that matches the filter.""" + return self.recv_packet( + filter_type="event", filter_event=filter, timeout=timeout + ) - def wait_for_stopped(self, timeout=None): + def wait_for_stopped( + self, timeout: Optional[float] = None + ) -> Optional[list[Event]]: stopped_events = [] stopped_event = self.wait_for_event( filter=["stopped", "exited"], timeout=timeout ) - exited = False while stopped_event: stopped_events.append(stopped_event) # If we exited, then we are done if stopped_event["event"] == "exited": - self.exit_status = stopped_event["body"]["exitCode"] - exited = True break # Otherwise we stopped and there might be one or more 'stopped' # events for each thread that stopped with a reason, so keep # checking for more 'stopped' events and return all of them - stopped_event = self.wait_for_event(filter="stopped", timeout=0.25) - if exited: - self.threads = [] + stopped_event = self.wait_for_event( + filter=["stopped", "exited"], timeout=0.25 + ) return stopped_events - def wait_for_breakpoint_events(self, timeout=None): - breakpoint_events = [] + def wait_for_breakpoint_events(self, timeout: Optional[float] = None): + breakpoint_events: list[Event] = [] while True: event = self.wait_for_event("breakpoint", timeout=timeout) if not event: @@ -408,14 +422,14 @@ def wait_for_breakpoint_events(self, timeout=None): breakpoint_events.append(event) return breakpoint_events - def wait_for_exited(self): - event_dict = self.wait_for_event("exited") + def wait_for_exited(self, timeout: Optional[float] = None): + event_dict = self.wait_for_event("exited", timeout=timeout) if event_dict is None: raise ValueError("didn't get exited event") return event_dict - def wait_for_terminated(self): - event_dict = self.wait_for_event("terminated") + def wait_for_terminated(self, timeout: Optional[float] = None): + event_dict = self.wait_for_event("terminated", timeout) if event_dict is None: raise ValueError("didn't get terminated event") return event_dict @@ -576,32 +590,30 @@ def replay_packets(self, replay_file_path): def request_attach( self, - program=None, - pid=None, - waitFor=None, - trace=None, - initCommands=None, - preRunCommands=None, - stopCommands=None, - exitCommands=None, - attachCommands=None, - terminateCommands=None, - coreFile=None, + *, + program: Optional[str] = None, + pid: Optional[int] = None, + waitFor=False, + initCommands: Optional[list[str]] = None, + preRunCommands: Optional[list[str]] = None, + attachCommands: Optional[list[str]] = None, + postRunCommands: Optional[list[str]] = None, + stopCommands: Optional[list[str]] = None, + exitCommands: Optional[list[str]] = None, + terminateCommands: Optional[list[str]] = None, + coreFile: Optional[str] = None, stopOnAttach=True, - postRunCommands=None, - sourceMap=None, - gdbRemotePort=None, - gdbRemoteHostname=None, + sourceMap: Optional[Union[list[tuple[str, str]], dict[str, str]]] = None, + gdbRemotePort: Optional[int] = None, + gdbRemoteHostname: Optional[str] = None, ): args_dict = {} if pid is not None: args_dict["pid"] = pid if program is not None: args_dict["program"] = program - if waitFor is not None: + if waitFor: args_dict["waitFor"] = waitFor - if trace: - args_dict["trace"] = trace args_dict["initCommands"] = self.init_commands if initCommands: args_dict["initCommands"].extend(initCommands) @@ -671,7 +683,7 @@ def _process_stopped(self): self.threads = None self.frame_scopes = {} - def request_continue(self, threadId=None): + def request_continue(self, threadId=None, singleThread=False): if self.exit_status is not None: raise ValueError("request_continue called after process exited") # If we have launched or attached, then the first continue is done by @@ -681,13 +693,18 @@ def request_continue(self, threadId=None): args_dict = {} if threadId is None: threadId = self.get_thread_id() - args_dict["threadId"] = threadId + if threadId: + args_dict["threadId"] = threadId + if singleThread: + args_dict["singleThread"] = True command_dict = { "command": "continue", "type": "request", "arguments": args_dict, } response = self.send_recv(command_dict) + if response["success"]: + self._process_continued(response["body"]["allThreadsContinued"]) # Caller must still call wait_for_stopped. return response @@ -775,7 +792,7 @@ def request_exceptionInfo(self, threadId=None): } return self.send_recv(command_dict) - def request_initialize(self, sourceInitFile): + def request_initialize(self, sourceInitFile=False): command_dict = { "command": "initialize", "type": "request", @@ -802,32 +819,32 @@ def request_initialize(self, sourceInitFile): def request_launch( self, - program, - args=None, - cwd=None, - env=None, - stopOnEntry=False, + program: str, + *, + args: Optional[list[str]] = None, + cwd: Optional[str] = None, + env: Optional[dict[str, str]] = None, + stopOnEntry=True, disableASLR=True, disableSTDIO=False, shellExpandArguments=False, - trace=False, - initCommands=None, - preRunCommands=None, - stopCommands=None, - exitCommands=None, - terminateCommands=None, - sourcePath=None, - debuggerRoot=None, - launchCommands=None, - sourceMap=None, runInTerminal=False, - postRunCommands=None, enableAutoVariableSummaries=False, displayExtendedBacktrace=False, enableSyntheticChildDebugging=False, - commandEscapePrefix=None, - customFrameFormat=None, - customThreadFormat=None, + initCommands: Optional[list[str]] = None, + preRunCommands: Optional[list[str]] = None, + launchCommands: Optional[list[str]] = None, + postRunCommands: Optional[list[str]] = None, + stopCommands: Optional[list[str]] = None, + exitCommands: Optional[list[str]] = None, + terminateCommands: Optional[list[str]] = None, + sourceMap: Optional[Union[list[tuple[str, str]], dict[str, str]]] = None, + sourcePath: Optional[str] = None, + debuggerRoot: Optional[str] = None, + commandEscapePrefix: Optional[str] = None, + customFrameFormat: Optional[str] = None, + customThreadFormat: Optional[str] = None, ): args_dict = {"program": program} if args: @@ -842,8 +859,6 @@ def request_launch( args_dict["disableSTDIO"] = disableSTDIO if shellExpandArguments: args_dict["shellExpandArguments"] = shellExpandArguments - if trace: - args_dict["trace"] = trace args_dict["initCommands"] = self.init_commands if initCommands: args_dict["initCommands"].extend(initCommands) @@ -1190,7 +1205,8 @@ def request_testGetTargetBreakpoints(self): def terminate(self): self.send.close() - # self.recv.close() + if self.recv_thread.is_alive(): + self.recv_thread.join() def request_setInstructionBreakpoints(self, memory_reference=[]): breakpoints = [] @@ -1211,11 +1227,11 @@ def request_setInstructionBreakpoints(self, memory_reference=[]): class DebugAdapterServer(DebugCommunication): def __init__( self, - executable=None, - connection=None, - init_commands=[], - log_file=None, - env=None, + executable: Optional[str] = None, + connection: Optional[str] = None, + init_commands: list[str] = [], + log_file: Optional[TextIO] = None, + env: Optional[dict[str, str]] = None, ): self.process = None self.connection = None @@ -1247,7 +1263,14 @@ def __init__( ) @classmethod - def launch(cls, /, executable, env=None, log_file=None, connection=None): + def launch( + cls, + *, + executable: str, + env: Optional[dict[str, str]] = None, + log_file: Optional[TextIO] = None, + connection: Optional[str] = None, + ) -> tuple[subprocess.Popen, Optional[str]]: adapter_env = os.environ.copy() if env is not None: adapter_env.update(env) @@ -1289,26 +1312,29 @@ def launch(cls, /, executable, env=None, log_file=None, connection=None): return (process, connection) - def get_pid(self): + def get_pid(self) -> int: if self.process: return self.process.pid return -1 def terminate(self): - super(DebugAdapterServer, self).terminate() - if self.process is not None: - process = self.process - self.process = None - try: - # When we close stdin it should signal the lldb-dap that no - # new messages will arrive and it should shutdown on its own. - process.stdin.close() - process.wait(timeout=20) - except subprocess.TimeoutExpired: - process.kill() - process.wait() - if process.returncode != 0: - raise DebugAdapterProcessError(process.returncode) + try: + if self.process is not None: + process = self.process + self.process = None + try: + # When we close stdin it should signal the lldb-dap that no + # new messages will arrive and it should shutdown on its + # own. + process.stdin.close() + process.wait(timeout=20) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + if process.returncode != 0: + raise DebugAdapterProcessError(process.returncode) + finally: + super(DebugAdapterServer, self).terminate() class DebugAdapterError(Exception): 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 c5a7eb76a58c7..d7cf8e2864324 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 @@ -1,5 +1,6 @@ import os import time +from typing import Optional import uuid import dap_server @@ -11,10 +12,14 @@ class DAPTestCaseBase(TestBase): # set timeout based on whether ASAN was enabled or not. Increase # timeout by a factor of 10 if ASAN is enabled. - timeoutval = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1) + DEFAULT_TIMEOUT = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1) NO_DEBUG_INFO_TESTCASE = True - def create_debug_adapter(self, lldbDAPEnv=None, connection=None): + def create_debug_adapter( + self, + lldbDAPEnv: Optional[dict[str, str]] = None, + connection: Optional[str] = None, + ): """Create the Visual Studio Code debug adapter""" self.assertTrue( is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" @@ -28,7 +33,11 @@ def create_debug_adapter(self, lldbDAPEnv=None, connection=None): env=lldbDAPEnv, ) - def build_and_create_debug_adapter(self, lldbDAPEnv=None, dictionary=None): + def build_and_create_debug_adapter( + self, + lldbDAPEnv: Optional[dict[str, str]] = None, + dictionary: Optional[dict] = None, + ): self.build(dictionary=dictionary) self.create_debug_adapter(lldbDAPEnv) @@ -78,13 +87,13 @@ def waitUntil(self, condition_callback): time.sleep(0.5) return False - def verify_breakpoint_hit(self, breakpoint_ids): + def verify_breakpoint_hit(self, breakpoint_ids, timeout=DEFAULT_TIMEOUT): """Wait for the process we are debugging to stop, and verify we hit any breakpoint location in the "breakpoint_ids" array. "breakpoint_ids" should be a list of breakpoint ID strings (["1", "2"]). The return value from self.set_source_breakpoints() or self.set_function_breakpoints() can be passed to this function""" - stopped_events = self.dap_server.wait_for_stopped() + stopped_events = self.dap_server.wait_for_stopped(timeout) for stopped_event in stopped_events: if "body" in stopped_event: body = stopped_event["body"] @@ -110,16 +119,15 @@ def verify_breakpoint_hit(self, breakpoint_ids): match_desc = "breakpoint %s." % (breakpoint_id) if match_desc in description: return - self.assertTrue(False, "breakpoint not hit") + self.assertTrue(False, f"breakpoint not hit, stopped_events={stopped_events}") - def verify_stop_exception_info(self, expected_description, timeout=timeoutval): + def verify_stop_exception_info(self, expected_description, timeout=DEFAULT_TIMEOUT): """Wait for the process we are debugging to stop, and verify the stop reason is 'exception' and that the description matches 'expected_description' """ - stopped_events = self.dap_server.wait_for_stopped(timeout=timeout) + stopped_events = self.dap_server.wait_for_stopped(timeout) for stopped_event in stopped_events: - print("stopped_event", stopped_event) if "body" in stopped_event: body = stopped_event["body"] if "reason" not in body: @@ -263,46 +271,61 @@ def set_global(self, name, value, id=None): return self.dap_server.request_setVariable(2, name, str(value), id=id) def stepIn( - self, threadId=None, targetId=None, waitForStop=True, granularity="statement" + self, + threadId=None, + targetId=None, + waitForStop=True, + granularity="statement", + timeout=DEFAULT_TIMEOUT, ): response = self.dap_server.request_stepIn( threadId=threadId, targetId=targetId, granularity=granularity ) self.assertTrue(response["success"]) if waitForStop: - return self.dap_server.wait_for_stopped() + return self.dap_server.wait_for_stopped(timeout) return None - def stepOver(self, threadId=None, waitForStop=True, granularity="statement"): + def stepOver( + self, + threadId=None, + waitForStop=True, + granularity="statement", + timeout=DEFAULT_TIMEOUT, + ): self.dap_server.request_next(threadId=threadId, granularity=granularity) if waitForStop: - return self.dap_server.wait_for_stopped() + return self.dap_server.wait_for_stopped(timeout) return None - def stepOut(self, threadId=None, waitForStop=True): + def stepOut(self, threadId=None, waitForStop=True, timeout=DEFAULT_TIMEOUT): self.dap_server.request_stepOut(threadId=threadId) if waitForStop: - return self.dap_server.wait_for_stopped() + return self.dap_server.wait_for_stopped(timeout) return None - def continue_to_next_stop(self): - self.dap_server.request_continue() - return self.dap_server.wait_for_stopped() + def do_continue(self): # `continue` is a keyword. + resp = self.dap_server.request_continue() + self.assertTrue(resp["success"], f"continue request failed: {resp}") + + def continue_to_next_stop(self, timeout=DEFAULT_TIMEOUT): + self.do_continue() + return self.dap_server.wait_for_stopped(timeout) - def continue_to_breakpoints(self, breakpoint_ids): - self.dap_server.request_continue() - self.verify_breakpoint_hit(breakpoint_ids) + def continue_to_breakpoints(self, breakpoint_ids, timeout=DEFAULT_TIMEOUT): + self.do_continue() + self.verify_breakpoint_hit(breakpoint_ids, timeout) - def continue_to_exception_breakpoint(self, filter_label): - self.dap_server.request_continue() + def continue_to_exception_breakpoint(self, filter_label, timeout=DEFAULT_TIMEOUT): + self.do_continue() self.assertTrue( - self.verify_stop_exception_info(filter_label), + self.verify_stop_exception_info(filter_label, timeout), 'verify we got "%s"' % (filter_label), ) - def continue_to_exit(self, exitCode=0): - self.dap_server.request_continue() - stopped_events = self.dap_server.wait_for_stopped() + def continue_to_exit(self, exitCode=0, timeout=DEFAULT_TIMEOUT): + self.do_continue() + stopped_events = self.dap_server.wait_for_stopped(timeout) self.assertEqual( len(stopped_events), 1, "stopped_events = {}".format(stopped_events) ) @@ -330,27 +353,15 @@ def disassemble(self, threadId=None, frameIndex=None): def attach( self, - program=None, - pid=None, - waitFor=None, - trace=None, - initCommands=None, - preRunCommands=None, - stopCommands=None, - exitCommands=None, - attachCommands=None, - coreFile=None, + *, stopOnAttach=True, disconnectAutomatically=True, - terminateCommands=None, - postRunCommands=None, - sourceMap=None, sourceInitFile=False, expectFailure=False, - gdbRemotePort=None, - gdbRemoteHostname=None, sourceBreakpoints=None, functionBreakpoints=None, + timeout=DEFAULT_TIMEOUT, + **kwargs, ): """Build the default Makefile target, create the DAP debug adapter, and attach to the process. @@ -367,7 +378,7 @@ def cleanup(): self.addTearDownHook(cleanup) # Initialize and launch the program self.dap_server.request_initialize(sourceInitFile) - self.dap_server.wait_for_event("initialized") + self.dap_server.wait_for_event("initialized", timeout) # Set source breakpoints as part of the launch sequence. if sourceBreakpoints: @@ -389,64 +400,28 @@ def cleanup(): ) self.dap_server.request_configurationDone() - response = self.dap_server.request_attach( - program=program, - pid=pid, - waitFor=waitFor, - trace=trace, - initCommands=initCommands, - preRunCommands=preRunCommands, - stopCommands=stopCommands, - exitCommands=exitCommands, - attachCommands=attachCommands, - terminateCommands=terminateCommands, - coreFile=coreFile, - stopOnAttach=stopOnAttach, - postRunCommands=postRunCommands, - sourceMap=sourceMap, - gdbRemotePort=gdbRemotePort, - gdbRemoteHostname=gdbRemoteHostname, - ) + response = self.dap_server.request_attach(stopOnAttach=stopOnAttach, **kwargs) if expectFailure: return response if not (response and response["success"]): self.assertTrue( response["success"], "attach failed (%s)" % (response["message"]) ) + if stopOnAttach: + self.dap_server.wait_for_stopped(timeout) def launch( self, program=None, - args=None, - cwd=None, - env=None, - stopOnEntry=False, - disableASLR=False, - disableSTDIO=False, - shellExpandArguments=False, - trace=False, - initCommands=None, - preRunCommands=None, - stopCommands=None, - exitCommands=None, - terminateCommands=None, - sourcePath=None, - debuggerRoot=None, + *, sourceInitFile=False, - launchCommands=None, - sourceMap=None, disconnectAutomatically=True, - runInTerminal=False, - expectFailure=False, - postRunCommands=None, - enableAutoVariableSummaries=False, - displayExtendedBacktrace=False, - enableSyntheticChildDebugging=False, - commandEscapePrefix=None, - customFrameFormat=None, - customThreadFormat=None, sourceBreakpoints=None, functionBreakpoints=None, + expectFailure=False, + stopOnEntry=True, + timeout=DEFAULT_TIMEOUT, + **kwargs, ): """Sending launch request to dap""" @@ -462,7 +437,7 @@ def cleanup(): # Initialize and launch the program self.dap_server.request_initialize(sourceInitFile) - self.dap_server.wait_for_event("initialized") + self.dap_server.wait_for_event("initialized", timeout) # Set source breakpoints as part of the launch sequence. if sourceBreakpoints: @@ -487,76 +462,28 @@ def cleanup(): response = self.dap_server.request_launch( program, - args=args, - cwd=cwd, - env=env, stopOnEntry=stopOnEntry, - disableASLR=disableASLR, - disableSTDIO=disableSTDIO, - shellExpandArguments=shellExpandArguments, - trace=trace, - initCommands=initCommands, - preRunCommands=preRunCommands, - stopCommands=stopCommands, - exitCommands=exitCommands, - terminateCommands=terminateCommands, - sourcePath=sourcePath, - debuggerRoot=debuggerRoot, - launchCommands=launchCommands, - sourceMap=sourceMap, - runInTerminal=runInTerminal, - postRunCommands=postRunCommands, - enableAutoVariableSummaries=enableAutoVariableSummaries, - displayExtendedBacktrace=displayExtendedBacktrace, - enableSyntheticChildDebugging=enableSyntheticChildDebugging, - commandEscapePrefix=commandEscapePrefix, - customFrameFormat=customFrameFormat, - customThreadFormat=customThreadFormat, + **kwargs, ) if expectFailure: return response - if not (response and response["success"]): self.assertTrue( response["success"], "launch failed (%s)" % (response["body"]["error"]["format"]), ) + if stopOnEntry: + self.dap_server.wait_for_stopped(timeout) + return response def build_and_launch( self, program, - args=None, - cwd=None, - env=None, - stopOnEntry=False, - disableASLR=False, - disableSTDIO=False, - shellExpandArguments=False, - trace=False, - initCommands=None, - preRunCommands=None, - stopCommands=None, - exitCommands=None, - terminateCommands=None, - sourcePath=None, - debuggerRoot=None, - sourceInitFile=False, - runInTerminal=False, - disconnectAutomatically=True, - postRunCommands=None, - lldbDAPEnv=None, - enableAutoVariableSummaries=False, - displayExtendedBacktrace=False, - enableSyntheticChildDebugging=False, - commandEscapePrefix=None, - customFrameFormat=None, - customThreadFormat=None, - launchCommands=None, - expectFailure=False, - sourceBreakpoints=None, - functionBreakpoints=None, + *, + lldbDAPEnv: Optional[dict[str, str]] = None, + **kwargs, ): """Build the default Makefile target, create the DAP debug adapter, and launch the process. @@ -564,38 +491,7 @@ def build_and_launch( self.build_and_create_debug_adapter(lldbDAPEnv) self.assertTrue(os.path.exists(program), "executable must exist") - return self.launch( - program, - args, - cwd, - env, - stopOnEntry, - disableASLR, - disableSTDIO, - shellExpandArguments, - trace, - initCommands, - preRunCommands, - stopCommands, - exitCommands, - terminateCommands, - sourcePath, - debuggerRoot, - sourceInitFile, - runInTerminal=runInTerminal, - disconnectAutomatically=disconnectAutomatically, - postRunCommands=postRunCommands, - enableAutoVariableSummaries=enableAutoVariableSummaries, - enableSyntheticChildDebugging=enableSyntheticChildDebugging, - displayExtendedBacktrace=displayExtendedBacktrace, - commandEscapePrefix=commandEscapePrefix, - customFrameFormat=customFrameFormat, - customThreadFormat=customThreadFormat, - launchCommands=launchCommands, - expectFailure=expectFailure, - sourceBreakpoints=sourceBreakpoints, - functionBreakpoints=functionBreakpoints, - ) + return self.launch(program, **kwargs) def getBuiltinDebugServerTool(self): # Tries to find simulation/lldb-server/gdbserver tool path. 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 a9218d3c3dde3..55557e6e0030e 100644 --- a/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py +++ b/lldb/test/API/tools/lldb-dap/attach/TestDAP_attach.py @@ -2,15 +2,11 @@ Test lldb-dap attach request """ -import dap_server from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbutil import lldbdap_testcase -import os -import shutil import subprocess -import tempfile import threading import time @@ -26,8 +22,6 @@ def spawn_and_wait(program, delay): class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase): def set_and_hit_breakpoint(self, continueToExit=True): - self.dap_server.wait_for_stopped() - source = "main.c" breakpoint1_line = line_number(source, "// breakpoint 1") lines = [breakpoint1_line] @@ -36,7 +30,12 @@ def set_and_hit_breakpoint(self, continueToExit=True): self.assertEqual( len(breakpoint_ids), len(lines), "expect correct number of breakpoints" ) - self.continue_to_breakpoints(breakpoint_ids) + # 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 + ) if continueToExit: self.continue_to_exit() @@ -160,7 +159,7 @@ def test_commands(self): # 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.dap_server.request_continue() + self.do_continue() time.sleep(0.5) self.dap_server.request_pause() self.dap_server.wait_for_stopped() @@ -198,9 +197,6 @@ def test_attach_command_process_failures(self): ) @skipIfNetBSD # Hangs on NetBSD as well - @skipIf( - archs=["arm", "aarch64"] - ) # Example of a flaky run http://lab.llvm.org:8011/builders/lldb-aarch64-ubuntu/builds/5517/steps/test/logs/stdio def test_terminate_commands(self): """ Tests that the "terminateCommands", that can be passed during diff --git a/lldb/test/API/tools/lldb-dap/breakpoint-events/TestDAP_breakpointEvents.py b/lldb/test/API/tools/lldb-dap/breakpoint-events/TestDAP_breakpointEvents.py index 8581f10cef22a..25f031db5cac5 100644 --- a/lldb/test/API/tools/lldb-dap/breakpoint-events/TestDAP_breakpointEvents.py +++ b/lldb/test/API/tools/lldb-dap/breakpoint-events/TestDAP_breakpointEvents.py @@ -60,7 +60,7 @@ def test_breakpoint_events(self): response = self.dap_server.request_setBreakpoints( main_source_path, [main_bp_line] ) - self.assertTrue(response) + self.assertTrue(response["success"]) breakpoints = response["body"]["breakpoints"] for breakpoint in breakpoints: main_bp_id = breakpoint["id"] @@ -72,7 +72,7 @@ def test_breakpoint_events(self): response = self.dap_server.request_setBreakpoints( foo_source_path, [foo_bp1_line] ) - self.assertTrue(response) + self.assertTrue(response["success"]) breakpoints = response["body"]["breakpoints"] for breakpoint in breakpoints: foo_bp_id = breakpoint["id"] @@ -81,9 +81,6 @@ def test_breakpoint_events(self): breakpoint["verified"], "expect foo breakpoint to not be verified" ) - # Make sure we're stopped. - self.dap_server.wait_for_stopped() - # Flush the breakpoint events. self.dap_server.wait_for_breakpoint_events(timeout=5) diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py index 479a91208a66c..948c146d4da68 100644 --- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py +++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py @@ -9,7 +9,7 @@ import lldbdap_testcase -class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase): +class TestDAP_cancel(lldbdap_testcase.DAPTestCaseBase): def send_async_req(self, command: str, arguments={}) -> int: seq = self.dap_server.sequence self.dap_server.send_packet( @@ -45,14 +45,13 @@ def test_pending_request(self): """ program = self.getBuildArtifact("a.out") self.build_and_launch(program, stopOnEntry=True) - self.continue_to_next_stop() # Use a relatively short timeout since this is only to ensure the # following request is queued. blocking_seq = self.async_blocking_request(duration=1.0) # Use a longer timeout to ensure we catch if the request was interrupted # properly. - pending_seq = self.async_blocking_request(duration=self.timeoutval / 2) + pending_seq = self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2) cancel_seq = self.async_cancel(requestId=pending_seq) blocking_resp = self.dap_server.recv_packet(filter_type=["response"]) @@ -78,12 +77,11 @@ def test_inflight_request(self): """ program = self.getBuildArtifact("a.out") self.build_and_launch(program, stopOnEntry=True) - self.continue_to_next_stop() - blocking_seq = self.async_blocking_request(duration=self.timeoutval / 2) + blocking_seq = self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2) # Wait for the sleep to start to cancel the inflight request. self.collect_console( - timeout_secs=self.timeoutval, + timeout_secs=self.DEFAULT_TIMEOUT, pattern="starting sleep", ) cancel_seq = self.async_cancel(requestId=blocking_seq) diff --git a/lldb/test/API/tools/lldb-dap/commands/TestDAP_commands.py b/lldb/test/API/tools/lldb-dap/commands/TestDAP_commands.py index 223258fbdd3dc..ea6b2ea7f28ab 100644 --- a/lldb/test/API/tools/lldb-dap/commands/TestDAP_commands.py +++ b/lldb/test/API/tools/lldb-dap/commands/TestDAP_commands.py @@ -75,11 +75,12 @@ def test_command_directive_abort_on_error_attach_commands(self): ) command_abort_on_error = "settings set foo bar" program = self.build_and_create_debug_adapter_for_attach() - self.attach( - program, + resp = self.attach( + program=program, attachCommands=["?!" + command_quiet, "!" + command_abort_on_error], expectFailure=True, ) + self.assertFalse(resp["success"], "expected 'attach' failure") full_output = self.collect_console( timeout_secs=1.0, pattern=command_abort_on_error, diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index a94288c7a669e..75876c248f86c 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -43,12 +43,12 @@ def verify_completions(self, actual_list, expected_list, not_expected_list=[]): for not_expected_item in not_expected_list: self.assertNotIn(not_expected_item, actual_list) - def setup_debugee(self, stopOnEntry=False): + def setup_debuggee(self): program = self.getBuildArtifact("a.out") source = "main.cpp" self.build_and_launch( program, - stopOnEntry=stopOnEntry, + stopOnEntry=True, sourceBreakpoints=[ ( source, @@ -64,7 +64,7 @@ def test_command_completions(self): """ Tests completion requests for lldb commands, within "repl-mode=command" """ - self.setup_debugee() + self.setup_debuggee() self.continue_to_next_stop() res = self.dap_server.request_evaluate( @@ -143,7 +143,7 @@ def test_variable_completions(self): """ Tests completion requests in "repl-mode=variable" """ - self.setup_debugee() + self.setup_debuggee() self.continue_to_next_stop() res = self.dap_server.request_evaluate( @@ -241,7 +241,7 @@ def test_auto_completions(self): """ Tests completion requests in "repl-mode=auto" """ - self.setup_debugee(stopOnEntry=True) + self.setup_debuggee() res = self.dap_server.request_evaluate( "`lldb-dap repl-mode auto", context="repl" diff --git a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py index 0b428994e7fba..1f810afdbb667 100644 --- a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py +++ b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py @@ -177,7 +177,7 @@ def test_diagnositcs(self): ) diagnostics = self.collect_important( - timeout_secs=self.timeoutval, pattern="minidump file" + timeout_secs=self.DEFAULT_TIMEOUT, pattern="minidump file" ) self.assertIn( diff --git a/lldb/test/API/tools/lldb-dap/coreFile/TestDAP_coreFile.py b/lldb/test/API/tools/lldb-dap/coreFile/TestDAP_coreFile.py index 1896acea15a99..e678c5ee77fdc 100644 --- a/lldb/test/API/tools/lldb-dap/coreFile/TestDAP_coreFile.py +++ b/lldb/test/API/tools/lldb-dap/coreFile/TestDAP_coreFile.py @@ -2,7 +2,6 @@ Test lldb-dap coreFile attaching """ - import dap_server from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * @@ -19,7 +18,7 @@ def test_core_file(self): core_file = os.path.join(current_dir, "linux-x86_64.core") self.create_debug_adapter() - self.attach(exe_file, coreFile=core_file) + self.attach(program=exe_file, coreFile=core_file) expected_frames = [ { @@ -51,7 +50,8 @@ def test_core_file(self): self.assertEqual(self.get_stackFrames(), expected_frames) # Resuming should have no effect and keep the process stopped - self.continue_to_next_stop() + resp = self.dap_server.request_continue() + self.assertFalse(resp["success"]) self.assertEqual(self.get_stackFrames(), expected_frames) self.dap_server.request_next(threadId=32259) @@ -67,7 +67,7 @@ def test_core_file_source_mapping_array(self): self.create_debug_adapter() source_map = [["/home/labath/test", current_dir]] - self.attach(exe_file, coreFile=core_file, sourceMap=source_map) + self.attach(program=exe_file, coreFile=core_file, sourceMap=source_map) self.assertIn(current_dir, self.get_stackFrames()[0]["source"]["path"]) @@ -81,6 +81,6 @@ def test_core_file_source_mapping_object(self): self.create_debug_adapter() source_map = {"/home/labath/test": current_dir} - self.attach(exe_file, coreFile=core_file, sourceMap=source_map) + self.attach(program=exe_file, coreFile=core_file, sourceMap=source_map) self.assertIn(current_dir, self.get_stackFrames()[0]["source"]["path"]) diff --git a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py index ec7387dabb0c2..f044bcae41892 100644 --- a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py +++ b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py @@ -16,6 +16,7 @@ def test_stopped_description(self): """ program = self.getBuildArtifact("a.out") self.build_and_launch(program) + self.do_continue() self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) exceptionInfo = self.get_exceptionInfo() diff --git a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py index 7c85f05c1ba45..0063954791fd5 100644 --- a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py +++ b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py @@ -75,9 +75,7 @@ def test_termination(self): self.dap_server.request_disconnect() # Wait until the underlying lldb-dap process dies. - self.dap_server.process.wait( - timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval - ) + self.dap_server.process.wait(timeout=self.DEFAULT_TIMEOUT) # Check the return code self.assertEqual(self.dap_server.process.poll(), 0) @@ -90,15 +88,16 @@ def test_stopOnEntry(self): program = self.getBuildArtifact("a.out") self.build_and_launch(program, stopOnEntry=True) - stopped_events = self.dap_server.wait_for_stopped() - for stopped_event in stopped_events: - if "body" in stopped_event: - body = stopped_event["body"] - if "reason" in body: - reason = body["reason"] - self.assertNotEqual( - reason, "breakpoint", 'verify stop isn\'t "main" breakpoint' - ) + self.assertTrue( + len(self.dap_server.thread_stop_reasons) > 0, + "expected stopped event during launch", + ) + for _, body in self.dap_server.thread_stop_reasons.items(): + if "reason" in body: + reason = body["reason"] + self.assertNotEqual( + reason, "breakpoint", 'verify stop isn\'t "main" breakpoint' + ) @skipIfWindows def test_cwd(self): @@ -393,14 +392,14 @@ def test_commands(self): # Get output from the console. This should contain both the # "stopCommands" that were run after the first breakpoint was hit self.continue_to_breakpoints(breakpoint_ids) - output = self.get_console(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output = self.get_console(timeout=self.DEFAULT_TIMEOUT) self.verify_commands("stopCommands", output, stopCommands) # Continue again and hit the second breakpoint. # Get output from the console. This should contain both the # "stopCommands" that were run after the second breakpoint was hit self.continue_to_breakpoints(breakpoint_ids) - output = self.get_console(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output = self.get_console(timeout=self.DEFAULT_TIMEOUT) self.verify_commands("stopCommands", output, stopCommands) # Continue until the program exits @@ -462,21 +461,21 @@ def test_extra_launch_commands(self): self.verify_commands("launchCommands", output, launchCommands) # Verify the "stopCommands" here self.continue_to_next_stop() - output = self.get_console(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output = self.get_console(timeout=self.DEFAULT_TIMEOUT) self.verify_commands("stopCommands", output, stopCommands) # Continue and hit the second breakpoint. # Get output from the console. This should contain both the # "stopCommands" that were run after the first breakpoint was hit self.continue_to_next_stop() - output = self.get_console(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output = self.get_console(timeout=self.DEFAULT_TIMEOUT) 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 - output = self.get_console(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output = self.get_console(timeout=self.DEFAULT_TIMEOUT) self.verify_commands("exitCommands", output, exitCommands) def test_failing_launch_commands(self): @@ -531,7 +530,7 @@ def test_terminate_commands(self): terminateCommands = ["expr 4+2"] self.launch( - program=program, + program, stopOnEntry=True, terminateCommands=terminateCommands, disconnectAutomatically=False, diff --git a/lldb/test/API/tools/lldb-dap/module/TestDAP_module.py b/lldb/test/API/tools/lldb-dap/module/TestDAP_module.py index 3fc0f752ee39e..b333efd7bfb1f 100644 --- a/lldb/test/API/tools/lldb-dap/module/TestDAP_module.py +++ b/lldb/test/API/tools/lldb-dap/module/TestDAP_module.py @@ -14,7 +14,7 @@ class TestDAP_module(lldbdap_testcase.DAPTestCaseBase): def run_test(self, symbol_basename, expect_debug_info_size): program_basename = "a.out.stripped" program = self.getBuildArtifact(program_basename) - self.build_and_launch(program) + self.build_and_launch(program, stopOnEntry=True) functions = ["foo"] breakpoint_ids = self.set_function_breakpoints(functions) self.assertEqual(len(breakpoint_ids), len(functions), "expect one breakpoint") @@ -108,7 +108,7 @@ def test_modules_dsym(self): @skipIfWindows def test_compile_units(self): program = self.getBuildArtifact("a.out") - self.build_and_launch(program) + self.build_and_launch(program, stopOnEntry=True) source = "main.cpp" main_source_path = self.getSourcePath(source) breakpoint1_line = line_number(source, "// breakpoint 1") diff --git a/lldb/test/API/tools/lldb-dap/output/TestDAP_output.py b/lldb/test/API/tools/lldb-dap/output/TestDAP_output.py index 49131ad9ecb17..0425b55a5e552 100644 --- a/lldb/test/API/tools/lldb-dap/output/TestDAP_output.py +++ b/lldb/test/API/tools/lldb-dap/output/TestDAP_output.py @@ -37,14 +37,14 @@ def test_output(self): # Disconnecting from the server to ensure any pending IO is flushed. self.dap_server.request_disconnect() - output += self.get_stdout(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval) + output += self.get_stdout(timeout=self.DEFAULT_TIMEOUT) self.assertTrue(output and len(output) > 0, "expect program stdout") self.assertIn( "abcdefghi\r\nhello world\r\nfinally\0\0", output, "full stdout not found in: " + repr(output), ) - console = self.get_console(timeout=self.timeoutval) + console = self.get_console(timeout=self.DEFAULT_TIMEOUT) self.assertTrue(console and len(console) > 0, "expect dap messages") self.assertIn( "out\0\0\r\nerr\0\0\r\n", console, f"full console message not found" diff --git a/lldb/test/API/tools/lldb-dap/restart/TestDAP_restart.py b/lldb/test/API/tools/lldb-dap/restart/TestDAP_restart.py index 5f95c7bfb1556..8681b31e8eb1b 100644 --- a/lldb/test/API/tools/lldb-dap/restart/TestDAP_restart.py +++ b/lldb/test/API/tools/lldb-dap/restart/TestDAP_restart.py @@ -22,9 +22,8 @@ def test_basic_functionality(self): [bp_A, bp_B] = self.set_source_breakpoints("main.c", [line_A, line_B]) # Verify we hit A, then B. - self.verify_breakpoint_hit([bp_A]) - self.dap_server.request_continue() - self.verify_breakpoint_hit([bp_B]) + self.continue_to_breakpoints([bp_A]) + self.continue_to_breakpoints([bp_B]) # Make sure i has been modified from its initial value of 0. self.assertEqual( @@ -34,8 +33,9 @@ def test_basic_functionality(self): ) # Restart then check we stop back at A and program state has been reset. - self.dap_server.request_restart() - self.verify_breakpoint_hit([bp_A]) + resp = self.dap_server.request_restart() + self.assertTrue(resp["success"]) + self.continue_to_breakpoints([bp_A]) self.assertEqual( int(self.dap_server.get_local_variable_value("i")), 0, @@ -50,27 +50,26 @@ def test_stopOnEntry(self): program = self.getBuildArtifact("a.out") self.build_and_launch(program, stopOnEntry=True) [bp_main] = self.set_function_breakpoints(["main"]) - self.dap_server.request_configurationDone() - # Once the "configuration done" event is sent, we should get a stopped # event immediately because of stopOnEntry. - stopped_events = self.dap_server.wait_for_stopped() - for stopped_event in stopped_events: - if "body" in stopped_event: - body = stopped_event["body"] - if "reason" in body: - reason = body["reason"] - self.assertNotEqual( - reason, "breakpoint", 'verify stop isn\'t "main" breakpoint' - ) + self.assertTrue( + len(self.dap_server.thread_stop_reasons) > 0, + "expected stopped event during launch", + ) + for _, body in self.dap_server.thread_stop_reasons.items(): + if "reason" in body: + reason = body["reason"] + self.assertNotEqual( + reason, "breakpoint", 'verify stop isn\'t "main" breakpoint' + ) # Then, if we continue, we should hit the breakpoint at main. - self.dap_server.request_continue() - self.verify_breakpoint_hit([bp_main]) + self.continue_to_breakpoints([bp_main]) # Restart and check that we still get a stopped event before reaching # main. - self.dap_server.request_restart() + resp = self.dap_server.request_restart() + self.assertTrue(resp["success"]) stopped_events = self.dap_server.wait_for_stopped() for stopped_event in stopped_events: if "body" in stopped_event: @@ -96,8 +95,7 @@ def test_arguments(self): [bp_A] = self.set_source_breakpoints("main.c", [line_A]) # Verify we hit A, then B. - self.dap_server.request_configurationDone() - self.verify_breakpoint_hit([bp_A]) + self.continue_to_breakpoints([bp_A]) # We don't set any arguments in the initial launch request, so argc # should be 1. @@ -109,7 +107,7 @@ def test_arguments(self): # Restart with some extra 'args' and check that the new argc reflects # the updated launch config. - self.dap_server.request_restart( + resp = self.dap_server.request_restart( restartArguments={ "arguments": { "program": program, @@ -117,6 +115,7 @@ def test_arguments(self): } } ) + self.assertTrue(resp["success"]) self.verify_breakpoint_hit([bp_A]) self.assertEqual( int(self.dap_server.get_local_variable_value("argc")), diff --git a/lldb/test/API/tools/lldb-dap/stop-hooks/TestDAP_stop_hooks.py b/lldb/test/API/tools/lldb-dap/stop-hooks/TestDAP_stop_hooks.py index 7e28a5af4331c..33e038408fa34 100644 --- a/lldb/test/API/tools/lldb-dap/stop-hooks/TestDAP_stop_hooks.py +++ b/lldb/test/API/tools/lldb-dap/stop-hooks/TestDAP_stop_hooks.py @@ -2,7 +2,6 @@ Test stop hooks """ - from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * import lldbdap_testcase @@ -17,10 +16,6 @@ def test_stop_hooks_before_run(self): program = self.getBuildArtifact("a.out") preRunCommands = ["target stop-hook add -o help"] self.build_and_launch(program, stopOnEntry=True, preRunCommands=preRunCommands) - - # The first stop is on entry. - self.dap_server.wait_for_stopped() - breakpoint_ids = self.set_function_breakpoints(["main"]) # This request hangs if the race happens, because, in that case, the # command interpreter is in synchronous mode while lldb-dap expects diff --git a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py index 296e4911f4052..340be0b39010d 100644 --- a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py +++ b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py @@ -116,7 +116,7 @@ def darwin_dwarf_missing_obj(self, initCommands): self.create_debug_adapter() self.assertTrue(os.path.exists(program), "executable must exist") - self.launch(program=program, initCommands=initCommands) + self.launch(program, initCommands=initCommands) functions = ["main"] breakpoint_ids = self.set_function_breakpoints(functions) diff --git a/lldb/tools/lldb-dap/DAPError.cpp b/lldb/tools/lldb-dap/DAPError.cpp index dcb955af0345f..60347d577f821 100644 --- a/lldb/tools/lldb-dap/DAPError.cpp +++ b/lldb/tools/lldb-dap/DAPError.cpp @@ -8,6 +8,7 @@ #include "DAPError.h" #include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" #include namespace lldb_dap { @@ -26,4 +27,12 @@ std::error_code DAPError::convertToErrorCode() const { return llvm::inconvertibleErrorCode(); } +char NotStoppedError::ID; + +void NotStoppedError::log(llvm::raw_ostream &OS) const { OS << "not stopped"; } + +std::error_code NotStoppedError::convertToErrorCode() const { + return llvm::inconvertibleErrorCode(); +} + } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/DAPError.h b/lldb/tools/lldb-dap/DAPError.h index 564651b1f587d..4c94bdd6ac3d6 100644 --- a/lldb/tools/lldb-dap/DAPError.h +++ b/lldb/tools/lldb-dap/DAPError.h @@ -13,7 +13,7 @@ namespace lldb_dap { -/// An Error that is reported as a DAP Error Message, which may be presented to +/// An error that is reported as a DAP Error Message, which may be presented to /// the user. class DAPError : public llvm::ErrorInfo { public: @@ -40,4 +40,13 @@ class DAPError : public llvm::ErrorInfo { std::optional m_url_label; }; +/// An error that indicates the current request handler cannot execute because +/// the process is not stopped. +class NotStoppedError : public llvm::ErrorInfo { +public: + static char ID; + void log(llvm::raw_ostream &OS) const override; + std::error_code convertToErrorCode() const override; +}; + } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp index ca4c9141eca38..361c86421cf1b 100644 --- a/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp @@ -31,6 +31,9 @@ ContinueRequestHandler::Run(const ContinueArguments &args) const { SBProcess process = dap.target.GetProcess(); SBError error; + if (!SBDebugger::StateIsStoppedState(process.GetState())) + return make_error(); + if (args.singleThread) dap.GetLLDBThread(args.threadId).Resume(error); else @@ -40,7 +43,7 @@ ContinueRequestHandler::Run(const ContinueArguments &args) const { return ToError(error); ContinueResponseBody body; - body.allThreadsContinued = args.singleThread; + body.allThreadsContinued = !args.singleThread; return body; } diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.h b/lldb/tools/lldb-dap/Handler/RequestHandler.h index b0002440cf72e..5a1c82305eb7a 100644 --- a/lldb/tools/lldb-dap/Handler/RequestHandler.h +++ b/lldb/tools/lldb-dap/Handler/RequestHandler.h @@ -106,11 +106,13 @@ class RequestHandler : public BaseRequestHandler { DAP_LOG(dap.log, "({0}) malformed request {1}, expected arguments but got none", dap.transport.GetClientName(), request.command); - response.success = false; - response.message = llvm::formatv("arguments required for command '{0}' " - "but none received", - request.command) - .str(); + HandleErrorResponse( + llvm::make_error( + llvm::formatv("arguments required for command '{0}' " + "but none received", + request.command) + .str()), + response); dap.Send(response); return; } @@ -123,26 +125,21 @@ class RequestHandler : public BaseRequestHandler { OS << "invalid arguments for request '" << request.command << "': " << llvm::toString(root.getError()) << "\n"; root.printErrorContext(*request.arguments, OS); - - response.success = false; - response.body = ToResponse(llvm::make_error(parse_failure)); - + HandleErrorResponse(llvm::make_error(parse_failure), response); dap.Send(response); return; } if constexpr (std::is_same_v) { if (llvm::Error err = Run(arguments)) { - response.success = false; - response.body = ToResponse(std::move(err)); + HandleErrorResponse(std::move(err), response); } else { response.success = true; } } else { Resp body = Run(arguments); if (llvm::Error err = body.takeError()) { - response.success = false; - response.body = ToResponse(std::move(err)); + HandleErrorResponse(std::move(err), response); } else { response.success = true; response.body = std::move(*body); @@ -172,26 +169,36 @@ class RequestHandler : public BaseRequestHandler { /// error. virtual void PostRun() const {}; - protocol::ErrorResponseBody ToResponse(llvm::Error err) const { - protocol::ErrorMessage error_message; - // Default to showing the user errors unless otherwise specified by a - // DAPError. - error_message.showUser = true; - error_message.sendTelemetry = false; - if (llvm::Error unhandled = llvm::handleErrors( - std::move(err), [&](const DAPError &E) -> llvm::Error { - error_message.format = E.getMessage(); - error_message.showUser = E.getShowUser(); - error_message.id = E.convertToErrorCode().value(); - error_message.url = E.getURL(); - error_message.urlLabel = E.getURLLabel(); - return llvm::Error::success(); - })) { - error_message.format = llvm::toString(std::move(unhandled)); - } - protocol::ErrorResponseBody body; - body.error = error_message; - return body; + void HandleErrorResponse(llvm::Error err, + protocol::Response &response) const { + response.success = false; + llvm::handleAllErrors( + std::move(err), + [&](const NotStoppedError &err) { + response.message = lldb_dap::protocol::eResponseMessageNotStopped; + }, + [&](const DAPError &err) { + protocol::ErrorMessage error_message; + error_message.sendTelemetry = false; + error_message.format = err.getMessage(); + error_message.showUser = err.getShowUser(); + error_message.id = err.convertToErrorCode().value(); + error_message.url = err.getURL(); + error_message.urlLabel = err.getURLLabel(); + protocol::ErrorResponseBody body; + body.error = error_message; + response.body = body; + }, + [&](const llvm::ErrorInfoBase &err) { + protocol::ErrorMessage error_message; + error_message.showUser = true; + error_message.sendTelemetry = false; + error_message.format = err.message(); + error_message.id = err.convertToErrorCode().value(); + protocol::ErrorResponseBody body; + body.error = error_message; + response.body = body; + }); } };