""" 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") def test_python_source_frames(self): """Test that frames can point to Python source files and display properly.""" 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() self.assertGreaterEqual( original_frame_count, 2, "Should have at least 2 real frames" ) # Import the provider. script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") self.runCmd("command script import " + script_path) # Register the PythonSourceFrameProvider. error = lldb.SBError() provider_id = target.RegisterScriptedFrameProvider( "test_frame_providers.PythonSourceFrameProvider", 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 3 more frames (Python frames). new_frame_count = thread.GetNumFrames() self.assertEqual( new_frame_count, original_frame_count + 3, "Should have original frames + 3 Python frames", ) # Verify first three frames are Python source frames. frame0 = thread.GetFrameAtIndex(0) self.assertIsNotNone(frame0) self.assertEqual( frame0.GetFunctionName(), "compute_fibonacci", "First frame should be compute_fibonacci", ) self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic") # PC-less frames should show invalid address and not crash. self.assertEqual( frame0.GetPC(), lldb.LLDB_INVALID_ADDRESS, "PC-less frame should have LLDB_INVALID_ADDRESS", ) self.assertEqual( frame0.GetFP(), lldb.LLDB_INVALID_ADDRESS, "PC-less frame FP should return LLDB_INVALID_ADDRESS", ) self.assertEqual( frame0.GetSP(), lldb.LLDB_INVALID_ADDRESS, "PC-less frame SP should return LLDB_INVALID_ADDRESS", ) self.assertEqual( frame0.GetCFA(), 0, "PC-less frame CFA should return 0", ) frame1 = thread.GetFrameAtIndex(1) self.assertIsNotNone(frame1) self.assertEqual( frame1.GetFunctionName(), "process_data", "Second frame should be process_data", ) self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic") frame2 = thread.GetFrameAtIndex(2) self.assertIsNotNone(frame2) self.assertEqual(frame2.GetFunctionName(), "main", "Third frame should be main") self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic") # Verify line entry information is present. line_entry0 = frame0.GetLineEntry() self.assertTrue(line_entry0.IsValid(), "Frame 0 should have a valid line entry") self.assertEqual(line_entry0.GetLine(), 7, "Frame 0 should point to line 7") file_spec0 = line_entry0.GetFileSpec() self.assertTrue(file_spec0.IsValid(), "Frame 0 should have valid file spec") self.assertEqual( file_spec0.GetFilename(), "python_helper.py", "Frame 0 should point to python_helper.py", ) line_entry1 = frame1.GetLineEntry() self.assertTrue(line_entry1.IsValid(), "Frame 1 should have a valid line entry") self.assertEqual(line_entry1.GetLine(), 16, "Frame 1 should point to line 16") line_entry2 = frame2.GetLineEntry() self.assertTrue(line_entry2.IsValid(), "Frame 2 should have a valid line entry") self.assertEqual(line_entry2.GetLine(), 27, "Frame 2 should point to line 27") # Verify the frames display properly in backtrace. # This tests that PC-less frames don't show 0xffffffffffffffff. self.runCmd("bt") output = self.res.GetOutput() # Should show function names. self.assertIn("compute_fibonacci", output) self.assertIn("process_data", output) self.assertIn("main", output) # Should show Python file. self.assertIn("python_helper.py", output) # Should show line numbers. self.assertIn(":7", output) # compute_fibonacci line. self.assertIn(":16", output) # process_data line. self.assertIn(":27", output) # main line. # Should NOT show invalid address (0xffffffffffffffff). self.assertNotIn("0xffffffffffffffff", output.lower()) # Verify frame 3 is the original real frame 0. frame3 = thread.GetFrameAtIndex(3) self.assertIsNotNone(frame3) self.assertIn("thread_func", frame3.GetFunctionName())