| """ |
| 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") |