Skip to content

Commit c53a4ec

Browse files
committed
[lldb] Add test for scripted frame provider producing python frames
Signed-off-by: Med Ismail Bennani <[email protected]>
1 parent cb00990 commit c53a4ec

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,128 @@ def test_circular_dependency_fix(self):
426426
# These calls should not trigger circular dependency
427427
pc = frame.GetPC()
428428
self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")
429+
430+
def test_python_source_frames(self):
431+
"""Test that frames can point to Python source files and display properly."""
432+
self.build()
433+
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
434+
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
435+
)
436+
437+
# Get original frame count
438+
original_frame_count = thread.GetNumFrames()
439+
self.assertGreaterEqual(
440+
original_frame_count, 2, "Should have at least 2 real frames"
441+
)
442+
443+
# Import the provider
444+
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
445+
self.runCmd("command script import " + script_path)
446+
447+
# Register the PythonSourceFrameProvider
448+
error = lldb.SBError()
449+
provider_id = target.RegisterScriptedFrameProvider(
450+
"test_frame_providers.PythonSourceFrameProvider",
451+
lldb.SBStructuredData(),
452+
error,
453+
)
454+
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
455+
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
456+
457+
# Verify we have 3 more frames (Python frames)
458+
new_frame_count = thread.GetNumFrames()
459+
self.assertEqual(
460+
new_frame_count,
461+
original_frame_count + 3,
462+
"Should have original frames + 3 Python frames",
463+
)
464+
465+
# Verify first three frames are Python source frames
466+
frame0 = thread.GetFrameAtIndex(0)
467+
self.assertIsNotNone(frame0)
468+
self.assertEqual(
469+
frame0.GetFunctionName(),
470+
"compute_fibonacci",
471+
"First frame should be compute_fibonacci",
472+
)
473+
self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
474+
# PC-less frames should show invalid address
475+
self.assertEqual(
476+
frame0.GetPC(),
477+
lldb.LLDB_INVALID_ADDRESS,
478+
"PC-less frame should have LLDB_INVALID_ADDRESS",
479+
)
480+
481+
frame1 = thread.GetFrameAtIndex(1)
482+
self.assertIsNotNone(frame1)
483+
self.assertEqual(
484+
frame1.GetFunctionName(),
485+
"process_data",
486+
"Second frame should be process_data",
487+
)
488+
self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic")
489+
490+
frame2 = thread.GetFrameAtIndex(2)
491+
self.assertIsNotNone(frame2)
492+
self.assertEqual(
493+
frame2.GetFunctionName(), "main", "Third frame should be main"
494+
)
495+
self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
496+
497+
# Verify line entry information is present
498+
line_entry0 = frame0.GetLineEntry()
499+
self.assertTrue(
500+
line_entry0.IsValid(), "Frame 0 should have a valid line entry"
501+
)
502+
self.assertEqual(
503+
line_entry0.GetLine(), 7, "Frame 0 should point to line 7"
504+
)
505+
file_spec0 = line_entry0.GetFileSpec()
506+
self.assertTrue(file_spec0.IsValid(), "Frame 0 should have valid file spec")
507+
self.assertEqual(
508+
file_spec0.GetFilename(),
509+
"python_helper.py",
510+
"Frame 0 should point to python_helper.py",
511+
)
512+
513+
line_entry1 = frame1.GetLineEntry()
514+
self.assertTrue(
515+
line_entry1.IsValid(), "Frame 1 should have a valid line entry"
516+
)
517+
self.assertEqual(
518+
line_entry1.GetLine(), 16, "Frame 1 should point to line 16"
519+
)
520+
521+
line_entry2 = frame2.GetLineEntry()
522+
self.assertTrue(
523+
line_entry2.IsValid(), "Frame 2 should have a valid line entry"
524+
)
525+
self.assertEqual(
526+
line_entry2.GetLine(), 27, "Frame 2 should point to line 27"
527+
)
528+
529+
# Verify the frames display properly in backtrace
530+
# This tests that PC-less frames don't show 0xffffffffffffffff
531+
self.runCmd("bt")
532+
output = self.res.GetOutput()
533+
534+
# Should show function names
535+
self.assertIn("compute_fibonacci", output)
536+
self.assertIn("process_data", output)
537+
self.assertIn("main", output)
538+
539+
# Should show Python file
540+
self.assertIn("python_helper.py", output)
541+
542+
# Should show line numbers
543+
self.assertIn(":7", output) # compute_fibonacci line
544+
self.assertIn(":16", output) # process_data line
545+
self.assertIn(":27", output) # main line
546+
547+
# Should NOT show invalid address (0xffffffffffffffff)
548+
self.assertNotIn("0xffffffffffffffff", output.lower())
549+
550+
# Verify frame 3 is the original real frame 0
551+
frame3 = thread.GetFrameAtIndex(3)
552+
self.assertIsNotNone(frame3)
553+
self.assertIn("thread_func", frame3.GetFunctionName())
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Sample Python module to demonstrate Python source display in scripted frames.
3+
"""
4+
5+
6+
def compute_fibonacci(n):
7+
"""Compute the nth Fibonacci number."""
8+
if n <= 1:
9+
return n
10+
a, b = 0, 1
11+
for _ in range(n - 1):
12+
a, b = b, a + b
13+
return b
14+
15+
16+
def process_data(data):
17+
"""Process some data and return result."""
18+
result = []
19+
for item in data:
20+
if isinstance(item, int):
21+
result.append(item * 2)
22+
elif isinstance(item, str):
23+
result.append(item.upper())
24+
return result
25+
26+
27+
def main():
28+
"""Main entry point for testing."""
29+
fib_10 = compute_fibonacci(10)
30+
data = [1, 2, "hello", 3, "world"]
31+
processed = process_data(data)
32+
return fib_10, processed
33+
34+
35+
if __name__ == "__main__":
36+
main()

lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
index to create stackframes
1111
"""
1212

13+
import os
1314
import lldb
1415
from lldb.plugins.scripted_process import ScriptedFrame
1516
from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
@@ -220,3 +221,98 @@ def get_frame_at_index(self, index):
220221
# Pass through original frames at indices 1, 2, 3, ...
221222
return index - 1
222223
return None
224+
225+
226+
class PythonSourceFrame(ScriptedFrame):
227+
"""Scripted frame that points to Python source code."""
228+
229+
def __init__(self, thread, idx, function_name, python_file, line_number):
230+
args = lldb.SBStructuredData()
231+
super().__init__(thread, args)
232+
233+
self.idx = idx
234+
self.function_name = function_name
235+
self.python_file = python_file
236+
self.line_number = line_number
237+
238+
def get_id(self):
239+
"""Return the frame index."""
240+
return self.idx
241+
242+
def get_pc(self):
243+
"""PC-less frame - return invalid address."""
244+
return lldb.LLDB_INVALID_ADDRESS
245+
246+
def get_function_name(self):
247+
"""Return the function name."""
248+
return self.function_name
249+
250+
def get_symbol_context(self):
251+
"""Return a symbol context with LineEntry pointing to Python source."""
252+
# Create a LineEntry pointing to the Python source file
253+
line_entry = lldb.SBLineEntry()
254+
line_entry.SetFileSpec(lldb.SBFileSpec(self.python_file, True))
255+
line_entry.SetLine(self.line_number)
256+
line_entry.SetColumn(0)
257+
258+
# Create a symbol context with the line entry
259+
sym_ctx = lldb.SBSymbolContext()
260+
sym_ctx.SetLineEntry(line_entry)
261+
262+
return sym_ctx
263+
264+
def is_artificial(self):
265+
"""Not artificial."""
266+
return False
267+
268+
def is_hidden(self):
269+
"""Not hidden."""
270+
return False
271+
272+
def get_register_context(self):
273+
"""No register context for PC-less frames."""
274+
return None
275+
276+
277+
class PythonSourceFrameProvider(ScriptedFrameProvider):
278+
"""
279+
Provider that demonstrates Python source display in scripted frames.
280+
281+
This provider prepends frames pointing to Python source code, showing
282+
that PC-less frames can display Python source files with proper line
283+
numbers and module/compile unit information.
284+
"""
285+
286+
def __init__(self, input_frames, args):
287+
super().__init__(input_frames, args)
288+
289+
# Find the python_helper.py file
290+
current_dir = os.path.dirname(os.path.abspath(__file__))
291+
self.python_file = os.path.join(current_dir, "python_helper.py")
292+
293+
@staticmethod
294+
def get_description():
295+
"""Return a description of this provider."""
296+
return "Provider that prepends frames pointing to Python source"
297+
298+
def get_frame_at_index(self, index):
299+
"""Return Python source frames followed by original frames."""
300+
if index == 0:
301+
# Frame pointing to compute_fibonacci function (line 7)
302+
return PythonSourceFrame(
303+
self.thread, 0, "compute_fibonacci", self.python_file, 7
304+
)
305+
elif index == 1:
306+
# Frame pointing to process_data function (line 16)
307+
return PythonSourceFrame(
308+
self.thread, 1, "process_data", self.python_file, 16
309+
)
310+
elif index == 2:
311+
# Frame pointing to main function (line 27)
312+
return PythonSourceFrame(
313+
self.thread, 2, "main", self.python_file, 27
314+
)
315+
elif index - 3 < len(self.input_frames):
316+
# Pass through original frames
317+
return index - 3
318+
return None

0 commit comments

Comments
 (0)