We want to reload the call stack whenever the frame providers are updated. To do so, we now emit a `eBroadcastBitStackChanged` on all threads whenever any changes to the frame providers take place. I found this very useful while iterating on a frame provider in lldb-dap. So far, the new frame provider only took effect after continuing execution. Now the backtrace in VS-Code gets refreshed immediately upon running `target frame-provider add`.
1189 lines
48 KiB
Python
1189 lines
48 KiB
Python
"""
|
|
Test scripted frame provider functionality.
|
|
"""
|
|
|
|
import os
|
|
|
|
import lldb
|
|
import lldbsuite.test.lldbplatformutil as lldbplatformutil
|
|
from lldbsuite.test.decorators import *
|
|
from lldbsuite.test.lldbtest import TestBase
|
|
from lldbsuite.test import lldbutil
|
|
|
|
class ScriptedFrameProviderTestCase(TestBase):
|
|
NO_DEBUG_INFO_TESTCASE = True
|
|
|
|
def setUp(self):
|
|
TestBase.setUp(self)
|
|
self.source = "main.cpp"
|
|
|
|
def test_replace_all_frames(self):
|
|
"""Test that we can replace the entire stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the test frame provider.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Attach the Replace provider.
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ReplaceFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have exactly 3 synthetic frames.
|
|
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
|
|
|
|
# Verify frame indices and PCs (dictionary-based frames don't have custom function names).
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(frame0.GetPC(), 0x1000)
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertIn("thread_func", frame1.GetFunctionName())
|
|
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIsNotNone(frame2)
|
|
self.assertEqual(frame2.GetPC(), 0x3000)
|
|
|
|
def test_prepend_frames(self):
|
|
"""Test that we can add frames before real stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count and PC.
|
|
original_frame_count = thread.GetNumFrames()
|
|
self.assertGreaterEqual(
|
|
original_frame_count, 2, "Should have at least 2 real frames"
|
|
)
|
|
|
|
# Import and attach Prepend provider.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.PrependFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 2 more frames.
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(new_frame_count, original_frame_count + 2)
|
|
|
|
# Verify first 2 frames are synthetic (check PCs, not function names).
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertEqual(frame0.GetPC(), 0x9000)
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertEqual(frame1.GetPC(), 0xA000)
|
|
|
|
# Verify frame 2 is the original real frame 0.
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIn("thread_func", frame2.GetFunctionName())
|
|
|
|
def test_append_frames(self):
|
|
"""Test that we can add frames after real stack."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count.
|
|
original_frame_count = thread.GetNumFrames()
|
|
|
|
# Import and attach Append provider.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.AppendFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 1 more frame.
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(new_frame_count, original_frame_count + 1)
|
|
|
|
# Verify first frames are still real.
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIn("thread_func", frame0.GetFunctionName())
|
|
|
|
frame_n_plus_1 = thread.GetFrameAtIndex(new_frame_count - 1)
|
|
self.assertEqual(frame_n_plus_1.GetPC(), 0x10)
|
|
|
|
def test_scripted_frame_objects(self):
|
|
"""Test that provider can return ScriptedFrame objects."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the provider that returns ScriptedFrame objects.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ScriptedFrameObjectProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 5 frames.
|
|
self.assertEqual(
|
|
thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
|
|
)
|
|
|
|
# Verify frame properties from CustomScriptedFrame.
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
|
|
self.assertEqual(frame0.GetPC(), 0x5000)
|
|
self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertEqual(frame1.GetPC(), 0x6000)
|
|
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIsNotNone(frame2)
|
|
self.assertEqual(frame2.GetFunctionName(), "custom_scripted_frame_2")
|
|
self.assertEqual(frame2.GetPC(), 0x7000)
|
|
self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
def test_applies_to_thread(self):
|
|
"""Test that applies_to_thread filters which threads get the provider."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# We should have at least 2 threads (worker threads) at the breakpoint.
|
|
num_threads = process.GetNumThreads()
|
|
self.assertGreaterEqual(
|
|
num_threads, 2, "Should have at least 2 threads at breakpoint"
|
|
)
|
|
|
|
# Import the test frame provider.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Collect original thread info before applying provider.
|
|
thread_info = {}
|
|
for i in range(num_threads):
|
|
t = process.GetThreadAtIndex(i)
|
|
thread_info[t.GetIndexID()] = {
|
|
"frame_count": t.GetNumFrames(),
|
|
"pc": t.GetFrameAtIndex(0).GetPC(),
|
|
}
|
|
|
|
# Register the ThreadFilterFrameProvider which only applies to thread ID 1.
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ThreadFilterFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Check each thread.
|
|
thread_id_1_found = False
|
|
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
|
|
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
|
|
expected_synthetic_pc = 0xFFFE if is_arm_32bit else 0xFFFF
|
|
|
|
for i in range(num_threads):
|
|
t = process.GetThreadAtIndex(i)
|
|
thread_id = t.GetIndexID()
|
|
|
|
if thread_id == 1:
|
|
# Thread with ID 1 should have synthetic frame.
|
|
thread_id_1_found = True
|
|
self.assertEqual(
|
|
t.GetNumFrames(),
|
|
1,
|
|
f"Thread with ID 1 should have 1 synthetic frame",
|
|
)
|
|
self.assertEqual(
|
|
t.GetFrameAtIndex(0).GetPC(),
|
|
expected_synthetic_pc,
|
|
f"Thread with ID 1 should have synthetic PC {expected_synthetic_pc:#x}",
|
|
)
|
|
else:
|
|
# Other threads should keep their original frames.
|
|
self.assertEqual(
|
|
t.GetNumFrames(),
|
|
thread_info[thread_id]["frame_count"],
|
|
f"Thread with ID {thread_id} should not be affected by provider",
|
|
)
|
|
self.assertEqual(
|
|
t.GetFrameAtIndex(0).GetPC(),
|
|
thread_info[thread_id]["pc"],
|
|
f"Thread with ID {thread_id} should have its original PC",
|
|
)
|
|
|
|
# We should have found at least one thread with ID 1.
|
|
self.assertTrue(
|
|
thread_id_1_found,
|
|
"Should have found a thread with ID 1 to test filtering",
|
|
)
|
|
|
|
def test_remove_frame_provider_by_id(self):
|
|
"""Test that RemoveScriptedFrameProvider removes a specific provider by ID."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Import the test frame providers.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Get original frame count.
|
|
original_frame_count = thread.GetNumFrames()
|
|
original_pc = thread.GetFrameAtIndex(0).GetPC()
|
|
|
|
# Register the first provider and get its ID.
|
|
error = lldb.SBError()
|
|
provider_id_1 = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.ReplaceFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")
|
|
|
|
# Verify first provider is active (3 synthetic frames).
|
|
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
|
|
)
|
|
|
|
# Register a second provider and get its ID.
|
|
provider_id_2 = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.PrependFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider 2: {error}")
|
|
|
|
# Verify IDs are different
|
|
self.assertNotEqual(
|
|
provider_id_1, provider_id_2, "Provider IDs should be unique"
|
|
)
|
|
|
|
# Now remove the first provider by ID
|
|
result = target.RemoveScriptedFrameProvider(provider_id_1)
|
|
self.assertSuccess(
|
|
result, f"Should successfully remove provider with ID {provider_id_1}"
|
|
)
|
|
|
|
# After removing the first provider, the second provider should still be
|
|
# active. The PrependFrameProvider adds 2 frames before the real stack.
|
|
# Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
|
|
# have the original frames (from real stack) with PrependFrameProvider applied.
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(
|
|
new_frame_count,
|
|
original_frame_count + 2,
|
|
"Should have original frames + 2 prepended frames",
|
|
)
|
|
|
|
# First two frames should be from PrependFrameProvider.
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(),
|
|
0x9000,
|
|
"First frame should be from PrependFrameProvider",
|
|
)
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(1).GetPC(),
|
|
0xA000,
|
|
"Second frame should be from PrependFrameProvider",
|
|
)
|
|
|
|
# Remove the second provider.
|
|
result = target.RemoveScriptedFrameProvider(provider_id_2)
|
|
self.assertSuccess(
|
|
result, f"Should successfully remove provider with ID {provider_id_2}"
|
|
)
|
|
|
|
# After removing both providers, frames should be back to original.
|
|
self.assertEqual(
|
|
thread.GetNumFrames(),
|
|
original_frame_count,
|
|
"Should restore original frame count",
|
|
)
|
|
self.assertEqual(
|
|
thread.GetFrameAtIndex(0).GetPC(),
|
|
original_pc,
|
|
"Should restore original PC",
|
|
)
|
|
|
|
# Try to remove a provider that doesn't exist.
|
|
result = target.RemoveScriptedFrameProvider(999999)
|
|
self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")
|
|
|
|
def test_circular_dependency_fix(self):
|
|
"""Test that accessing input_frames in __init__ doesn't cause circular dependency.
|
|
|
|
This test verifies the fix for the circular dependency issue where:
|
|
1. Thread::GetStackFrameList() creates the frame provider
|
|
2. Provider's __init__ accesses input_frames and calls methods on frames
|
|
3. SBFrame methods trigger ExecutionContextRef::GetFrameSP()
|
|
4. Before the fix: GetFrameSP() would call Thread::GetStackFrameList() again -> circular dependency!
|
|
5. After the fix: GetFrameSP() uses the remembered frame list -> no circular dependency
|
|
|
|
The fix works by:
|
|
- StackFrame stores m_frame_list_wp (weak pointer to originating list)
|
|
- ExecutionContextRef stores m_frame_list_wp when created from a frame
|
|
- ExecutionContextRef::GetFrameSP() tries the remembered list first before asking the thread
|
|
"""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count and PC.
|
|
original_frame_count = thread.GetNumFrames()
|
|
original_pc = thread.GetFrameAtIndex(0).GetPC()
|
|
self.assertGreaterEqual(
|
|
original_frame_count, 2, "Should have at least 2 real frames"
|
|
)
|
|
|
|
# Import the provider that accesses input frames in __init__.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Register the CircularDependencyTestProvider.
|
|
# Before the fix, this would crash or hang due to circular dependency.
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.CircularDependencyTestProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
|
|
# If we get here without crashing, the fix is working!
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify the provider worked correctly,
|
|
# Should have 1 synthetic frame + all original frames.
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(
|
|
new_frame_count,
|
|
original_frame_count + 1,
|
|
"Should have original frames + 1 synthetic frame",
|
|
)
|
|
|
|
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
|
|
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
|
|
expected_synthetic_pc = 0xDEADBEEE if is_arm_32bit else 0xDEADBEEF
|
|
|
|
# First frame should be synthetic.
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(
|
|
frame0.GetPC(),
|
|
expected_synthetic_pc,
|
|
f"First frame should be synthetic frame with PC {expected_synthetic_pc:#x}",
|
|
)
|
|
|
|
# Second frame should be the original first frame.
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertEqual(
|
|
frame1.GetPC(),
|
|
original_pc,
|
|
"Second frame should be original first frame",
|
|
)
|
|
|
|
# Verify we can still call methods on frames (no circular dependency!).
|
|
for i in range(min(3, new_frame_count)):
|
|
frame = thread.GetFrameAtIndex(i)
|
|
self.assertIsNotNone(frame)
|
|
# These calls should not trigger circular dependency.
|
|
pc = frame.GetPC()
|
|
self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")
|
|
|
|
def test_python_source_frames(self):
|
|
"""Test that frames can point to Python source files and display properly."""
|
|
self.build()
|
|
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
|
|
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
|
|
)
|
|
|
|
# Get original frame count.
|
|
original_frame_count = thread.GetNumFrames()
|
|
self.assertGreaterEqual(
|
|
original_frame_count, 2, "Should have at least 2 real frames"
|
|
)
|
|
|
|
# Import the provider.
|
|
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
|
|
self.runCmd("command script import " + script_path)
|
|
|
|
# Register the PythonSourceFrameProvider.
|
|
error = lldb.SBError()
|
|
provider_id = target.RegisterScriptedFrameProvider(
|
|
"test_frame_providers.PythonSourceFrameProvider",
|
|
lldb.SBStructuredData(),
|
|
error,
|
|
)
|
|
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
|
|
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
|
|
|
|
# Verify we have 3 more frames (Python frames).
|
|
new_frame_count = thread.GetNumFrames()
|
|
self.assertEqual(
|
|
new_frame_count,
|
|
original_frame_count + 3,
|
|
"Should have original frames + 3 Python frames",
|
|
)
|
|
|
|
# Verify first three frames are Python source frames.
|
|
frame0 = thread.GetFrameAtIndex(0)
|
|
self.assertIsNotNone(frame0)
|
|
self.assertEqual(
|
|
frame0.GetFunctionName(),
|
|
"compute_fibonacci",
|
|
"First frame should be compute_fibonacci",
|
|
)
|
|
self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
|
|
# PC-less frames should show invalid address and not crash.
|
|
self.assertEqual(
|
|
frame0.GetPC(),
|
|
lldb.LLDB_INVALID_ADDRESS,
|
|
"PC-less frame should have LLDB_INVALID_ADDRESS",
|
|
)
|
|
|
|
self.assertEqual(
|
|
frame0.GetFP(),
|
|
lldb.LLDB_INVALID_ADDRESS,
|
|
"PC-less frame FP should return LLDB_INVALID_ADDRESS",
|
|
)
|
|
self.assertEqual(
|
|
frame0.GetSP(),
|
|
lldb.LLDB_INVALID_ADDRESS,
|
|
"PC-less frame SP should return LLDB_INVALID_ADDRESS",
|
|
)
|
|
self.assertEqual(
|
|
frame0.GetCFA(),
|
|
0,
|
|
"PC-less frame CFA should return 0",
|
|
)
|
|
|
|
frame1 = thread.GetFrameAtIndex(1)
|
|
self.assertIsNotNone(frame1)
|
|
self.assertEqual(
|
|
frame1.GetFunctionName(),
|
|
"process_data",
|
|
"Second frame should be process_data",
|
|
)
|
|
self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
frame2 = thread.GetFrameAtIndex(2)
|
|
self.assertIsNotNone(frame2)
|
|
self.assertEqual(frame2.GetFunctionName(), "main", "Third frame should be main")
|
|
self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
|
|
|
|
# Verify line entry information is present.
|
|
line_entry0 = frame0.GetLineEntry()
|
|
self.assertTrue(line_entry0.IsValid(), "Frame 0 should have a valid line entry")
|
|
self.assertEqual(line_entry0.GetLine(), 7, "Frame 0 should point to line 7")
|
|
file_spec0 = line_entry0.GetFileSpec()
|
|
self.assertTrue(file_spec0.IsValid(), "Frame 0 should have valid file spec")
|
|
self.assertEqual(
|
|
file_spec0.GetFilename(),
|
|
"python_helper.py",
|
|
"Frame 0 should point to python_helper.py",
|
|
)
|
|
|
|
line_entry1 = frame1.GetLineEntry()
|
|
self.assertTrue(line_entry1.IsValid(), "Frame 1 should have a valid line entry")
|
|
self.assertEqual(line_entry1.GetLine(), 16, "Frame 1 should point to line 16")
|
|
|
|
line_entry2 = frame2.GetLineEntry()
|
|
self.assertTrue(line_entry2.IsValid(), "Frame 2 should have a valid line entry")
|
|
self.assertEqual(line_entry2.GetLine(), 27, "Frame 2 should point to line 27")
|
|
|
|
# Verify the frames display properly in backtrace.
|
|
# This tests that PC-less frames don't show 0xffffffffffffffff.
|
|
self.runCmd("bt")
|
|
output = self.res.GetOutput()
|
|
|
|
# Should show function names.
|
|
self.assertIn("compute_fibonacci", output)
|
|
self.assertIn("process_data", output)
|
|
self.assertIn("main", output)
|
|
|
|
# Should show Python file.
|
|
self.assertIn("python_helper.py", output)
|
|
|
|
# Should show line numbers.
|
|
self.assertIn(":7", output) # compute_fibonacci line.
|
|
self.assertIn(":16", output) # process_data line.
|
|
self.assertIn(":27", output) # main line.
|
|
|
|
# Should NOT show invalid address (0xffffffffffffffff).
|
|
self.assertNotIn("0xffffffffffffffff", output.lower())
|
|
|
|
# Verify frame 3 is the original real frame 0.
|
|
frame3 = thread.GetFrameAtIndex(3)
|
|
self.assertIsNotNone(frame3)
|
|
self.assertIn("thread_func", frame3.GetFunctionName())
|
|
|
|
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)
|
|
# Get every variable visible at this point
|
|
variables = frame0.GetVariables(True, True, True, False)
|
|
self.assertTrue(variables.IsValid())
|
|
self.assertEqual(variables.GetSize(), 1)
|
|
|
|
# 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.
|
|
one = frame0.GetValueForVariablePath("_handler_one")
|
|
self.assertEqual(one.unsigned, 1)
|
|
var = frame0.GetValueForVariablePath("variable_in_main")
|
|
# The names won't necessarily match, but the values should (the frame renames the SBValue)
|
|
self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned)
|
|
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",
|
|
) |