Files
cpython/Lib/test/test_pystats.py
Neil Schemenauer c98c5b3449 gh-131253: free-threaded build support for pystats (gh-137189)
Allow the --enable-pystats build option to be used with free-threading.  The
stats are now stored on a per-interpreter basis, rather than process global.
For free-threaded builds, the stats structure is allocated per-thread and
then periodically merged into the per-interpreter stats structure (on thread
exit or when the reporting function is called). Most of the pystats related
code has be moved into the file Python/pystats.c.
2025-11-03 11:36:37 -08:00

216 lines
5.8 KiB
Python

import sys
import textwrap
import unittest
from test.support import script_helper
# This function is available for the --enable-pystats config.
HAVE_PYSTATS = hasattr(sys, '_stats_on')
TEST_TEMPLATE = """
import sys
import threading
import time
THREADS = 2
class A:
pass
class B:
pass
def modify_class():
# This is used as a rare event we can assume doesn't happen unless we do it.
# It increments the "Rare event (set_class)" count.
a = A()
a.__class__ = B
TURNED_ON = False
def stats_on():
global TURNED_ON
sys._stats_on()
TURNED_ON = True
TURNED_OFF = False
def stats_off():
global TURNED_OFF
sys._stats_off()
TURNED_OFF = True
CLEARED = False
def stats_clear():
global CLEARED
sys._stats_clear()
CLEARED = True
def func_start():
pass
def func_end():
pass
def func_test(thread_id):
pass
_TEST_CODE_
func_start()
threads = []
for i in range(THREADS):
t = threading.Thread(target=func_test, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
func_end()
"""
def run_test_code(
test_code,
args=[],
env_vars=None,
):
"""Run test code and return the value of the "set_class" stats counter.
"""
code = textwrap.dedent(TEST_TEMPLATE)
code = code.replace('_TEST_CODE_', textwrap.dedent(test_code))
script_args = args + ['-c', code]
env_vars = env_vars or {}
res, _ = script_helper.run_python_until_end(*script_args, **env_vars)
stderr = res.err.decode("ascii", "backslashreplace")
for line in stderr.split('\n'):
if 'Rare event (set_class)' in line:
label, _, value = line.partition(':')
return value.strip()
return ''
@unittest.skipUnless(HAVE_PYSTATS, "requires pystats build option")
class TestPyStats(unittest.TestCase):
"""Tests for pystats functionality (requires --enable-pystats build
option).
"""
def test_stats_toggle_on(self):
"""Check the toggle on functionality.
"""
code = """
def func_start():
modify_class()
"""
# If turned on with command line flag, should get one count.
stat_count = run_test_code(code, args=['-X', 'pystats'])
self.assertEqual(stat_count, '1')
# If turned on with env var, should get one count.
stat_count = run_test_code(code, env_vars={'PYTHONSTATS': '1'})
self.assertEqual(stat_count, '1')
# If not turned on, should be no counts.
stat_count = run_test_code(code)
self.assertEqual(stat_count, '')
code = """
def func_start():
modify_class()
sys._stats_on()
modify_class()
"""
# Not initially turned on but enabled by sys._stats_on(), should get
# one count.
stat_count = run_test_code(code)
self.assertEqual(stat_count, '1')
def test_stats_toggle_on_thread(self):
"""Check the toggle on functionality when threads are used.
"""
code = """
def func_test(thread_id):
if thread_id == 0:
modify_class()
stats_on()
modify_class()
else:
while not TURNED_ON:
pass
modify_class()
"""
# Turning on in one thread will count in other thread.
stat_count = run_test_code(code)
self.assertEqual(stat_count, '2')
code = """
def func_test(thread_id):
if thread_id == 0:
modify_class()
stats_off()
modify_class()
else:
while not TURNED_OFF:
pass
modify_class()
"""
# Turning off in one thread will not count in other threads.
stat_count = run_test_code(code, args=['-X', 'pystats'])
self.assertEqual(stat_count, '1')
def test_thread_exit_merge(self):
"""Check that per-thread stats (when free-threading enabled) are merged.
"""
code = """
def func_test(thread_id):
modify_class()
if thread_id == 0:
raise SystemExit
"""
# Stats from a thread exiting early should still be counted.
stat_count = run_test_code(code, args=['-X', 'pystats'])
self.assertEqual(stat_count, '2')
def test_stats_dump(self):
"""Check that sys._stats_dump() works.
"""
code = """
def func_test(thread_id):
if thread_id == 0:
stats_on()
else:
while not TURNED_ON:
pass
modify_class()
sys._stats_dump()
stats_off()
"""
# Stats from a thread exiting early should still be counted.
stat_count = run_test_code(code)
self.assertEqual(stat_count, '1')
def test_stats_clear(self):
"""Check that sys._stats_clear() works.
"""
code = """
ready = False
def func_test(thread_id):
global ready
if thread_id == 0:
stats_on()
modify_class()
while not ready:
pass # wait until other thread has called modify_class()
stats_clear() # clears stats for all threads
else:
while not TURNED_ON:
pass
modify_class()
ready = True
"""
# Clearing stats will clear for all threads
stat_count = run_test_code(code)
self.assertEqual(stat_count, '0')
if __name__ == "__main__":
unittest.main()