This patch extends ScriptedFrame to work with real (non-scripted) threads, enabling frame providers to synthesize frames for native processes. Previously, ScriptedFrame only worked within ScriptedProcess/ScriptedThread contexts. This patch decouples ScriptedFrame from ScriptedThread, allowing users to augment or replace stack frames in real debugging sessions for use cases like custom calling conventions, reconstructing corrupted frames from core files, or adding diagnostic frames. Key changes: - ScriptedFrame::Create() now accepts ThreadSP instead of requiring ScriptedThread, extracting architecture from the target triple rather than ScriptedProcess.arch - Added SBTarget::RegisterScriptedFrameProvider() and ClearScriptedFrameProvider() APIs, with Target storing a SyntheticFrameProviderDescriptor template for new threads - Added "target frame-provider register/clear" commands for CLI access - Thread class gains LoadScriptedFrameProvider(), ClearScriptedFrameProvider(), and GetFrameProvider() methods for per-thread frame provider management - New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily provide frames from either the frame provider or the real stack This enables practical use of the SyntheticFrameProvider infrastructure in real debugging workflows. rdar://161834688 Signed-off-by: Med Ismail Bennani <ismail@bennani.ma> Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
340 lines
13 KiB
Python
340 lines
13 KiB
Python
"""
|
|
Test scripted frame provider functionality.
|
|
"""
|
|
|
|
import os
|
|
|
|
import lldb
|
|
from lldbsuite.test.lldbtest import TestBase
|
|
from lldbsuite.test import lldbutil
|
|
|
|
|
|
class ScriptedFrameProviderTestCase(TestBase):
|
|
NO_DEBUG_INFO_TESTCASE = True
|
|
|
|
def setUp(self):
|
|
TestBase.setUp(self)
|
|
self.source = "main.cpp"
|
|
|
|
def test_replace_all_frames(self):
|
|
"""Test that we can replace the entire stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the test frame provider
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Attach the Replace provider
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ReplaceFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have exactly 3 synthetic frames
|
|
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
|
|
|
|
# Verify frame indices and PCs (dictionary-based frames don't have custom function names)
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(frame0.GetPC(), 0x1000)
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertIn("thread_func", frame1.GetFunctionName())
|
|
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIsNotNone(frame2)
|
|
self.assertEqual(frame2.GetPC(), 0x3000)
|
|
|
|
def test_prepend_frames(self):
|
|
"""Test that we can add frames before real stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count and PC
|
|
original_frame_count = thread.GetNumFrames()
|
|
self.assertGreaterEqual(
|
|
original_frame_count, 2, "Should have at least 2 real frames"
|
|
)
|
|
|
|
# Import and attach Prepend provider
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.PrependFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 2 more frames
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(new_frame_count, original_frame_count + 2)
|
|
|
|
# Verify first 2 frames are synthetic (check PCs, not function names)
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertEqual(frame0.GetPC(), 0x9000)
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertEqual(frame1.GetPC(), 0xA000)
|
|
|
|
# Verify frame 2 is the original real frame 0
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIn("thread_func", frame2.GetFunctionName())
|
|
|
|
def test_append_frames(self):
|
|
"""Test that we can add frames after real stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count
|
|
original_frame_count = thread.GetNumFrames()
|
|
|
|
# Import and attach Append provider
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.AppendFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 1 more frame
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(new_frame_count, original_frame_count + 1)
|
|
|
|
# Verify first frames are still real
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIn("thread_func", frame0.GetFunctionName())
|
|
|
|
frame_n_plus_1 = thread.GetFrameAtIndex(new_frame_count - 1)
|
|
self.assertEqual(frame_n_plus_1.GetPC(), 0x10)
|
|
|
|
def test_scripted_frame_objects(self):
|
|
"""Test that provider can return ScriptedFrame objects."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the provider that returns ScriptedFrame objects
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ScriptedFrameObjectProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 5 frames
|
|
self.assertEqual(
|
|
thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
|
|
)
|
|
|
|
# Verify frame properties from CustomScriptedFrame
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
|
|
self.assertEqual(frame0.GetPC(), 0x5000)
|
|
self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertEqual(frame1.GetPC(), 0x6000)
|
|
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIsNotNone(frame2)
|
|
self.assertEqual(frame2.GetFunctionName(), "custom_scripted_frame_2")
|
|
self.assertEqual(frame2.GetPC(), 0x7000)
|
|
self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
def test_applies_to_thread(self):
|
|
"""Test that applies_to_thread filters which threads get the provider."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# We should have at least 2 threads (worker threads) at the breakpoint
|
|
num_threads = process.GetNumThreads()
|
|
self.assertGreaterEqual(
|
|
num_threads, 2, "Should have at least 2 threads at breakpoint"
|
|
)
|
|
|
|
# Import the test frame provider
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Collect original thread info before applying provider
|
|
thread_info = {}
|
|
for i in range(num_threads):
|
|
t = process.GetThreadAtIndex(i)
|
|
thread_info[t.GetIndexID()] = {
|
|
"frame_count": t.GetNumFrames(),
|
|
"pc": t.GetFrameAtIndex(0).GetPC(),
|
|
}
|
|
|
|
# Register the ThreadFilterFrameProvider which only applies to thread ID 1
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ThreadFilterFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Check each thread
|
|
thread_id_1_found = False
|
|
for i in range(num_threads):
|
|
t = process.GetThreadAtIndex(i)
|
|
thread_id = t.GetIndexID()
|
|
|
|
if thread_id == 1:
|
|
# Thread with ID 1 should have synthetic frame
|
|
thread_id_1_found = True
|
|
self.assertEqual(
|
|
t.GetNumFrames(),
|
|
1,
|
|
f"Thread with ID 1 should have 1 synthetic frame",
|
|
)
|
|
self.assertEqual(
|
|
t.GetFrameAtIndex(0).GetPC(),
|
|
0xFFFF,
|
|
f"Thread with ID 1 should have synthetic PC 0xFFFF",
|
|
)
|
|
else:
|
|
# Other threads should keep their original frames
|
|
self.assertEqual(
|
|
t.GetNumFrames(),
|
|
thread_info[thread_id]["frame_count"],
|
|
f"Thread with ID {thread_id} should not be affected by provider",
|
|
)
|
|
self.assertEqual(
|
|
t.GetFrameAtIndex(0).GetPC(),
|
|
thread_info[thread_id]["pc"],
|
|
f"Thread with ID {thread_id} should have its original PC",
|
|
)
|
|
|
|
# We should have found at least one thread with ID 1
|
|
self.assertTrue(
|
|
thread_id_1_found,
|
|
"Should have found a thread with ID 1 to test filtering",
|
|
)
|
|
|
|
def test_remove_frame_provider_by_id(self):
|
|
"""Test that RemoveScriptedFrameProvider removes a specific provider by ID."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the test frame providers
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Get original frame count
|
|
original_frame_count = thread.GetNumFrames()
|
|
original_pc = thread.GetFrameAtIndex(0).GetPC()
|
|
|
|
# Register the first provider and get its ID
|
|
error = lldb.SBError()
|
|
provider_id_1 = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ReplaceFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")
|
|
|
|
# Verify first provider is active (3 synthetic frames)
|
|
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
|
|
)
|
|
|
|
# Register a second provider and get its ID
|
|
provider_id_2 = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.PrependFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider 2: {error}")
|
|
|
|
# Verify IDs are different
|
|
self.assertNotEqual(
|
|
provider_id_1, provider_id_2, "Provider IDs should be unique"
|
|
)
|
|
|
|
# Now remove the first provider by ID
|
|
result = target.RemoveScriptedFrameProvider(provider_id_1)
|
|
self.assertSuccess(
|
|
result, f"Should successfully remove provider with ID {provider_id_1}"
|
|
)
|
|
|
|
# After removing the first provider, the second provider should still be active
|
|
# The PrependFrameProvider adds 2 frames before the real stack
|
|
# Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
|
|
# have the original frames (from real stack) with PrependFrameProvider applied
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(
|
|
new_frame_count,
|
|
original_frame_count + 2,
|
|
"Should have original frames + 2 prepended frames",
|
|
)
|
|
|
|
# First two frames should be from PrependFrameProvider
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(),
|
|
0x9000,
|
|
"First frame should be from PrependFrameProvider",
|
|
)
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(1).GetPC(),
|
|
0xA000,
|
|
"Second frame should be from PrependFrameProvider",
|
|
)
|
|
|
|
# Remove the second provider
|
|
result = target.RemoveScriptedFrameProvider(provider_id_2)
|
|
self.assertSuccess(
|
|
result, f"Should successfully remove provider with ID {provider_id_2}"
|
|
)
|
|
|
|
# After removing both providers, frames should be back to original
|
|
self.assertEqual(
|
|
thread.GetNumFrames(),
|
|
original_frame_count,
|
|
"Should restore original frame count",
|
|
)
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(),
|
|
original_pc,
|
|
"Should restore original PC",
|
|
)
|
|
|
|
# Try to remove a provider that doesn't exist
|
|
result = target.RemoveScriptedFrameProvider(999999)
|
|
self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")
|