| """ | |
| 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 | |
| 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_thread_member(self): | |
| """Test that ScriptedFrame.thread is correctly set via GetThreadByID. | |
| This is a regression test for a bug where ScriptedFrame.__init__ used | |
| GetThreadByIndexID(tid) instead of GetThreadByID(tid). Since thread ID | |
| and index ID differ, the wrong API would produce an invalid thread. | |
| """ | |
| self.build() | |
| target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( | |
| self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False | |
| ) | |
| 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.ThreadValidatingFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register provider: {error}") | |
| self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero") | |
| # The ThreadValidatingFrame encodes thread validity in its function name. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| func_name = frame0.GetFunctionName() | |
| # If GetThreadByIndexID were used instead of GetThreadByID, the thread | |
| # would be invalid and the function name would be "thread_INVALID". | |
| self.assertNotEqual( | |
| func_name, | |
| "thread_INVALID", | |
| "ScriptedFrame.thread should be valid " | |
| "(GetThreadByID vs GetThreadByIndexID)", | |
| ) | |
| self.assertIn("thread_valid_id_", func_name) | |
| # Verify the encoded thread ID matches the actual thread ID. | |
| expected_tid = thread.GetThreadID() | |
| self.assertIn( | |
| hex(expected_tid), | |
| func_name, | |
| f"ScriptedFrame.thread ID should match: expected {expected_tid:#x} " | |
| f"in '{func_name}'", | |
| ) | |
| 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()) | |
| def test_valid_pc_no_module_frames(self): | |
| """Test that frames with valid PC but no module display correctly in backtrace.""" | |
| 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 ValidPCNoModuleFrameProvider. | |
| error = lldb.SBError() | |
| provider_id = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.ValidPCNoModuleFrameProvider", | |
| 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 (the synthetic frames). | |
| new_frame_count = thread.GetNumFrames() | |
| self.assertEqual( | |
| new_frame_count, | |
| original_frame_count + 2, | |
| "Should have original frames + 2 synthetic frames", | |
| ) | |
| # Verify first two frames have valid PCs and function names. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertIsNotNone(frame0) | |
| self.assertEqual( | |
| frame0.GetFunctionName(), | |
| "unknown_function_1", | |
| "First frame should be unknown_function_1", | |
| ) | |
| self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic") | |
| self.assertEqual( | |
| frame0.GetPC(), 0x1234000, "First frame should have PC 0x1234000" | |
| ) | |
| frame1 = thread.GetFrameAtIndex(1) | |
| self.assertIsNotNone(frame1) | |
| self.assertEqual( | |
| frame1.GetFunctionName(), | |
| "unknown_function_2", | |
| "Second frame should be unknown_function_2", | |
| ) | |
| self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic") | |
| self.assertEqual( | |
| frame1.GetPC(), 0x5678000, "Second frame should have PC 0x5678000" | |
| ) | |
| # Verify the frames display properly in backtrace. | |
| # The backtrace should show the PC values without crashing or displaying | |
| # invalid addresses like 0xffffffffffffffff. | |
| self.runCmd("bt") | |
| output = self.res.GetOutput() | |
| # Should show function names. | |
| self.assertIn("unknown_function_1", output) | |
| self.assertIn("unknown_function_2", output) | |
| # Should show PC addresses in hex format. | |
| self.assertIn("1234000", output) | |
| self.assertIn("5678000", output) | |
| # Verify PC and function name are properly separated by space. | |
| self.assertIn("1234000 unknown_function_1", output) | |
| self.assertIn("5678000 unknown_function_2", output) | |
| # Should NOT show invalid address. | |
| self.assertNotIn("ffffff", output.lower()) | |
| # Verify frame 2 is the original real frame 0. | |
| frame2 = thread.GetFrameAtIndex(2) | |
| self.assertIsNotNone(frame2) | |
| self.assertIn("thread_func", frame2.GetFunctionName()) | |
| def test_chained_frame_providers(self): | |
| """Test that multiple frame providers chain together.""" | |
| 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 test frame providers. | |
| script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") | |
| self.runCmd("command script import " + script_path) | |
| # Register 3 providers with different priorities. | |
| # Each provider adds 1 frame at the beginning. | |
| error = lldb.SBError() | |
| # Provider 1: Priority 10 - adds "foo" frame | |
| provider_id_1 = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.AddFooFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register foo provider: {error}") | |
| # Provider 2: Priority 20 - adds "bar" frame | |
| provider_id_2 = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.AddBarFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register bar provider: {error}") | |
| # Provider 3: Priority 30 - adds "baz" frame | |
| provider_id_3 = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.AddBazFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register baz provider: {error}") | |
| # Verify we have 3 more frames (one from each provider). | |
| new_frame_count = thread.GetNumFrames() | |
| self.assertEqual( | |
| new_frame_count, | |
| original_frame_count + 3, | |
| "Should have original frames + 3 chained frames", | |
| ) | |
| # Verify the chaining order: baz, bar, foo, then real frames. | |
| # Since priority is lower = higher, the order should be: | |
| # Provider 1 (priority 10) transforms real frames first -> adds "foo" | |
| # Provider 2 (priority 20) transforms Provider 1's output -> adds "bar" | |
| # Provider 3 (priority 30) transforms Provider 2's output -> adds "baz" | |
| # So final stack is: baz, bar, foo, real frames... | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertIsNotNone(frame0) | |
| self.assertEqual( | |
| frame0.GetFunctionName(), | |
| "baz", | |
| "Frame 0 should be 'baz' from last provider in chain", | |
| ) | |
| self.assertEqual(frame0.GetPC(), 0xBAC) | |
| frame1 = thread.GetFrameAtIndex(1) | |
| self.assertIsNotNone(frame1) | |
| self.assertEqual( | |
| frame1.GetFunctionName(), | |
| "bar", | |
| "Frame 1 should be 'bar' from second provider in chain", | |
| ) | |
| self.assertEqual(frame1.GetPC(), 0xBAA) | |
| frame2 = thread.GetFrameAtIndex(2) | |
| self.assertIsNotNone(frame2) | |
| self.assertEqual( | |
| frame2.GetFunctionName(), | |
| "foo", | |
| "Frame 2 should be 'foo' from first provider in chain", | |
| ) | |
| self.assertEqual(frame2.GetPC(), 0xF00) | |
| # Frame 3 should be the original real frame 0. | |
| frame3 = thread.GetFrameAtIndex(3) | |
| self.assertIsNotNone(frame3) | |
| self.assertIn("thread_func", frame3.GetFunctionName()) | |
| def test_get_values(self): | |
| """Test a frame that provides values.""" | |
| self.build() | |
| # Set the breakpoint after the variable_in_main variable exists and can be queried. | |
| target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( | |
| self, | |
| "Breakpoint for variable tests", | |
| 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 test frame providers. | |
| script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") | |
| self.runCmd("command script import " + script_path) | |
| # Register a provider that can provide variables. | |
| error = lldb.SBError() | |
| target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.ValueProvidingFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register provider: {error}") | |
| # Verify we have 1 more frame. | |
| new_frame_count = thread.GetNumFrames() | |
| self.assertEqual( | |
| new_frame_count, | |
| original_frame_count + 1, | |
| "Should have original frames + 1 extra frames", | |
| ) | |
| # Check that we can get variables from this frame. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertIsNotNone(frame0) | |
| # Ensure that we can get synthetic variables with `SetIncludeSynthetic`. | |
| options = lldb.SBVariablesOptions() | |
| options.SetIncludeSynthetic(True) | |
| variables = frame0.GetVariables(options) | |
| self.assertTrue(variables.IsValid()) | |
| self.assertTrue(variables.GetValueAtIndex(0).name == "_handler_one") | |
| # Check the `frame variable` command(s) handle synthetic variables the | |
| # way we expect by printing them. | |
| self.expect("frame var", substrs=["variable_in_main", "_handler_one"]) | |
| # Then, try and run it without synthetic variables and ensure we don't | |
| # get any, but we still get the others. | |
| interp = self.dbg.GetCommandInterpreter() | |
| command_result = lldb.SBCommandReturnObject() | |
| result = interp.HandleCommand("frame var -e", command_result) | |
| self.assertEqual( | |
| result, lldb.eReturnStatusSuccessFinishResult, "frame var -e didn't succeed" | |
| ) | |
| output = command_result.GetOutput() | |
| self.assertIn("variable_in_main", output, "Didn't find a regular variable") | |
| self.assertNotIn("_handler_one", output, "Found an synthetic variable") | |
| # Check that we can get values from paths. `_handler_one` is a special | |
| # value we provide through only our expression handler in the frame | |
| # implementation. We can't evaluate expressions on the special value | |
| # just because the test implementation doesn't handle it, and we | |
| # delegate all expression handling to the implementation. | |
| one = frame0.GetValueForVariablePath("_handler_one") | |
| self.assertEqual(one.unsigned, 1) | |
| # Ensure I can still access and do arithmetic on regular variables. | |
| varp1 = frame0.GetValueForVariablePath("variable_in_main + 1") | |
| self.assertEqual(varp1.unsigned, 124) | |
| def test_frame_validity_after_step(self): | |
| """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping. | |
| This test verifies that ExecutionContextRef properly handles frame list identifiers | |
| when the underlying stack changes. After stepping, old frame references should become | |
| invalid gracefully without crashing. | |
| """ | |
| 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) | |
| # Register a provider that prepends synthetic frames. | |
| error = lldb.SBError() | |
| provider_id = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.PrependFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertTrue(error.Success(), f"Failed to register provider: {error}") | |
| # Get frame references before stepping. | |
| frame0_before = thread.GetFrameAtIndex(0) | |
| frame1_before = thread.GetFrameAtIndex(1) | |
| frame2_before = thread.GetFrameAtIndex(2) | |
| self.assertIsNotNone(frame0_before) | |
| self.assertIsNotNone(frame1_before) | |
| self.assertIsNotNone(frame2_before) | |
| # Verify frames are valid and have expected PCs. | |
| self.assertTrue(frame0_before.IsValid(), "Frame 0 should be valid before step") | |
| self.assertTrue(frame1_before.IsValid(), "Frame 1 should be valid before step") | |
| self.assertTrue(frame2_before.IsValid(), "Frame 2 should be valid before step") | |
| pc0_before = frame0_before.GetPC() | |
| pc1_before = frame1_before.GetPC() | |
| pc2_before = frame2_before.GetPC() | |
| self.assertEqual(pc0_before, 0x9000, "Frame 0 should have synthetic PC 0x9000") | |
| self.assertEqual(pc1_before, 0xA000, "Frame 1 should have synthetic PC 0xA000") | |
| # Step the thread, which will invalidate the old frame list. | |
| thread.StepInstruction(False) | |
| # After stepping, the frame list has changed. Old frame references should | |
| # detect this and become invalid, but shouldn't crash. | |
| # The key here is that GetPC() and other operations should handle the | |
| # "frame provider no longer available" case gracefully. | |
| # Try to access the old frames - they should either: | |
| # 1. Return invalid/default values gracefully, or | |
| # 2. Still work if the frame provider is re-applied. | |
| # Get new frames after stepping. | |
| frame0_after = thread.GetFrameAtIndex(0) | |
| self.assertIsNotNone(frame0_after) | |
| self.assertTrue( | |
| frame0_after.IsValid(), "New frame 0 should be valid after step" | |
| ) | |
| # The old frame references might or might not be valid depending on whether | |
| # the frame provider is still active. What's important is that accessing | |
| # them doesn't crash and handles the situation gracefully. | |
| # We'll just verify we can call methods on them without crashing. | |
| try: | |
| _ = frame0_before.GetPC() | |
| _ = frame0_before.IsValid() | |
| _ = frame0_before.GetFunctionName() | |
| except Exception as e: | |
| self.fail(f"Accessing old frame reference should not crash: {e}") | |
| def test_provider_lifecycle_with_frame_validity(self): | |
| """Test provider registration/removal at breakpoints and SBFrame validity across lifecycle. | |
| This test verifies: | |
| 1. Registering a provider while stopped at a breakpoint. | |
| 2. SBFrame references from synthetic frames persist across continues. | |
| 3. SBFrame references can access variables in real frames while provider is active. | |
| 4. Removing a provider while stopped at a breakpoint. | |
| 5. SBFrame references from removed provider don't crash when accessed. | |
| """ | |
| self.build() | |
| target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) | |
| self.assertTrue(target.IsValid(), "Target should be valid") | |
| # Import the test frame provider. | |
| script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") | |
| self.runCmd("command script import " + script_path) | |
| # Set up breakpoints at the return statements in foo, bar, and baz. | |
| # This ensures local variables are initialized. | |
| bp_foo = target.BreakpointCreateBySourceRegex( | |
| "Break in foo", lldb.SBFileSpec(self.source) | |
| ) | |
| bp_bar = target.BreakpointCreateBySourceRegex( | |
| "Break in bar", lldb.SBFileSpec(self.source) | |
| ) | |
| bp_baz = target.BreakpointCreateBySourceRegex( | |
| "Break in baz", lldb.SBFileSpec(self.source) | |
| ) | |
| self.assertTrue(bp_foo.IsValid(), "Breakpoint at foo should be valid") | |
| self.assertTrue(bp_bar.IsValid(), "Breakpoint at bar should be valid") | |
| self.assertTrue(bp_baz.IsValid(), "Breakpoint at baz should be valid") | |
| # Launch the process. | |
| process = target.LaunchSimple(None, None, self.get_process_working_directory()) | |
| self.assertTrue(process.IsValid(), "Process should be valid") | |
| # We should hit the foo breakpoint first. | |
| self.assertEqual( | |
| process.GetState(), lldb.eStateStopped, "Process should be stopped at foo" | |
| ) | |
| thread = process.GetSelectedThread() | |
| self.assertIsNotNone(thread, "Should have a selected thread") | |
| # Register the provider at foo breakpoint. | |
| 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") | |
| # Get individual frames BEFORE getting the full backtrace. | |
| # This tests accessing frames before forcing evaluation of all frames. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| frame1 = thread.GetFrameAtIndex(1) | |
| frame2 = thread.GetFrameAtIndex(2) | |
| self.assertIsNotNone(frame0, "Frame 0 should exist") | |
| self.assertIsNotNone(frame1, "Frame 1 should exist") | |
| self.assertIsNotNone(frame2, "Frame 2 should exist") | |
| # First two frames should be synthetic with expected PCs. | |
| pc0 = frame0.GetPC() | |
| pc1 = frame1.GetPC() | |
| self.assertEqual(pc0, 0x9000, "Frame 0 should have synthetic PC 0x9000") | |
| self.assertEqual(pc1, 0xA000, "Frame 1 should have synthetic PC 0xA000") | |
| # Frame 2 should be the real foo frame. | |
| self.assertIn("foo", frame2.GetFunctionName(), "Frame 2 should be in foo") | |
| # Save references to the synthetic frames. | |
| saved_frames = [frame0, frame1, frame2] | |
| # Test accessing saved frames at foo BEFORE getting full backtrace. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| _ = saved_frames[2].GetFunctionName() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at foo before full backtrace should not crash: {e}" | |
| ) | |
| # Now verify the provider is active by checking frame count. | |
| # PrependFrameProvider adds 2 synthetic frames. | |
| # This forces a full backtrace evaluation. | |
| original_frame_count = thread.GetNumFrames() | |
| self.assertGreater( | |
| original_frame_count, | |
| 2, | |
| "Should have at least synthetic frames + real frames", | |
| ) | |
| # Test accessing saved frames at foo AFTER getting full backtrace. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| _ = saved_frames[2].GetFunctionName() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at foo after full backtrace should not crash: {e}" | |
| ) | |
| # Verify we can access variables in frame2 (real frame). | |
| foo_local = frame2.FindVariable("foo_local") | |
| self.assertTrue(foo_local.IsValid(), "Should find foo_local variable") | |
| self.assertEqual( | |
| foo_local.GetValueAsUnsigned(), 20, "foo_local should be 20 (10 * 2)" | |
| ) | |
| # Continue to bar breakpoint. | |
| threads = lldbutil.continue_to_breakpoint(process, bp_bar) | |
| self.assertIsNotNone(threads, "Should have stopped at bar breakpoint") | |
| self.assertEqual(len(threads), 1, "Should have one thread stopped at bar") | |
| thread = threads[0] | |
| # Verify the saved frames are still accessible without crashing at bar. | |
| # Do this BEFORE getting the full backtrace. | |
| # Note: They might not be "valid" in the traditional sense since we've moved | |
| # to a different execution context, but they shouldn't crash. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at bar before full backtrace should not crash: {e}" | |
| ) | |
| # Verify the provider is still active by getting frame count. | |
| # This forces full backtrace evaluation. | |
| current_frame_count = thread.GetNumFrames() | |
| self.assertGreater( | |
| current_frame_count, 2, "Should still have synthetic frames at bar" | |
| ) | |
| # Access the saved frames again AFTER getting the full backtrace. | |
| # This ensures that forcing a full backtrace evaluation doesn't break | |
| # the saved frame references. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at bar after full backtrace should not crash: {e}" | |
| ) | |
| # Get current frames at bar. | |
| bar_frame0 = thread.GetFrameAtIndex(0) | |
| bar_frame1 = thread.GetFrameAtIndex(1) | |
| bar_frame2 = thread.GetFrameAtIndex(2) | |
| # Verify current frames have synthetic PCs. | |
| self.assertEqual( | |
| bar_frame0.GetPC(), 0x9000, "Frame 0 at bar should have synthetic PC" | |
| ) | |
| self.assertEqual( | |
| bar_frame1.GetPC(), 0xA000, "Frame 1 at bar should have synthetic PC" | |
| ) | |
| self.assertIn("bar", bar_frame2.GetFunctionName(), "Frame 2 should be in bar") | |
| # Verify we can access variables in the bar frame. | |
| bar_local = bar_frame2.FindVariable("bar_local") | |
| self.assertTrue(bar_local.IsValid(), "Should find bar_local variable") | |
| self.assertEqual( | |
| bar_local.GetValueAsUnsigned(), 25, "bar_local should be 25 (5 * 5)" | |
| ) | |
| # Continue to baz breakpoint. | |
| threads = lldbutil.continue_to_breakpoint(process, bp_baz) | |
| self.assertIsNotNone(threads, "Should have stopped at baz breakpoint") | |
| self.assertEqual(len(threads), 1, "Should have one thread stopped at baz") | |
| thread = threads[0] | |
| # Verify the saved frames are still accessible without crashing at baz. | |
| # Do this BEFORE getting the full backtrace. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| _ = saved_frames[2].GetFunctionName() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at baz before full backtrace should not crash: {e}" | |
| ) | |
| # Get the frame count to force full backtrace evaluation at baz. | |
| baz_frame_count = thread.GetNumFrames() | |
| self.assertGreater( | |
| baz_frame_count, 2, "Should still have synthetic frames at baz" | |
| ) | |
| # Verify the saved frames are still accessible AFTER getting full backtrace at baz. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[1].GetPC() | |
| _ = saved_frames[2].GetFunctionName() | |
| except Exception as e: | |
| self.fail( | |
| f"Accessing saved frames at baz after full backtrace should not crash: {e}" | |
| ) | |
| # Now manually remove the provider. | |
| result = target.RemoveScriptedFrameProvider(provider_id) | |
| self.assertSuccess( | |
| result, f"Should successfully remove provider with ID {provider_id}" | |
| ) | |
| # Verify frames no longer have synthetic frames. | |
| final_frame_count = thread.GetNumFrames() | |
| # Without the provider, we should have fewer frames (no synthetic ones). | |
| self.assertLess( | |
| final_frame_count, | |
| original_frame_count, | |
| "Frame count should decrease after provider removal", | |
| ) | |
| # First frame should now be the real baz frame (no synthetic frames). | |
| baz_frame0 = thread.GetFrameAtIndex(0) | |
| self.assertIn( | |
| "baz", baz_frame0.GetFunctionName(), "Frame 0 should now be real baz frame" | |
| ) | |
| # The synthetic PC values should no longer appear. | |
| for i in range(final_frame_count): | |
| frame = thread.GetFrameAtIndex(i) | |
| pc = frame.GetPC() | |
| self.assertNotEqual( | |
| pc, 0x9000, f"Frame {i} should not have synthetic PC 0x9000" | |
| ) | |
| self.assertNotEqual( | |
| pc, 0xA000, f"Frame {i} should not have synthetic PC 0xA000" | |
| ) | |
| # Verify the originally saved frames are now truly invalid/stale. | |
| # They should still not crash when accessed. | |
| try: | |
| _ = saved_frames[0].GetPC() | |
| _ = saved_frames[0].IsValid() | |
| _ = saved_frames[1].GetPC() | |
| _ = saved_frames[1].IsValid() | |
| except Exception as e: | |
| self.fail(f"Accessing invalidated frames should not crash: {e}") | |
| def test_event_broadcasting(self): | |
| """Test that adding/removing frame providers broadcasts eBroadcastBitStackChanged.""" | |
| self.build() | |
| listener = lldb.SBListener("stack_changed_listener") | |
| listener.StartListeningForEventClass( | |
| self.dbg, | |
| lldb.SBThread.GetBroadcasterClassName(), | |
| lldb.SBThread.eBroadcastBitStackChanged, | |
| ) | |
| target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( | |
| self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False | |
| ) | |
| expected_thread_ids = { | |
| process.GetThreadAtIndex(i).GetIndexID() | |
| for i in range(process.GetNumThreads()) | |
| } | |
| def collect_stack_changed_thread_ids(count): | |
| event = lldb.SBEvent() | |
| thread_ids = set() | |
| for _ in range(count): | |
| if not listener.WaitForEvent(5, event): | |
| break | |
| self.assertEqual( | |
| event.GetType(), | |
| lldb.SBThread.eBroadcastBitStackChanged, | |
| "Event should be stack changed", | |
| ) | |
| thread_ids.add(lldb.SBThread.GetThreadFromEvent(event).GetIndexID()) | |
| return thread_ids | |
| # Import the test frame provider. | |
| script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") | |
| self.runCmd("command script import " + script_path) | |
| # 1. Test registration. | |
| error = lldb.SBError() | |
| provider_id = target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.ReplaceFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertSuccess(error, f"Failed to register provider: {error}") | |
| self.assertEqual( | |
| collect_stack_changed_thread_ids(len(expected_thread_ids)), | |
| expected_thread_ids, | |
| "All threads should broadcast eBroadcastBitStackChanged on registration", | |
| ) | |
| # 2. Test removal. | |
| result = target.RemoveScriptedFrameProvider(provider_id) | |
| self.assertSuccess(result, f"Failed to remove provider: {result}") | |
| self.assertEqual( | |
| collect_stack_changed_thread_ids(len(expected_thread_ids)), | |
| expected_thread_ids, | |
| "All threads should broadcast eBroadcastBitStackChanged on removal", | |
| ) | |
| # 3. Test clear. | |
| target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.ReplaceFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| # Consume registration | |
| collect_stack_changed_thread_ids(len(expected_thread_ids)) | |
| self.runCmd("target frame-provider clear") | |
| self.assertEqual( | |
| collect_stack_changed_thread_ids(len(expected_thread_ids)), | |
| expected_thread_ids, | |
| "All threads should broadcast eBroadcastBitStackChanged on clear", | |
| ) | |
| def test_incremental_fetch_synthetic_frame_identity(self): | |
| """Test that incrementally fetched PC-less synthetic frames each get a | |
| unique StackID so they resolve to the correct frame.""" | |
| self.build() | |
| target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( | |
| self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False | |
| ) | |
| script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") | |
| self.runCmd("command script import " + script_path) | |
| error = lldb.SBError() | |
| target.RegisterScriptedFrameProvider( | |
| "test_frame_providers.PythonSourceFrameProvider", | |
| lldb.SBStructuredData(), | |
| error, | |
| ) | |
| self.assertSuccess(error) | |
| # Fetch frames one at a time to trigger separate FetchFramesUpTo calls. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| frame1 = thread.GetFrameAtIndex(1) | |
| self.assertEqual(frame0.GetFunctionName(), "compute_fibonacci") | |
| self.assertEqual(frame1.GetFunctionName(), "process_data") |