Skip to content

Commit 2846e25

Browse files
committed
fixup! test
1 parent 89695bf commit 2846e25

1 file changed

Lines changed: 168 additions & 38 deletions

File tree

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

Lines changed: 168 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
try:
1313
import _remote_debugging # noqa: F401
14+
from profiling.sampling import gecko_collector
1415
from profiling.sampling.pstats_collector import PstatsCollector
1516
from profiling.sampling.stack_collector import (
1617
CollapsedStackCollector,
@@ -59,6 +60,42 @@ def find_child_by_name(children, strings, substr):
5960
return None
6061

6162

63+
def export_gecko_profile(testcase, collector):
64+
gecko_out = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
65+
testcase.addCleanup(close_and_unlink, gecko_out)
66+
# We cannot overwrite an open file on Windows.
67+
gecko_out.close()
68+
69+
with captured_stdout(), captured_stderr():
70+
collector.export(gecko_out.name)
71+
72+
testcase.assertGreater(os.path.getsize(gecko_out.name), 0)
73+
with open(gecko_out.name, encoding="utf-8") as file:
74+
return json.load(file)
75+
76+
77+
def assert_gecko_column_lengths(testcase, table, columns):
78+
expected = table["length"]
79+
for column in columns:
80+
testcase.assertEqual(
81+
len(table[column]), expected,
82+
f"{column!r} has wrong length",
83+
)
84+
85+
86+
def gecko_marker_names(profile, markers):
87+
string_array = profile["shared"]["stringArray"]
88+
return [string_array[idx] for idx in markers["name"]]
89+
90+
91+
def gecko_opcode_marker_data(profile):
92+
markers = profile["threads"][0]["markers"]
93+
return [
94+
data for data in markers["data"]
95+
if data.get("type") == "Opcode"
96+
]
97+
98+
6299
class TestSampleProfilerComponents(unittest.TestCase):
63100
"""Unit tests for individual profiler components."""
64101

@@ -583,9 +620,10 @@ def test_gecko_collector_basic(self):
583620

584621
# Verify samples
585622
samples = thread_data["samples"]
586-
self.assertEqual(len(samples["stack"]), 1)
587-
self.assertEqual(len(samples["time"]), 1)
588623
self.assertEqual(samples["length"], 1)
624+
assert_gecko_column_lengths(
625+
self, samples, ("stack", "time", "eventDelay")
626+
)
589627

590628
# Verify function table structure and content
591629
func_table = thread_data["funcTable"]
@@ -622,11 +660,6 @@ def test_gecko_collector_basic(self):
622660
@unittest.skipIf(is_emscripten, "threads not available")
623661
def test_gecko_collector_export(self):
624662
"""Test Gecko profile export functionality."""
625-
gecko_out = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
626-
self.addCleanup(close_and_unlink, gecko_out)
627-
# We cannot overwrite an open file on Windows.
628-
gecko_out.close()
629-
630663
collector = GeckoCollector(1000)
631664

632665
test_frames1 = [
@@ -659,17 +692,7 @@ def test_gecko_collector_export(self):
659692
collector.collect(test_frames2)
660693
collector.collect(test_frames3)
661694

662-
# Export gecko profile
663-
with captured_stdout(), captured_stderr():
664-
collector.export(gecko_out.name)
665-
666-
# Verify file was created and contains valid data
667-
self.assertTrue(os.path.exists(gecko_out.name))
668-
self.assertGreater(os.path.getsize(gecko_out.name), 0)
669-
670-
# Check file contains valid JSON
671-
with open(gecko_out.name, "r") as f:
672-
profile_data = json.load(f)
695+
profile_data = export_gecko_profile(self, collector)
673696

674697
# Should be valid Gecko profile format
675698
self.assertIn("meta", profile_data)
@@ -690,6 +713,100 @@ def test_gecko_collector_export(self):
690713
self.assertIn("func2", string_array)
691714
self.assertIn("other_func", string_array)
692715

716+
thread_data = profile_data["threads"][0]
717+
assert_gecko_column_lengths(
718+
self, thread_data["samples"], ("stack", "time", "eventDelay")
719+
)
720+
721+
@unittest.skipIf(is_emscripten, "threads not available")
722+
def test_gecko_collector_export_after_spill_flush(self):
723+
"""Test Gecko profile export after spill buffers flush to disk."""
724+
old_buffer_bytes = gecko_collector.DEFAULT_SPILL_BUFFER_BYTES
725+
gecko_collector.DEFAULT_SPILL_BUFFER_BYTES = 1
726+
self.addCleanup(
727+
setattr, gecko_collector, "DEFAULT_SPILL_BUFFER_BYTES",
728+
old_buffer_bytes
729+
)
730+
731+
collector = GeckoCollector(1000)
732+
test_frames = [
733+
MockInterpreterInfo(
734+
0,
735+
[
736+
MockThreadInfo(
737+
1,
738+
[MockFrameInfo("file.py", 10, "func")],
739+
status=THREAD_STATUS_HAS_GIL,
740+
)
741+
],
742+
)
743+
]
744+
collector.collect(test_frames, timestamps_us=[1000, 2000, 3000])
745+
746+
profile_data = export_gecko_profile(self, collector)
747+
samples = profile_data["threads"][0]["samples"]
748+
self.assertEqual(samples["length"], 3)
749+
assert_gecko_column_lengths(
750+
self, samples, ("stack", "time", "eventDelay")
751+
)
752+
753+
@unittest.skipIf(is_emscripten, "threads not available")
754+
def test_gecko_collector_rejects_collect_after_export(self):
755+
collector = GeckoCollector(1000)
756+
test_frames = [
757+
MockInterpreterInfo(
758+
0,
759+
[
760+
MockThreadInfo(
761+
1,
762+
[MockFrameInfo("file.py", 10, "func")],
763+
status=THREAD_STATUS_HAS_GIL,
764+
)
765+
],
766+
)
767+
]
768+
collector.collect(test_frames)
769+
export_gecko_profile(self, collector)
770+
771+
with self.assertRaisesRegex(RuntimeError, "after export"):
772+
collector.collect(test_frames)
773+
774+
@unittest.skipIf(is_emscripten, "threads not available")
775+
def test_gecko_collector_export_failure_keeps_existing_file(self):
776+
collector = GeckoCollector(1000)
777+
test_frames = [
778+
MockInterpreterInfo(
779+
0,
780+
[
781+
MockThreadInfo(
782+
1,
783+
[MockFrameInfo("file.py", 10, "func")],
784+
status=THREAD_STATUS_HAS_GIL,
785+
)
786+
],
787+
)
788+
]
789+
collector.collect(test_frames)
790+
791+
with tempfile.TemporaryDirectory() as temp_dir:
792+
filename = os.path.join(temp_dir, "profile.json")
793+
with open(filename, "w", encoding="utf-8") as file:
794+
file.write("existing")
795+
796+
before = set(os.listdir(temp_dir))
797+
798+
def fail(file):
799+
raise OSError("boom")
800+
801+
collector._stream_profile = fail
802+
with captured_stdout(), captured_stderr():
803+
with self.assertRaisesRegex(OSError, "boom"):
804+
collector.export(filename)
805+
806+
with open(filename, encoding="utf-8") as file:
807+
self.assertEqual(file.read(), "existing")
808+
self.assertEqual(set(os.listdir(temp_dir)), before)
809+
693810
def test_gecko_collector_markers(self):
694811
"""Test Gecko profile markers for GIL and CPU state tracking."""
695812
collector = GeckoCollector(1000)
@@ -773,21 +890,16 @@ def test_gecko_collector_markers(self):
773890
self.assertIn("markers", thread_data)
774891
markers = thread_data["markers"]
775892

776-
# Should have marker arrays
777-
self.assertIn("name", markers)
778-
self.assertIn("startTime", markers)
779-
self.assertIn("endTime", markers)
780-
self.assertIn("category", markers)
781893
self.assertGreater(
782894
markers["length"], 0, "Should have generated markers"
783895
)
784-
785-
# Get marker names from string table
786-
string_array = profile_data["shared"]["stringArray"]
787-
marker_names = [string_array[idx] for idx in markers["name"]]
896+
assert_gecko_column_lengths(
897+
self, markers,
898+
("data", "name", "startTime", "endTime", "phase", "category"),
899+
)
788900

789901
# Verify we have different marker types
790-
marker_name_set = set(marker_names)
902+
marker_name_set = set(gecko_marker_names(profile_data, markers))
791903

792904
# Should have "Has GIL" markers (when thread had GIL)
793905
self.assertIn(
@@ -2661,7 +2773,7 @@ def test_gecko_collector_opcodes_enabled(self):
26612773
def test_gecko_opcode_state_tracking(self):
26622774
"""Test that GeckoCollector tracks opcode state changes."""
26632775
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
2664-
self.addCleanup(lambda: collector.spill_dir.cleanup())
2776+
self.addCleanup(collector._cleanup_spills)
26652777

26662778
# First sample with opcode 90 (RAISE_VARARGS)
26672779
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
@@ -2683,7 +2795,6 @@ def test_gecko_opcode_state_tracking(self):
26832795
def test_gecko_opcode_state_change_emits_marker(self):
26842796
"""Test that opcode state change emits an interval marker."""
26852797
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
2686-
self.addCleanup(lambda: collector.spill_dir.cleanup())
26872798

26882799
# First sample: opcode 90
26892800
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
@@ -2706,12 +2817,32 @@ def test_gecko_opcode_state_change_emits_marker(self):
27062817
collector.collect(frames2)
27072818

27082819
# Should have emitted a marker for the first opcode
2709-
self.assertGreater(collector.thread_spills[1].marker_count, 0)
2820+
profile = collector._build_profile()
2821+
markers = profile["threads"][0]["markers"]
2822+
assert_gecko_column_lengths(
2823+
self, markers,
2824+
("data", "name", "startTime", "endTime", "phase", "category"),
2825+
)
2826+
opcode_markers = gecko_opcode_marker_data(profile)
2827+
self.assertIn(
2828+
{
2829+
"opcode": 90,
2830+
"line": 10,
2831+
"function": "func",
2832+
},
2833+
[
2834+
{
2835+
"opcode": marker["opcode"],
2836+
"line": marker["line"],
2837+
"function": marker["function"],
2838+
}
2839+
for marker in opcode_markers
2840+
],
2841+
)
27102842

27112843
def test_gecko_opcode_markers_not_emitted_when_disabled(self):
27122844
"""Test that no opcode markers when opcodes=False."""
27132845
collector = GeckoCollector(sample_interval_usec=1000, opcodes=False)
2714-
self.addCleanup(lambda: collector.spill_dir.cleanup())
27152846

27162847
frame1 = MockFrameInfo("test.py", 10, "func", opcode=90)
27172848
frames1 = [
@@ -2731,13 +2862,13 @@ def test_gecko_opcode_markers_not_emitted_when_disabled(self):
27312862
]
27322863
collector.collect(frames2)
27332864

2734-
# opcode_state should not be tracked
2735-
self.assertEqual(len(collector.opcode_state), 0)
2865+
profile = collector._build_profile()
2866+
self.assertEqual(gecko_opcode_marker_data(profile), [])
2867+
self.assertEqual(profile["meta"]["markerSchema"], [])
27362868

27372869
def test_gecko_opcode_with_none_opcode(self):
27382870
"""Test that None opcode doesn't cause issues."""
27392871
collector = GeckoCollector(sample_interval_usec=1000, opcodes=True)
2740-
self.addCleanup(lambda: collector.spill_dir.cleanup())
27412872

27422873
# Frame with no opcode (None)
27432874
frame = MockFrameInfo("test.py", 10, "func", opcode=None)
@@ -2749,9 +2880,8 @@ def test_gecko_opcode_with_none_opcode(self):
27492880
]
27502881
collector.collect(frames)
27512882

2752-
# Should track the state but opcode is None
2753-
self.assertIn(1, collector.opcode_state)
2754-
self.assertIsNone(collector.opcode_state[1][0])
2883+
profile = collector._build_profile()
2884+
self.assertEqual(gecko_opcode_marker_data(profile), [])
27552885

27562886

27572887
class TestCollectorFrameFormat(unittest.TestCase):

0 commit comments

Comments
 (0)