| # It's most useful to run these tests with ThreadSanitizer enabled. | |
| import sys | |
| import functools | |
| import threading | |
| import time | |
| import unittest | |
| import _testinternalcapi | |
| import warnings | |
| from test.support import threading_helper | |
| class TestBase(unittest.TestCase): | |
| pass | |
| def do_race(func1, func2): | |
| """Run func1() and func2() repeatedly in separate threads.""" | |
| n = 1000 | |
| barrier = threading.Barrier(2) | |
| def repeat(func): | |
| barrier.wait() | |
| for _i in range(n): | |
| func() | |
| threads = [ | |
| threading.Thread(target=functools.partial(repeat, func1)), | |
| threading.Thread(target=functools.partial(repeat, func2)), | |
| ] | |
| for thread in threads: | |
| thread.start() | |
| for thread in threads: | |
| thread.join() | |
| @threading_helper.requires_working_threading() | |
| class TestRaces(TestBase): | |
| def test_racing_cell_set(self): | |
| """Test cell object gettr/settr properties.""" | |
| def nested_func(): | |
| x = 0 | |
| def inner(): | |
| nonlocal x | |
| x += 1 | |
| # This doesn't race because LOAD_DEREF and STORE_DEREF on the | |
| # cell object use critical sections. | |
| do_race(nested_func, nested_func) | |
| def nested_func2(): | |
| x = 0 | |
| def inner(): | |
| y = x | |
| frame = sys._getframe(1) | |
| frame.f_locals["x"] = 2 | |
| return inner | |
| def mutate_func2(): | |
| inner = nested_func2() | |
| cell = inner.__closure__[0] | |
| old_value = cell.cell_contents | |
| cell.cell_contents = 1000 | |
| time.sleep(0) | |
| cell.cell_contents = old_value | |
| time.sleep(0) | |
| # This revealed a race with cell_set_contents() since it was missing | |
| # the critical section. | |
| do_race(nested_func2, mutate_func2) | |
| def test_racing_cell_cmp_repr(self): | |
| """Test cell object compare and repr methods.""" | |
| def nested_func(): | |
| x = 0 | |
| y = 0 | |
| def inner(): | |
| return x + y | |
| return inner.__closure__ | |
| cell_a, cell_b = nested_func() | |
| def mutate(): | |
| cell_a.cell_contents += 1 | |
| def access(): | |
| cell_a == cell_b | |
| s = repr(cell_a) | |
| # cell_richcompare() and cell_repr used to have data races | |
| do_race(mutate, access) | |
| def test_racing_load_super_attr(self): | |
| """Test (un)specialization of LOAD_SUPER_ATTR opcode.""" | |
| class C: | |
| def __init__(self): | |
| try: | |
| super().__init__ | |
| super().__init__() | |
| except RuntimeError: | |
| pass # happens if __class__ is replaced with non-type | |
| def access(): | |
| C() | |
| def mutate(): | |
| # Swap out the super() global with a different one | |
| real_super = super | |
| globals()["super"] = lambda s=1: s | |
| time.sleep(0) | |
| globals()["super"] = real_super | |
| time.sleep(0) | |
| # Swap out the __class__ closure value with a non-type | |
| cell = C.__init__.__closure__[0] | |
| real_class = cell.cell_contents | |
| cell.cell_contents = 99 | |
| time.sleep(0) | |
| cell.cell_contents = real_class | |
| # The initial PR adding specialized opcodes for LOAD_SUPER_ATTR | |
| # had some races (one with the super() global changing and one | |
| # with the cell binding being changed). | |
| do_race(access, mutate) | |
| def test_racing_to_bool(self): | |
| seq = [1] | |
| class C: | |
| def __bool__(self): | |
| return False | |
| def access(): | |
| if seq: | |
| return 1 | |
| else: | |
| return 2 | |
| def mutate(): | |
| nonlocal seq | |
| seq = [1] | |
| time.sleep(0) | |
| seq = C() | |
| time.sleep(0) | |
| do_race(access, mutate) | |
| def test_racing_store_attr_slot(self): | |
| class C: | |
| __slots__ = ['x', '__dict__'] | |
| c = C() | |
| def set_slot(): | |
| for i in range(10): | |
| c.x = i | |
| time.sleep(0) | |
| def change_type(): | |
| def set_x(self, x): | |
| pass | |
| def get_x(self): | |
| pass | |
| C.x = property(get_x, set_x) | |
| time.sleep(0) | |
| del C.x | |
| time.sleep(0) | |
| do_race(set_slot, change_type) | |
| def set_getattribute(): | |
| C.__getattribute__ = lambda self, x: x | |
| time.sleep(0) | |
| del C.__getattribute__ | |
| time.sleep(0) | |
| do_race(set_slot, set_getattribute) | |
| def test_racing_store_attr_instance_value(self): | |
| class C: | |
| pass | |
| c = C() | |
| def set_value(): | |
| for i in range(100): | |
| c.x = i | |
| set_value() | |
| def read(): | |
| x = c.x | |
| def mutate(): | |
| # Adding a property for 'x' should unspecialize it. | |
| C.x = property(lambda self: None, lambda self, x: None) | |
| time.sleep(0) | |
| del C.x | |
| time.sleep(0) | |
| do_race(read, mutate) | |
| def test_racing_store_attr_with_hint(self): | |
| class C: | |
| pass | |
| c = C() | |
| for i in range(29): | |
| setattr(c, f"_{i}", None) | |
| def set_value(): | |
| for i in range(100): | |
| c.x = i | |
| set_value() | |
| def read(): | |
| x = c.x | |
| def mutate(): | |
| # Adding a property for 'x' should unspecialize it. | |
| C.x = property(lambda self: None, lambda self, x: None) | |
| time.sleep(0) | |
| del C.x | |
| time.sleep(0) | |
| do_race(read, mutate) | |
| def make_shared_key_dict(self): | |
| class C: | |
| pass | |
| a = C() | |
| a.x = 1 | |
| return a.__dict__ | |
| def test_racing_store_attr_dict(self): | |
| """Test STORE_ATTR with various dictionary types.""" | |
| class C: | |
| pass | |
| c = C() | |
| def set_value(): | |
| for i in range(20): | |
| c.x = i | |
| def mutate(): | |
| nonlocal c | |
| c.x = 1 | |
| self.assertTrue(_testinternalcapi.has_inline_values(c)) | |
| for i in range(30): | |
| setattr(c, f"_{i}", None) | |
| self.assertFalse(_testinternalcapi.has_inline_values(c.__dict__)) | |
| c.__dict__ = self.make_shared_key_dict() | |
| self.assertTrue(_testinternalcapi.has_split_table(c.__dict__)) | |
| c.__dict__[1] = None | |
| self.assertFalse(_testinternalcapi.has_split_table(c.__dict__)) | |
| c = C() | |
| do_race(set_value, mutate) | |
| def test_racing_recursion_limit(self): | |
| self.addCleanup(sys.setrecursionlimit, sys.getrecursionlimit()) | |
| def something_recursive(): | |
| def count(n): | |
| if n > 0: | |
| return count(n - 1) + 1 | |
| return 0 | |
| count(50) | |
| def set_recursion_limit(): | |
| for limit in range(100, 200): | |
| sys.setrecursionlimit(limit) | |
| do_race(something_recursive, set_recursion_limit) | |
| @threading_helper.requires_working_threading() | |
| class TestWarningsRaces(TestBase): | |
| def setUp(self): | |
| self.saved_filters = warnings.filters[:] | |
| warnings.resetwarnings() | |
| # Add multiple filters to the list to increase odds of race. | |
| for lineno in range(20): | |
| warnings.filterwarnings('ignore', message='not matched', category=Warning, lineno=lineno) | |
| # Override showwarning() so that we don't actually show warnings. | |
| def showwarning(*args): | |
| pass | |
| warnings.showwarning = showwarning | |
| def tearDown(self): | |
| warnings.filters[:] = self.saved_filters | |
| warnings.showwarning = warnings._showwarning_orig | |
| def test_racing_warnings_filter(self): | |
| # Modifying the warnings.filters list while another thread is using | |
| # warn() should not crash or race. | |
| def modify_filters(): | |
| time.sleep(0) | |
| warnings.filters[:] = [('ignore', None, UserWarning, None, 0)] | |
| time.sleep(0) | |
| warnings.filters[:] = self.saved_filters | |
| def emit_warning(): | |
| warnings.warn('dummy message', category=UserWarning) | |
| do_race(modify_filters, emit_warning) | |
| if __name__ == "__main__": | |
| unittest.main() |