blob: 02b93091aaa555fd7cb3386ab64669592df7f0ed [file]
"""
Test lldb-dap attach request
"""
import subprocess
import uuid
from pathlib import Path
from lldbsuite.test import lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase
from lldbsuite.test.tools.lldb_dap.types import (
AttachArgs,
CancelArgs,
ProcessEvent,
ProgressStartEvent,
)
# Often fails on Arm/AArch64 Linux, but not specifically because of the
# architecture, something in process scheduling can cause a massive (minutes)
# delay during this test.
# https://github.com/llvm/llvm-project/issues/137660
@skipIf(oslist=["linux"], archs=["arm$", "aarch64"])
@requireNotWasm("no attach support")
class TestDAP_attach(DAPTestCaseBase):
SHARED_BUILD_TESTCASE = False
def spawn(self, program: str, *, wait_for_sync: bool = True):
"""Spawn the target and (by default) block until it has called `lldb_enable_attach`."""
sync_token = lldbutil.append_to_process_working_directory(
self, f"sync_{uuid.uuid4().hex}"
)
proc = self.spawnSubprocess(
executable=program,
args=[sync_token],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
if wait_for_sync:
lldbutil.wait_for_file_on_target(self, sync_token)
self.subprocesses.append(proc)
return proc
def verify_pid(self, proc):
out, _ = proc.communicate("f")
self.assertIn(f"char = f", out)
self.assertIn(f"pid = {proc.pid}", out)
def test_by_pid(self):
"""Tests attaching to a process by process ID."""
program = self.build_for_attach()
session = self.create_session()
proc = self.spawn(program=program)
self.assertIsNone(proc.poll(), "process should be running")
process_event = session.attach(AttachArgs(pid=proc.pid))
self.assertEqual(process_event.body.systemProcessId, proc.pid)
self.verify_pid(proc)
def test_by_name(self):
"""Tests attaching to a process by process name."""
program = self.build_for_attach()
session = self.create_session()
proc = self.spawn(program=program)
process_event = session.attach(AttachArgs(program=program))
self.assertEqual(process_event.body.systemProcessId, proc.pid)
self.verify_pid(proc)
@expectedFailureWindowsAndNoLLDBServer
def test_by_name_waitFor(self):
"""
Tests waiting for, and attaching to a process by process name that
doesn't exist yet.
"""
self.do_attach_waitFor(use_basename=False)
@expectedFailureWindows
@skipIfWindowsAndLLDBServer
def test_by_basename_waitFor(self):
"""
Tests waiting for and attaching to a process by the process base name
that doesn't exist yet.
"""
self.do_attach_waitFor(use_basename=True)
def do_attach_waitFor(self, use_basename: bool):
"""Kick off attach with waitFor=True; spawn the target once lldb-dap
signals it has entered the wait-for-process polling loop."""
session = self.create_session()
program = self.build_for_attach()
attach_name = Path(program).name if use_basename else program
init_response = session.initialize_sequence(session.initialize_args)
pending = session.send_request(AttachArgs(program=attach_name, waitFor=True))
# Wait until lldb-dap is actually polling for the process before we
# spawn it, so we don't race the poll setup.
session.wait_for_event(
ProgressStartEvent,
until=lambda e: "Waiting to attach" in e.body.title,
after=init_response,
timeout_msg="Waiting for attach progress event.",
)
proc = self.spawn(program=program, wait_for_sync=False)
session.ensure_initialized()
session.verify_configuration_done()
process_event = session.wait_for_event(ProcessEvent, after=init_response)
pending.result("expects attach response.")
self.assertEqual(process_event.body.systemProcessId, proc.pid)
self.verify_pid(proc)
def waitFor_attach_request(self, session):
"""Sends a waitFor attach and returns it once it is waiting."""
program = self.build_for_attach()
init_response = session.initialize_sequence(session.initialize_args)
pending = session.send_request(AttachArgs(program=program, waitFor=True))
session.wait_for_event(
ProgressStartEvent,
until=lambda e: "Waiting to attach" in e.body.title,
after=init_response,
timeout_msg="Waiting for attach progress event.",
)
return pending
@expectedFailureWindows
@skipIfWindowsAndLLDBServer
def test_waitFor_cancel(self):
"""
Tests that a cancel request ends an attach that is still waiting for
the process to launch.
"""
session = self.create_session(disconnect_automatically=False)
pending = self.waitFor_attach_request(session)
session.send_request(CancelArgs(requestId=pending.seq)).result()
# `initialized` is only sent once the attach request stopped waiting.
session.ensure_initialized()
session.disconnect()
pending.error("expects attach response.")
@expectedFailureWindows
@skipIfWindowsAndLLDBServer
def test_waitFor_disconnect(self):
"""
Tests that a disconnect request alone ends an attach that is still
waiting for the process to launch.
"""
session = self.create_session(disconnect_automatically=False)
pending = self.waitFor_attach_request(session)
session.disconnect()
pending.error("expects attach response.")
def test_attach_with_missing_session_debugger(self):
"""
Test that attaching with only one of debuggerId/targetId specified
fails with the expected error message.
"""
session = self.create_session()
# Test with only targetId specified (no debuggerId)
resp = session.initialize_and_launch(
AttachArgs(session=AttachArgs.Session(targetId=99999))
).error()
message = self.expect_not_none(resp.body and resp.body.error)
self.assertIn(
"missing value at arguments.session.debuggerId",
message.format,
)
def test_attach_with_invalid_session(self):
"""
Test that attaching with both debuggerId and targetId specified but
invalid fails with an appropriate error message.
"""
session = self.create_session()
# Attach with both debuggerId=9999 and targetId=9999 (both invalid).
# Since debugger ID 9999 likely doesn't exist in the global registry,
# we expect a validation error.
pending = session.initialize_and_launch(
AttachArgs(session=AttachArgs.Session(targetId=9999, debuggerId=9999))
)
session.configuration_done().result_or_error()
resp = pending.error()
message = self.expect_not_none(resp.body and resp.body.error)
error_msg = message.format
# Either error is acceptable - both indicate the debugger reuse
# validation is working correctly
self.assertTrue(
"Unable to find existing debugger" in error_msg
or f"Expected debugger/target not found error, got: {error_msg}"
)