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()