mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
Update tests for location tuple and opcode field
Frame location is now a 4-tuple (lineno, end_lineno, col_offset, end_col_offset). MockFrameInfo wraps locations in LocationInfo struct. Updates assertions throughout and adds opcode_utils coverage.
This commit is contained in:
parent
7ffe4cb39e
commit
8b423df632
6 changed files with 924 additions and 160 deletions
|
|
@ -4,8 +4,12 @@
|
|||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
# Matches the C structseq LocationInfo from _remote_debugging
|
||||
LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'])
|
||||
|
||||
from profiling.sampling.heatmap_collector import (
|
||||
HeatmapCollector,
|
||||
get_python_path_info,
|
||||
|
|
@ -220,7 +224,7 @@ def test_process_frames_increments_total_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
initial_count = collector._total_samples
|
||||
frames = [('file.py', 10, 'func')]
|
||||
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertEqual(collector._total_samples, initial_count + 1)
|
||||
|
|
@ -229,7 +233,7 @@ def test_process_frames_records_line_samples(self):
|
|||
"""Test that process_frames records line samples."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 5, 'test_func')]
|
||||
frames = [('test.py', (5, 5, -1, -1), 'test_func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
# Check that line was recorded
|
||||
|
|
@ -241,9 +245,9 @@ def test_process_frames_records_multiple_lines_in_stack(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('file1.py', 10, 'func1'),
|
||||
('file2.py', 20, 'func2'),
|
||||
('file3.py', 30, 'func3')
|
||||
('file1.py', (10, 10, -1, -1), 'func1', None),
|
||||
('file2.py', (20, 20, -1, -1), 'func2', None),
|
||||
('file3.py', (30, 30, -1, -1), 'func3', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -257,8 +261,8 @@ def test_process_frames_distinguishes_self_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('leaf.py', 5, 'leaf_func'), # This is the leaf (top of stack)
|
||||
('caller.py', 10, 'caller_func')
|
||||
('leaf.py', (5, 5, -1, -1), 'leaf_func', None), # This is the leaf (top of stack)
|
||||
('caller.py', (10, 10, -1, -1), 'caller_func', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -273,7 +277,7 @@ def test_process_frames_accumulates_samples(self):
|
|||
"""Test that multiple calls accumulate samples."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('file.py', 10, 'func')]
|
||||
frames = [('file.py', (10, 10, -1, -1), 'func', None)]
|
||||
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
|
@ -288,11 +292,11 @@ def test_process_frames_ignores_invalid_frames(self):
|
|||
|
||||
# These should be ignored
|
||||
invalid_frames = [
|
||||
('<string>', 1, 'test'),
|
||||
('[eval]', 1, 'test'),
|
||||
('', 1, 'test'),
|
||||
(None, 1, 'test'),
|
||||
('__init__', 0, 'test'), # Special invalid frame
|
||||
('<string>', (1, 1, -1, -1), 'test', None),
|
||||
('[eval]', (1, 1, -1, -1), 'test', None),
|
||||
('', (1, 1, -1, -1), 'test', None),
|
||||
(None, (1, 1, -1, -1), 'test', None),
|
||||
('__init__', (0, 0, -1, -1), 'test', None), # Special invalid frame
|
||||
]
|
||||
|
||||
for frame in invalid_frames:
|
||||
|
|
@ -301,15 +305,15 @@ def test_process_frames_ignores_invalid_frames(self):
|
|||
# Should not record these invalid frames
|
||||
for frame in invalid_frames:
|
||||
if frame[0]:
|
||||
self.assertNotIn((frame[0], frame[1]), collector.line_samples)
|
||||
self.assertNotIn((frame[0], frame[1][0]), collector.line_samples)
|
||||
|
||||
def test_process_frames_builds_call_graph(self):
|
||||
"""Test that process_frames builds call graph relationships."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('callee.py', 5, 'callee_func'),
|
||||
('caller.py', 10, 'caller_func')
|
||||
('callee.py', (5, 5, -1, -1), 'callee_func', None),
|
||||
('caller.py', (10, 10, -1, -1), 'caller_func', None)
|
||||
]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
|
|
@ -325,7 +329,7 @@ def test_process_frames_records_function_definitions(self):
|
|||
"""Test that process_frames records function definition locations."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('module.py', 42, 'my_function')]
|
||||
frames = [('module.py', (42, 42, -1, -1), 'my_function', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertIn(('module.py', 'my_function'), collector.function_definitions)
|
||||
|
|
@ -336,8 +340,8 @@ def test_process_frames_tracks_edge_samples(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [
|
||||
('callee.py', 5, 'callee'),
|
||||
('caller.py', 10, 'caller')
|
||||
('callee.py', (5, 5, -1, -1), 'callee', None),
|
||||
('caller.py', (10, 10, -1, -1), 'caller', None)
|
||||
]
|
||||
|
||||
# Process same call stack multiple times
|
||||
|
|
@ -361,7 +365,7 @@ def test_process_frames_with_file_samples_dict(self):
|
|||
"""Test that file_samples dict is properly populated."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
self.assertIn('test.py', collector.file_samples)
|
||||
|
|
@ -382,7 +386,7 @@ def test_export_creates_output_directory(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Add some data
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -397,7 +401,7 @@ def test_export_creates_index_html(self):
|
|||
"""Test that export creates index.html."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -412,7 +416,7 @@ def test_export_creates_file_htmls(self):
|
|||
"""Test that export creates individual file HTMLs."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'heatmap_output')
|
||||
|
|
@ -439,7 +443,7 @@ def test_export_handles_html_suffix(self):
|
|||
"""Test that export handles .html suffix in output path."""
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
frames = [('test.py', 10, 'func')]
|
||||
frames = [('test.py', (10, 10, -1, -1), 'func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
# Path with .html suffix should be stripped
|
||||
|
|
@ -457,9 +461,9 @@ def test_export_with_multiple_files(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Add samples for multiple files
|
||||
collector.process_frames([('file1.py', 10, 'func1')], thread_id=1)
|
||||
collector.process_frames([('file2.py', 20, 'func2')], thread_id=1)
|
||||
collector.process_frames([('file3.py', 30, 'func3')], thread_id=1)
|
||||
collector.process_frames([('file1.py', (10, 10, -1, -1), 'func1', None)], thread_id=1)
|
||||
collector.process_frames([('file2.py', (20, 20, -1, -1), 'func2', None)], thread_id=1)
|
||||
collector.process_frames([('file3.py', (30, 30, -1, -1), 'func3', None)], thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'multi_file')
|
||||
|
||||
|
|
@ -476,7 +480,7 @@ def test_export_index_contains_file_references(self):
|
|||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
collector.set_stats(sample_interval_usec=100, duration_sec=1.0, sample_rate=100.0)
|
||||
|
||||
frames = [('mytest.py', 10, 'my_func')]
|
||||
frames = [('mytest.py', (10, 10, -1, -1), 'my_func', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'test_output')
|
||||
|
|
@ -500,7 +504,7 @@ def test_export_file_html_has_line_numbers(self):
|
|||
with open(temp_file, 'w') as f:
|
||||
f.write('def test():\n pass\n')
|
||||
|
||||
frames = [(temp_file, 1, 'test')]
|
||||
frames = [(temp_file, (1, 1, -1, -1), 'test', None)]
|
||||
collector.process_frames(frames, thread_id=1)
|
||||
|
||||
output_path = os.path.join(self.test_dir, 'line_test')
|
||||
|
|
@ -521,23 +525,39 @@ def test_export_file_html_has_line_numbers(self):
|
|||
|
||||
|
||||
class MockFrameInfo:
|
||||
"""Mock FrameInfo for testing since the real one isn't accessible."""
|
||||
"""Mock FrameInfo for testing.
|
||||
|
||||
def __init__(self, filename, lineno, funcname):
|
||||
Frame format: (filename, location, funcname, opcode) where:
|
||||
- location is a tuple (lineno, end_lineno, col_offset, end_col_offset)
|
||||
- opcode is an int or None
|
||||
"""
|
||||
|
||||
def __init__(self, filename, lineno, funcname, opcode=None):
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.funcname = funcname
|
||||
self.opcode = opcode
|
||||
self.location = (lineno, lineno, -1, -1)
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.filename, self.location, self.funcname, self.opcode))
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.filename, self.location, self.funcname, self.opcode)[index]
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')"
|
||||
return f"MockFrameInfo('{self.filename}', {self.location}, '{self.funcname}', {self.opcode})"
|
||||
|
||||
|
||||
class MockThreadInfo:
|
||||
"""Mock ThreadInfo for testing since the real one isn't accessible."""
|
||||
|
||||
def __init__(self, thread_id, frame_info):
|
||||
def __init__(self, thread_id, frame_info, status=0):
|
||||
self.thread_id = thread_id
|
||||
self.frame_info = frame_info
|
||||
self.status = status # Thread status flags
|
||||
|
||||
def __repr__(self):
|
||||
return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})"
|
||||
|
|
@ -565,13 +585,13 @@ def test_heatmap_collector_basic(self):
|
|||
self.assertEqual(len(collector.file_samples), 0)
|
||||
self.assertEqual(len(collector.line_samples), 0)
|
||||
|
||||
# Test collecting sample data
|
||||
# Test collecting sample data - frames are 4-tuples: (filename, location, funcname, opcode)
|
||||
test_frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(
|
||||
1,
|
||||
[("file.py", 10, "func1"), ("file.py", 20, "func2")],
|
||||
[MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
|
||||
)]
|
||||
)
|
||||
]
|
||||
|
|
@ -592,21 +612,21 @@ def test_heatmap_collector_export(self):
|
|||
|
||||
collector = HeatmapCollector(sample_interval_usec=100)
|
||||
|
||||
# Create test data with multiple files
|
||||
# Create test data with multiple files using MockFrameInfo
|
||||
test_frames1 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
|
||||
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
|
||||
)
|
||||
]
|
||||
test_frames2 = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])],
|
||||
[MockThreadInfo(1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")])],
|
||||
)
|
||||
] # Same stack
|
||||
test_frames3 = [
|
||||
MockInterpreterInfo(0, [MockThreadInfo(1, [("other.py", 5, "other_func")])])
|
||||
MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])])
|
||||
]
|
||||
|
||||
collector.collect(test_frames1)
|
||||
|
|
@ -649,5 +669,95 @@ def test_heatmap_collector_export(self):
|
|||
self.assertIn("nav-btn", file_content)
|
||||
|
||||
|
||||
class TestHeatmapCollectorLocation(unittest.TestCase):
|
||||
"""Tests for HeatmapCollector location handling."""
|
||||
|
||||
def test_heatmap_with_full_location_info(self):
|
||||
"""Test HeatmapCollector uses full location tuple."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
# Frame with full location: (lineno, end_lineno, col_offset, end_col_offset)
|
||||
frame = MockFrameInfo("test.py", 10, "func")
|
||||
# Override with full location info
|
||||
frame.location = LocationInfo(10, 15, 4, 20)
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Verify data was collected with location info
|
||||
# HeatmapCollector uses file_samples dict with filename -> Counter of linenos
|
||||
self.assertIn("test.py", collector.file_samples)
|
||||
# Line 10 should have samples
|
||||
self.assertIn(10, collector.file_samples["test.py"])
|
||||
|
||||
def test_heatmap_with_none_location(self):
|
||||
"""Test HeatmapCollector handles None location gracefully."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
# Synthetic frame with None location
|
||||
frame = MockFrameInfo("~", 0, "<native>")
|
||||
frame.location = None
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
# Should not raise
|
||||
collector.collect(frames)
|
||||
|
||||
def test_heatmap_export_with_location_data(self):
|
||||
"""Test HeatmapCollector export includes location info."""
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, tmp_dir)
|
||||
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
frame = MockFrameInfo("test.py", 10, "process")
|
||||
frame.location = LocationInfo(10, 12, 0, 30)
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[MockThreadInfo(1, [frame])]
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Export should work
|
||||
with (captured_stdout(), captured_stderr()):
|
||||
collector.export(tmp_dir)
|
||||
self.assertTrue(os.path.exists(os.path.join(tmp_dir, "index.html")))
|
||||
|
||||
def test_heatmap_collector_frame_format(self):
|
||||
"""Test HeatmapCollector with 4-element frame format."""
|
||||
collector = HeatmapCollector(sample_interval_usec=1000)
|
||||
|
||||
frames = [
|
||||
MockInterpreterInfo(
|
||||
0,
|
||||
[
|
||||
MockThreadInfo(
|
||||
1,
|
||||
[
|
||||
MockFrameInfo("app.py", 100, "main", opcode=90),
|
||||
MockFrameInfo("utils.py", 50, "helper", opcode=100),
|
||||
MockFrameInfo("lib.py", 25, "process", opcode=None),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
collector.collect(frames)
|
||||
|
||||
# Should have recorded data for the files
|
||||
self.assertIn("app.py", collector.file_samples)
|
||||
self.assertIn("utils.py", collector.file_samples)
|
||||
self.assertIn("lib.py", collector.file_samples)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue