""" 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") 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", ) # First frame should be synthetic frame0 = thread.GetFrameAtIndex(0) self.assertIsNotNone(frame0) self.assertEqual( frame0.GetPC(), 0xDEADBEEF, "First frame should be synthetic frame with PC 0xDEADBEEF", ) # 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")