[Lldb-commits] [lldb] Add DAP tests for initialized event to be sure stats are present (PR #134266)
via lldb-commits
lldb-commits at lists.llvm.org
Thu Apr 3 09:00:05 PDT 2025
https://github.com/youngd007 created https://github.com/llvm/llvm-project/pull/134266
As the DAP JSON was recently modified to move the statistics from one key to another, it was revealed there was no test for the initialized event that also emits this information.
https://github.com/llvm/llvm-project/pull/130454/files
So adding a test to make sure the keys stay in sync between DAP initialized and terminated.
>From 80fafd267a19e4e0dd4f35e3807ae4a69b47c86e Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Thu, 3 Apr 2025 08:55:10 -0700
Subject: [PATCH] Add DAP tests for initialized event to be sure stats are
present
---
.../test/tools/lldb-dap/dap_server.py | 130 +++++++++++++-----
.../API/tools/lldb-dap/initialized/Makefile | 17 +++
.../initialized/TestDAP_initializedEvent.py | 47 +++++++
.../API/tools/lldb-dap/initialized/foo.cpp | 1 +
.../test/API/tools/lldb-dap/initialized/foo.h | 1 +
.../API/tools/lldb-dap/initialized/main.cpp | 8 ++
6 files changed, 171 insertions(+), 33 deletions(-)
create mode 100644 lldb/test/API/tools/lldb-dap/initialized/Makefile
create mode 100644 lldb/test/API/tools/lldb-dap/initialized/TestDAP_initializedEvent.py
create mode 100644 lldb/test/API/tools/lldb-dap/initialized/foo.cpp
create mode 100644 lldb/test/API/tools/lldb-dap/initialized/foo.h
create mode 100644 lldb/test/API/tools/lldb-dap/initialized/main.cpp
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 45403e9df8525..3471770e807f0 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
@@ -88,13 +88,13 @@ def packet_type_is(packet, packet_type):
def dump_dap_log(log_file):
- print("========= DEBUG ADAPTER PROTOCOL LOGS =========", file=sys.stderr)
+ print("========= DEBUG ADAPTER PROTOCOL LOGS =========")
if log_file is None:
- print("no log file available", file=sys.stderr)
+ print("no log file available")
else:
with open(log_file, "r") as file:
- print(file.read(), file=sys.stderr)
- print("========= END =========", file=sys.stderr)
+ print(file.read())
+ print("========= END =========")
def read_packet_thread(vs_comm, log_file):
@@ -107,43 +107,46 @@ def read_packet_thread(vs_comm, log_file):
# 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):
+ def __init__(self, recv, send, init_commands, log_file=None, keepAlive=False):
self.trace_file = None
self.send = send
self.recv = recv
self.recv_packets = []
self.recv_condition = threading.Condition()
+ self.sequence = 1
self.recv_thread = threading.Thread(
target=read_packet_thread, args=(self, log_file)
)
+ self.recv_thread.start()
+ self.output_condition = threading.Condition()
+ self.reset(init_commands, keepAlive)
+
+ # This will be called to re-initialize DebugCommunication object during
+ # reusing lldb-dap.
+ def reset(self, init_commands, keepAlive=False):
+ self.trace_file = None
+ self.recv_packets = []
self.process_event_body = None
self.exit_status = None
self.initialize_body = None
self.thread_stop_reasons = {}
self.breakpoint_events = []
+ self.thread_events_body = []
self.progress_events = []
self.reverse_requests = []
- self.sequence = 1
+ self.startup_events = []
self.threads = None
- self.recv_thread.start()
- self.output_condition = threading.Condition()
self.output = {}
self.configuration_done_sent = False
self.frame_scopes = {}
self.init_commands = init_commands
self.disassembled_instructions = {}
+ self.initialized_event = None
+ self.keepAlive = keepAlive
@classmethod
def encode_content(cls, s):
@@ -243,6 +246,8 @@ def handle_recv_packet(self, packet):
self._process_stopped()
tid = body["threadId"]
self.thread_stop_reasons[tid] = body
+ elif event == "initialized":
+ self.initialized_event = packet
elif event == "breakpoint":
# Breakpoint events come in when a breakpoint has locations
# added or removed. Keep track of them so we can look for them
@@ -250,15 +255,23 @@ def handle_recv_packet(self, packet):
self.breakpoint_events.append(packet)
# no need to add 'breakpoint' event packets to our packets list
return keepGoing
+ elif event == "thread":
+ self.thread_events_body.append(body)
+ # no need to add 'thread' event packets to our packets list
+ return keepGoing
elif event.startswith("progress"):
# Progress events come in as 'progressStart', 'progressUpdate',
# and 'progressEnd' events. Keep these around in case test
# cases want to verify them.
self.progress_events.append(packet)
+ # No need to add 'progress' event packets to our packets list.
+ return keepGoing
elif packet_type == "response":
if packet["command"] == "disconnect":
- keepGoing = False
+ # Disconnect response should exit the packet read loop unless
+ # client wants to keep adapter alive for reusing.
+ keepGoing = self.keepAlive
self.enqueue_recv_packet(packet)
return keepGoing
@@ -424,6 +437,14 @@ def get_threads(self):
self.request_threads()
return self.threads
+ def get_thread_events(self, reason=None):
+ if reason == None:
+ return self.thread_events_body
+ else:
+ return [
+ body for body in self.thread_events_body if body["reason"] == reason
+ ]
+
def get_thread_id(self, threadIndex=0):
"""Utility function to get the first thread ID in the thread list.
If the thread list is empty, then fetch the threads.
@@ -582,6 +603,7 @@ def request_attach(
sourceMap=None,
gdbRemotePort=None,
gdbRemoteHostname=None,
+ vscode_session_id=None,
):
args_dict = {}
if pid is not None:
@@ -615,6 +637,9 @@ def request_attach(
args_dict["gdb-remote-port"] = gdbRemotePort
if gdbRemoteHostname is not None:
args_dict["gdb-remote-hostname"] = gdbRemoteHostname
+ if vscode_session_id:
+ args_dict["__sessionId"] = vscode_session_id
+
command_dict = {"command": "attach", "type": "request", "arguments": args_dict}
return self.send_recv(command_dict)
@@ -759,7 +784,7 @@ def request_exceptionInfo(self, threadId=None):
}
return self.send_recv(command_dict)
- def request_initialize(self, sourceInitFile):
+ def request_initialize(self, sourceInitFile, singleStoppedEvent=False):
command_dict = {
"command": "initialize",
"type": "request",
@@ -774,8 +799,8 @@ def request_initialize(self, sourceInitFile):
"supportsVariablePaging": True,
"supportsVariableType": True,
"supportsStartDebuggingRequest": True,
- "supportsProgressReporting": True,
- "$__lldb_sourceInitFile": sourceInitFile,
+ "sourceInitFile": sourceInitFile,
+ "singleStoppedEvent": singleStoppedEvent,
},
}
response = self.send_recv(command_dict)
@@ -812,6 +837,7 @@ def request_launch(
commandEscapePrefix=None,
customFrameFormat=None,
customThreadFormat=None,
+ vscode_session_id=None,
):
args_dict = {"program": program}
if args:
@@ -855,6 +881,8 @@ def request_launch(
args_dict["customFrameFormat"] = customFrameFormat
if customThreadFormat:
args_dict["customThreadFormat"] = customThreadFormat
+ if vscode_session_id:
+ args_dict["__sessionId"] = vscode_session_id
args_dict["disableASLR"] = disableASLR
args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries
@@ -866,8 +894,12 @@ def request_launch(
if response["success"]:
# Wait for a 'process' and 'initialized' event in any order
- self.wait_for_event(filter=["process", "initialized"])
- self.wait_for_event(filter=["process", "initialized"])
+ self.startup_events.append(
+ self.wait_for_event(filter=["process", "initialized"])
+ )
+ self.startup_events.append(
+ self.wait_for_event(filter=["process", "initialized"])
+ )
return response
def request_next(self, threadId, granularity="statement"):
@@ -1199,12 +1231,17 @@ def __init__(
init_commands=[],
log_file=None,
env=None,
+ keepAliveTimeout=None,
):
self.process = None
self.connection = None
if executable is not None:
process, connection = DebugAdapterServer.launch(
- executable=executable, connection=connection, env=env, log_file=log_file
+ executable=executable,
+ connection=connection,
+ env=env,
+ log_file=log_file,
+ keepAliveTimeout=keepAliveTimeout,
)
self.process = process
self.connection = connection
@@ -1226,18 +1263,39 @@ def __init__(
self.connection = connection
else:
DebugCommunication.__init__(
- self, self.process.stdout, self.process.stdin, init_commands, log_file
+ self,
+ self.process.stdout,
+ self.process.stdin,
+ init_commands,
+ log_file,
+ keepAlive=(keepAliveTimeout is not None),
)
+ @staticmethod
+ def get_args(executable, keepAliveTimeout=None):
+ return (
+ [executable]
+ if keepAliveTimeout is None
+ else [executable, "--keep-alive", str(keepAliveTimeout)]
+ )
+
@classmethod
- def launch(cls, /, executable, env=None, log_file=None, connection=None):
+ def launch(
+ cls,
+ /,
+ executable,
+ env=None,
+ log_file=None,
+ connection=None,
+ keepAliveTimeout=None,
+ ):
adapter_env = os.environ.copy()
if env is not None:
adapter_env.update(env)
if log_file:
adapter_env["LLDBDAP_LOG"] = log_file
- args = [executable]
+ args = cls.get_args(executable, keepAliveTimeout)
if connection is not None:
args.append("--connection")
@@ -1260,7 +1318,7 @@ def launch(cls, /, executable, env=None, log_file=None, connection=None):
expected_prefix = "Listening for: "
out = process.stdout.readline().decode()
if not out.startswith(expected_prefix):
- process.kill()
+ self.process.kill()
raise ValueError(
"lldb-dap failed to print listening address, expected '{}', got '{}'".format(
expected_prefix, out
@@ -1281,11 +1339,7 @@ def terminate(self):
super(DebugAdapterServer, self).terminate()
if self.process is not None:
self.process.terminate()
- try:
- self.process.wait(timeout=20)
- except subprocess.TimeoutExpired:
- self.process.kill()
- self.process.wait()
+ self.process.wait()
self.process = None
@@ -1587,6 +1641,14 @@ def main():
),
)
+ parser.add_option(
+ "--keep-alive",
+ type="int",
+ dest="keepAliveTimeout",
+ help="The number of milliseconds to keep lldb-dap alive after client disconnection for reusing. Zero or negative value will not keep lldb-dap alive.",
+ default=None,
+ )
+
(options, args) = parser.parse_args(sys.argv[1:])
if options.vscode_path is None and options.connection is None:
@@ -1597,7 +1659,9 @@ def main():
)
return
dbg = DebugAdapterServer(
- executable=options.vscode_path, connection=options.connection
+ executable=options.vscode_path,
+ connection=options.connection,
+ keepAliveTimeout=options.keepAliveTimeout,
)
if options.debug:
raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
diff --git a/lldb/test/API/tools/lldb-dap/initialized/Makefile b/lldb/test/API/tools/lldb-dap/initialized/Makefile
new file mode 100644
index 0000000000000..c7d626a1a7e4c
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/initialized/Makefile
@@ -0,0 +1,17 @@
+DYLIB_NAME := foo
+DYLIB_CXX_SOURCES := foo.cpp
+CXX_SOURCES := main.cpp
+
+LD_EXTRAS := -Wl,-rpath "-Wl,$(shell pwd)"
+USE_LIBDL :=1
+
+include Makefile.rules
+
+all: a.out.stripped
+
+a.out.stripped:
+ $(STRIP) -o a.out.stripped a.out
+
+ifneq "$(CODESIGN)" ""
+ $(CODESIGN) -fs - a.out.stripped
+endif
\ No newline at end of file
diff --git a/lldb/test/API/tools/lldb-dap/initialized/TestDAP_initializedEvent.py b/lldb/test/API/tools/lldb-dap/initialized/TestDAP_initializedEvent.py
new file mode 100644
index 0000000000000..3f0a4bc481072
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/initialized/TestDAP_initializedEvent.py
@@ -0,0 +1,47 @@
+"""
+Test lldb-dap terminated event
+"""
+
+import dap_server
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import json
+import re
+
+import lldbdap_testcase
+from lldbsuite.test import lldbutil
+
+
+class TestDAP_terminatedEvent(lldbdap_testcase.DAPTestCaseBase):
+ @skipIfWindows
+ def test_initialized_event(self):
+ """
+ Initialized Event
+ Now contains the statistics of a debug session:
+ memory:
+ strings
+ bytesTotal > 0
+ ...
+ targets:
+ list
+ totalSymbolTableParseTime int:
+ totalSymbolTablesLoadedFromCache int:
+ """
+
+ program_basename = "a.out.stripped"
+ program = self.getBuildArtifact(program_basename)
+ self.build_and_launch(program)
+
+ self.continue_to_next_stop()
+
+ initialized_event = next(
+ (x for x in self.dap_server.startup_events if x["event"] == "initialized"),
+ None,
+ )
+ self.assertIsNotNone(initialized_event)
+
+ statistics = initialized_event["body"]["$__lldb_statistics"]
+ self.assertGreater(statistics["memory"]["strings"]["bytesTotal"], 0)
+
+ self.assertIn("targets", statistics.keys())
+ self.assertIn("totalSymbolTableParseTime", statistics.keys())
diff --git a/lldb/test/API/tools/lldb-dap/initialized/foo.cpp b/lldb/test/API/tools/lldb-dap/initialized/foo.cpp
new file mode 100644
index 0000000000000..b6f33b8e070a4
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/initialized/foo.cpp
@@ -0,0 +1 @@
+int foo() { return 12; }
diff --git a/lldb/test/API/tools/lldb-dap/initialized/foo.h b/lldb/test/API/tools/lldb-dap/initialized/foo.h
new file mode 100644
index 0000000000000..5d5f8f0c9e786
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/initialized/foo.h
@@ -0,0 +1 @@
+int foo();
diff --git a/lldb/test/API/tools/lldb-dap/initialized/main.cpp b/lldb/test/API/tools/lldb-dap/initialized/main.cpp
new file mode 100644
index 0000000000000..50dd77c0a9c1d
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/initialized/main.cpp
@@ -0,0 +1,8 @@
+#include "foo.h"
+#include <iostream>
+
+int main(int argc, char const *argv[]) {
+ std::cout << "Hello World!" << std::endl; // main breakpoint 1
+ foo();
+ return 0;
+}
More information about the lldb-commits
mailing list