429 lines
17 KiB
Python
429 lines
17 KiB
Python
"""
|
|
Test scripted frame provider functionality.
|
|
"""
|
|
|
|
import os
|
|
|
|
import lldb
|
|
import lldbsuite.test.lldbplatformutil as lldbplatformutil
|
|
from lldbsuite.test.decorators import *
|
|
from lldbsuite.test.lldbtest import TestBase
|
|
from lldbsuite.test import lldbutil
|
|
|
|
@skipIf(oslist=["linux"], archs=["arm$"])
|
|
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
|
|
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
|
|
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
|
|
expected_synthetic_pc = 0xFFFE if is_arm_32bit else 0xFFFF
|
|
|
|
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(),
|
|
expected_synthetic_pc,
|
|
f"Thread with ID 1 should have synthetic PC {expected_synthetic_pc:#x}",
|
|
)
|
|
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")
|
|
|
|
def test_circular_dependency_fix(self):
|
|
"""Test that accessing input_frames in __init__ doesn't cause circular dependency.
|
|
|
|
This test verifies the fix for the circular dependency issue where:
|
|
1. Thread::GetStackFrameList() creates the frame provider
|
|
2. Provider's __init__ accesses input_frames and calls methods on frames
|
|
3. SBFrame methods trigger ExecutionContextRef::GetFrameSP()
|
|
4. Before the fix: GetFrameSP() would call Thread::GetStackFrameList() again -> circular dependency!
|
|
5. After the fix: GetFrameSP() uses the remembered frame list -> no circular dependency
|
|
|
|
The fix works by:
|
|
- StackFrame stores m_frame_list_wp (weak pointer to originating list)
|
|
- ExecutionContextRef stores m_frame_list_wp when created from a frame
|
|
- ExecutionContextRef::GetFrameSP() tries the remembered list first before asking the thread
|
|
"""
|
|
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()
|
|
original_pc = thread.GetFrameAtIndex(0).GetPC()
|
|
self.assertGreaterEqual(
|
|
original_frame_count, 2, "Should have at least 2 real frames"
|
|
)
|
|
|
|
# Import the provider that accesses input frames in __init__
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Register the CircularDependencyTestProvider
|
|
# Before the fix, this would crash or hang due to circular dependency
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.CircularDependencyTestProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
|
|
# If we get here without crashing, the fix is working!
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify the provider worked correctly
|
|
# Should have 1 synthetic frame + all original frames
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(
|
|
new_frame_count,
|
|
original_frame_count + 1,
|
|
"Should have original frames + 1 synthetic frame",
|
|
)
|
|
|
|
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
|
|
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
|
|
expected_synthetic_pc = 0xDEADBEEE if is_arm_32bit else 0xDEADBEEF
|
|
|
|
# First frame should be synthetic
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(
|
|
frame0.GetPC(),
|
|
expected_synthetic_pc,
|
|
f"First frame should be synthetic frame with PC {expected_synthetic_pc:#x}",
|
|
)
|
|
|
|
# Second frame should be the original first frame
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertEqual(
|
|
frame1.GetPC(),
|
|
original_pc,
|
|
"Second frame should be original first frame",
|
|
)
|
|
|
|
# Verify we can still call methods on frames (no circular dependency!)
|
|
for i in range(min(3, new_frame_count)):
|
|
frame = thread.GetFrameAtIndex(i)
|
|
self.assertIsNotNone(frame)
|
|
# These calls should not trigger circular dependency
|
|
pc = frame.GetPC()
|
|
self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")
|