Files
llvm/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
Janet Yang 7546bd3804 [lldb] Add support for unique target ids (#160736)
### Summary
Add support for unique target ids per Target instance. This is needed
for upcoming changes to allow debugger instances to be shared across
separate DAP instances for child process debugging. We want the IDE to
be able to attach to existing targets in an already runny lldb-dap
session, and having a unique ID per target would make that easier.

Each Target instance will have its own unique id, and uses a
function-local counter in `TargetList::CreateTargetInternal` to assign
incremental unique ids.

### Tests
Added several unit tests to test basic functionality, uniqueness of
targets, and target deletion doesn't affect the uniqueness.
```
bin/lldb-dotest -p TestDebuggerAPI
```
2025-10-07 15:36:47 -04:00

444 lines
17 KiB
Python

"""
Test Debugger APIs.
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class DebuggerAPITestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def test_debugger_api_boundary_condition(self):
"""Exercise SBDebugger APIs with boundary conditions."""
self.dbg.HandleCommand(None)
self.dbg.SetDefaultArchitecture(None)
self.dbg.GetScriptingLanguage(None)
self.dbg.CreateTarget(None)
self.dbg.CreateTarget(None, None, None, True, lldb.SBError())
self.dbg.CreateTargetWithFileAndTargetTriple(None, None)
self.dbg.CreateTargetWithFileAndArch(None, None)
self.dbg.FindTargetWithFileAndArch(None, None)
self.dbg.SetInternalVariable(None, None, None)
self.dbg.GetInternalVariableValue(None, None)
# FIXME (filcab): We must first allow for the swig bindings to know if
# a Python callback is set. (Check python-typemaps.swig)
# self.dbg.SetLoggingCallback(None)
self.dbg.SetPrompt(None)
self.dbg.SetCurrentPlatform(None)
self.dbg.SetCurrentPlatformSDKRoot(None)
fresh_dbg = lldb.SBDebugger()
self.assertEqual(len(fresh_dbg), 0)
def test_debugger_delete_invalid_target(self):
"""SBDebugger.DeleteTarget() should not crash LLDB given and invalid target."""
target = lldb.SBTarget()
self.assertFalse(target.IsValid())
self.dbg.DeleteTarget(target)
def test_debugger_internal_variables(self):
"""Ensure that SBDebugger reachs the same instance of properties
regardless CommandInterpreter's context initialization"""
self.build()
exe = self.getBuildArtifact("a.out")
# Create a target by the debugger.
target = self.dbg.CreateTarget(exe)
self.assertTrue(target, VALID_TARGET)
property_name = "target.process.memory-cache-line-size"
def get_cache_line_size():
value_list = lldb.SBStringList()
value_list = self.dbg.GetInternalVariableValue(
property_name, self.dbg.GetInstanceName()
)
self.assertEqual(value_list.GetSize(), 1)
try:
return int(value_list.GetStringAtIndex(0))
except ValueError as error:
self.fail("Value is not a number: " + error)
# Get global property value while there are no processes.
global_cache_line_size = get_cache_line_size()
# Run a process via SB interface. CommandInterpreter's execution context
# remains empty.
error = lldb.SBError()
launch_info = lldb.SBLaunchInfo(None)
launch_info.SetLaunchFlags(lldb.eLaunchFlagStopAtEntry)
process = target.Launch(launch_info, error)
self.assertTrue(process, PROCESS_IS_VALID)
# This should change the value of a process's local property.
new_cache_line_size = global_cache_line_size + 512
error = self.dbg.SetInternalVariable(
property_name, str(new_cache_line_size), self.dbg.GetInstanceName()
)
self.assertSuccess(error, property_name + " value was changed successfully")
# Check that it was set actually.
self.assertEqual(get_cache_line_size(), new_cache_line_size)
# Run any command to initialize CommandInterpreter's execution context.
self.runCmd("target list")
# Test the local property again, is it set to new_cache_line_size?
self.assertEqual(get_cache_line_size(), new_cache_line_size)
@expectedFailureAll(
remote=True,
bugnumber="github.com/llvm/llvm-project/issues/92419",
)
def test_CreateTarget_platform(self):
exe = self.getBuildArtifact("a.out")
self.yaml2obj("elf.yaml", exe)
error = lldb.SBError()
target1 = self.dbg.CreateTarget(exe, None, "remote-linux", False, error)
self.assertSuccess(error)
platform1 = target1.GetPlatform()
platform1.SetWorkingDirectory("/foo/bar")
# Reuse a platform if it matches the currently selected one...
target2 = self.dbg.CreateTarget(exe, None, "remote-linux", False, error)
self.assertSuccess(error)
platform2 = target2.GetPlatform()
self.assertTrue(
platform2.GetWorkingDirectory().endswith("bar"),
platform2.GetWorkingDirectory(),
)
# ... but create a new one if it doesn't.
self.dbg.SetSelectedPlatform(lldb.SBPlatform("remote-windows"))
target3 = self.dbg.CreateTarget(exe, None, "remote-linux", False, error)
self.assertSuccess(error)
platform3 = target3.GetPlatform()
self.assertIsNone(platform3.GetWorkingDirectory())
def test_CreateTarget_arch(self):
exe = self.getBuildArtifact("a.out")
if lldbplatformutil.getHostPlatform() == "linux":
self.yaml2obj("macho.yaml", exe)
arch = "x86_64-apple-macosx"
expected_platform = "remote-macosx"
else:
self.yaml2obj("elf.yaml", exe)
arch = "x86_64-pc-linux"
expected_platform = "remote-linux"
fbsd = lldb.SBPlatform("remote-freebsd")
self.dbg.SetSelectedPlatform(fbsd)
error = lldb.SBError()
target1 = self.dbg.CreateTarget(exe, arch, None, False, error)
self.assertSuccess(error)
platform1 = target1.GetPlatform()
self.assertEqual(platform1.GetName(), expected_platform)
platform1.SetWorkingDirectory("/foo/bar")
# Reuse a platform even if it is not currently selected.
self.dbg.SetSelectedPlatform(fbsd)
target2 = self.dbg.CreateTarget(exe, arch, None, False, error)
self.assertSuccess(error)
platform2 = target2.GetPlatform()
self.assertEqual(platform2.GetName(), expected_platform)
self.assertTrue(
platform2.GetWorkingDirectory().endswith("bar"),
platform2.GetWorkingDirectory(),
)
def test_SetDestroyCallback(self):
destroy_dbg_id = None
def foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal destroy_dbg_id
destroy_dbg_id = dbg_id
self.dbg.SetDestroyCallback(foo)
original_dbg_id = self.dbg.GetID()
self.dbg.Destroy(self.dbg)
self.assertEqual(destroy_dbg_id, original_dbg_id)
def test_AddDestroyCallback(self):
original_dbg_id = self.dbg.GetID()
called = []
def foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal called
called += [('foo', dbg_id)]
def bar(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal called
called += [('bar', dbg_id)]
token_foo = self.dbg.AddDestroyCallback(foo)
token_bar = self.dbg.AddDestroyCallback(bar)
self.dbg.Destroy(self.dbg)
# Should call both `foo()` and `bar()`.
self.assertEqual(called, [
('foo', original_dbg_id),
('bar', original_dbg_id),
])
def test_RemoveDestroyCallback(self):
original_dbg_id = self.dbg.GetID()
called = []
def foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal called
called += [('foo', dbg_id)]
def bar(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal called
called += [('bar', dbg_id)]
token_foo = self.dbg.AddDestroyCallback(foo)
token_bar = self.dbg.AddDestroyCallback(bar)
ret = self.dbg.RemoveDestroyCallback(token_foo)
self.dbg.Destroy(self.dbg)
# `Remove` should be successful
self.assertTrue(ret)
# Should only call `bar()`
self.assertEqual(called, [('bar', original_dbg_id)])
def test_RemoveDestroyCallback_invalid_token(self):
original_dbg_id = self.dbg.GetID()
magic_token_that_should_not_exist = 32413
called = []
def foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal called
called += [('foo', dbg_id)]
token_foo = self.dbg.AddDestroyCallback(foo)
ret = self.dbg.RemoveDestroyCallback(magic_token_that_should_not_exist)
self.dbg.Destroy(self.dbg)
# `Remove` should be unsuccessful
self.assertFalse(ret)
# Should call `foo()`
self.assertEqual(called, [('foo', original_dbg_id)])
def test_HandleDestroyCallback(self):
"""
Validates:
1. AddDestroyCallback and RemoveDestroyCallback work during debugger destroy.
2. HandleDestroyCallback invokes all callbacks in FIFO order.
"""
original_dbg_id = self.dbg.GetID()
events = []
bar_token = None
def foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal events
events.append(('foo called', dbg_id))
def bar(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal events
events.append(('bar called', dbg_id))
def add_foo(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal events
events.append(('add_foo called', dbg_id))
events.append(('foo token', self.dbg.AddDestroyCallback(foo)))
def remove_bar(dbg_id):
# Need nonlocal to modify closure variable.
nonlocal events
events.append(('remove_bar called', dbg_id))
events.append(('remove bar ret', self.dbg.RemoveDestroyCallback(bar_token)))
# Setup
events.append(('add_foo token', self.dbg.AddDestroyCallback(add_foo)))
bar_token = self.dbg.AddDestroyCallback(bar)
events.append(('bar token', bar_token))
events.append(('remove_bar token', self.dbg.AddDestroyCallback(remove_bar)))
# Destroy
self.dbg.Destroy(self.dbg)
self.assertEqual(events, [
# Setup
('add_foo token', 0), # add_foo should be added
('bar token', 1), # bar should be added
('remove_bar token', 2), # remove_bar should be added
# Destroy
('add_foo called', original_dbg_id), # add_foo should be called
('foo token', 3), # foo should be added
('bar called', original_dbg_id), # bar should be called
('remove_bar called', original_dbg_id), # remove_bar should be called
('remove bar ret', False), # remove_bar should fail, because it's already invoked and removed
('foo called', original_dbg_id), # foo should be called
])
def test_version(self):
instance_str = self.dbg.GetVersionString()
class_str = lldb.SBDebugger.GetVersionString()
property_str = lldb.SBDebugger.version
self.assertEqual(instance_str, class_str)
self.assertEqual(class_str, property_str)
def test_find_target_with_unique_id(self):
"""Test SBDebugger.FindTargetByGloballyUniqueID() functionality."""
# Test with invalid ID - should return invalid target
invalid_target = self.dbg.FindTargetByGloballyUniqueID(999999)
self.assertFalse(invalid_target.IsValid())
# Test with ID 0 - should return invalid target
zero_target = self.dbg.FindTargetByGloballyUniqueID(0)
self.assertFalse(zero_target.IsValid())
# Build a real executable and create target with it
self.build()
exe = self.getBuildArtifact("a.out")
target = self.dbg.CreateTarget(exe)
self.assertTrue(target.IsValid())
# Find the target using its unique ID
unique_id = target.GetGloballyUniqueID()
self.assertNotEqual(unique_id, lldb.LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID)
found_target = self.dbg.FindTargetByGloballyUniqueID(unique_id)
self.assertTrue(found_target.IsValid())
self.assertEqual(
self.dbg.GetIndexOfTarget(target), self.dbg.GetIndexOfTarget(found_target)
)
self.assertEqual(found_target.GetGloballyUniqueID(), unique_id)
def test_target_unique_id_uniqueness(self):
"""Test that Target.GetGloballyUniqueID() returns unique values across multiple targets."""
# Create multiple targets and verify they all have unique IDs
self.build()
exe = self.getBuildArtifact("a.out")
targets = []
unique_ids = set()
for i in range(10):
target = self.dbg.CreateTarget(exe)
self.assertTrue(target.IsValid())
unique_id = target.GetGloballyUniqueID()
self.assertNotEqual(unique_id, 0)
# Verify this ID hasn't been used before
self.assertNotIn(
unique_id, unique_ids, f"Duplicate unique ID found: {unique_id}"
)
unique_ids.add(unique_id)
targets.append(target)
# Verify all targets can still be found by their IDs
for target in targets:
unique_id = target.GetGloballyUniqueID()
found = self.dbg.FindTargetByGloballyUniqueID(unique_id)
self.assertTrue(found.IsValid())
self.assertEqual(found.GetGloballyUniqueID(), unique_id)
def test_target_unique_id_uniqueness_after_deletion(self):
"""Test finding targets have unique ID after target deletion."""
# Create two targets
self.build()
exe = self.getBuildArtifact("a.out")
target1 = self.dbg.CreateTarget(exe)
target2 = self.dbg.CreateTarget(exe)
self.assertTrue(target1.IsValid())
self.assertTrue(target2.IsValid())
unique_id1 = target1.GetGloballyUniqueID()
unique_id2 = target2.GetGloballyUniqueID()
self.assertNotEqual(unique_id1, 0)
self.assertNotEqual(unique_id2, 0)
self.assertNotEqual(unique_id1, unique_id2)
# Verify we can find them initially
found_target1 = self.dbg.FindTargetByGloballyUniqueID(unique_id1)
found_target2 = self.dbg.FindTargetByGloballyUniqueID(unique_id2)
self.assertTrue(found_target1.IsValid())
self.assertTrue(found_target2.IsValid())
target2_index = self.dbg.GetIndexOfTarget(target2)
# Delete target 2
deleted = self.dbg.DeleteTarget(target2)
self.assertTrue(deleted)
# Try to find the deleted target - should not be found
not_found_target = self.dbg.FindTargetByGloballyUniqueID(unique_id2)
self.assertFalse(not_found_target.IsValid())
# Create a new target
target3 = self.dbg.CreateTarget(exe)
self.assertTrue(target3.IsValid())
# Target list index of target3 should be the same as target2's
# since it was deleted, but it should have a distinct unique ID
target3_index = self.dbg.GetIndexOfTarget(target3)
unique_id3 = target3.GetGloballyUniqueID()
self.assertEqual(target3_index, target2_index)
self.assertNotEqual(unique_id3, unique_id2)
self.assertNotEqual(unique_id3, unique_id1)
# Make sure we can find the new target
found_target3 = self.dbg.FindTargetByGloballyUniqueID(
target3.GetGloballyUniqueID()
)
self.assertTrue(found_target3.IsValid())
def test_target_globally_unique_id_across_debuggers(self):
"""Test that target IDs are globally unique across multiple debuggers."""
self.build()
exe = self.getBuildArtifact("a.out")
# Create two debuggers with targets each
debugger1 = lldb.SBDebugger.Create()
debugger2 = lldb.SBDebugger.Create()
self.addTearDownHook(lambda: lldb.SBDebugger.Destroy(debugger1))
self.addTearDownHook(lambda: lldb.SBDebugger.Destroy(debugger2))
# Create 2 targets per debugger
targets_d1 = [debugger1.CreateTarget(exe), debugger1.CreateTarget(exe)]
targets_d2 = [debugger2.CreateTarget(exe), debugger2.CreateTarget(exe)]
targets = targets_d1 + targets_d2
# Get all IDs and verify they're unique
ids = [target.GetGloballyUniqueID() for target in targets]
self.assertEqual(
len(set(ids)), len(ids), f"IDs should be globally unique: {ids}"
)
self.assertTrue(
all(uid != lldb.LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID for uid in ids),
"All IDs should be valid",
)
# Verify targets can be found by their IDs in respective debuggers
for debugger, target_pair in [
(debugger1, targets[:2]),
(debugger2, targets[2:]),
]:
for target in target_pair:
found = debugger.FindTargetByGloballyUniqueID(
target.GetGloballyUniqueID()
)
self.assertTrue(
found.IsValid(), "Target should be found by its unique ID"
)
self.assertEqual(
found.GetGloballyUniqueID(), target.GetGloballyUniqueID()
)