|
16 | 16 | # under the License. |
17 | 17 |
|
18 | 18 | import base64 |
| 19 | +import concurrent.futures |
19 | 20 | import threading |
| 21 | +import time |
20 | 22 |
|
21 | 23 | import pytest |
22 | 24 |
|
@@ -930,197 +932,149 @@ def on_context_created_2(info): |
930 | 932 | driver.browsing_context.remove_event_handler("context_created", callback_id_2) |
931 | 933 |
|
932 | 934 |
|
933 | | -def test_event_handler_thread_safety(driver): |
934 | | - """Test thread safety with multiple non-atomic operations in callbacks.""" |
935 | | - import concurrent.futures |
936 | | - import time |
937 | | - |
938 | | - events_received = [] |
939 | | - context_counts = {} |
940 | | - event_type_counts = {} |
941 | | - processing_times = [] |
942 | | - consistency_errors = [] |
943 | | - thread_errors = [] |
944 | | - |
945 | | - data_lock = threading.Lock() |
946 | | - callback_ids = [] |
947 | | - registration_complete = threading.Event() |
948 | | - |
949 | | - def complex_event_callback(info): |
950 | | - """Callback with multiple non-atomic operations that require thread synchronization.""" |
951 | | - start_time = time.time() |
952 | | - time.sleep(0.02) # Create race condition window |
953 | | - |
954 | | - with data_lock: |
955 | | - # Multiple operations that could race without proper locking |
956 | | - initial_event_count = len(events_received) |
957 | | - _ = sum(context_counts.values()) if context_counts else 0 |
958 | | - _ = sum(event_type_counts.values()) if event_type_counts else 0 |
959 | | - |
960 | | - events_received.append(info) |
961 | | - |
962 | | - context_id = info.context |
963 | | - if context_id not in context_counts: |
964 | | - context_counts[context_id] = 0 |
965 | | - context_counts[context_id] += 1 |
966 | | - |
967 | | - event_type = info.__class__.__name__ |
968 | | - if event_type not in event_type_counts: |
969 | | - event_type_counts[event_type] = 0 |
970 | | - event_type_counts[event_type] += 1 |
971 | | - |
972 | | - processing_time = time.time() - start_time |
973 | | - processing_times.append(processing_time) |
974 | | - |
975 | | - # Verify data consistency |
976 | | - final_event_count = len(events_received) |
977 | | - final_context_total = sum(context_counts.values()) |
978 | | - final_type_total = sum(event_type_counts.values()) |
979 | | - final_processing_count = len(processing_times) |
980 | | - |
981 | | - expected_count = initial_event_count + 1 |
982 | | - if not ( |
983 | | - final_event_count == final_context_total == final_type_total == final_processing_count == expected_count |
984 | | - ): |
985 | | - error_msg = ( |
986 | | - f"Data consistency error! Events: {final_event_count}, " |
987 | | - f"Contexts: {final_context_total}, Types: {final_type_total}, " |
988 | | - f"Times: {final_processing_count}, Expected: {expected_count}" |
989 | | - ) |
990 | | - consistency_errors.append(error_msg) |
991 | | - |
992 | | - def register_handler(thread_id): |
| 935 | +class _EventHandlerTestHelper: |
| 936 | + def __init__(self, driver): |
| 937 | + self.driver = driver |
| 938 | + self.events_received = [] |
| 939 | + self.context_counts = {} |
| 940 | + self.event_type_counts = {} |
| 941 | + self.processing_times = [] |
| 942 | + self.consistency_errors = [] |
| 943 | + self.thread_errors = [] |
| 944 | + self.callback_ids = [] |
| 945 | + self.data_lock = threading.Lock() |
| 946 | + self.registration_complete = threading.Event() |
| 947 | + |
| 948 | + def make_callback(self): |
| 949 | + def callback(info): |
| 950 | + start_time = time.time() |
| 951 | + time.sleep(0.02) # Simulate race window |
| 952 | + |
| 953 | + with self.data_lock: |
| 954 | + initial_event_count = len(self.events_received) |
| 955 | + |
| 956 | + self.events_received.append(info) |
| 957 | + |
| 958 | + context_id = info.context |
| 959 | + self.context_counts.setdefault(context_id, 0) |
| 960 | + self.context_counts[context_id] += 1 |
| 961 | + |
| 962 | + event_type = info.__class__.__name__ |
| 963 | + self.event_type_counts.setdefault(event_type, 0) |
| 964 | + self.event_type_counts[event_type] += 1 |
| 965 | + |
| 966 | + processing_time = time.time() - start_time |
| 967 | + self.processing_times.append(processing_time) |
| 968 | + |
| 969 | + final_event_count = len(self.events_received) |
| 970 | + final_context_total = sum(self.context_counts.values()) |
| 971 | + final_type_total = sum(self.event_type_counts.values()) |
| 972 | + final_processing_count = len(self.processing_times) |
| 973 | + |
| 974 | + expected_count = initial_event_count + 1 |
| 975 | + if not ( |
| 976 | + final_event_count |
| 977 | + == final_context_total |
| 978 | + == final_type_total |
| 979 | + == final_processing_count |
| 980 | + == expected_count |
| 981 | + ): |
| 982 | + self.consistency_errors.append("Data consistency error") |
| 983 | + |
| 984 | + return callback |
| 985 | + |
| 986 | + def register_handler(self, thread_id): |
993 | 987 | try: |
994 | | - callback_id = driver.browsing_context.add_event_handler("context_created", complex_event_callback) |
995 | | - with data_lock: |
996 | | - callback_ids.append(callback_id) |
997 | | - if len(callback_ids) == 5: |
998 | | - registration_complete.set() |
| 988 | + callback = self.make_callback() |
| 989 | + callback_id = self.driver.browsing_context.add_event_handler("context_created", callback) |
| 990 | + with self.data_lock: |
| 991 | + self.callback_ids.append(callback_id) |
| 992 | + if len(self.callback_ids) == 5: |
| 993 | + self.registration_complete.set() |
999 | 994 | return callback_id |
1000 | 995 | except Exception as e: |
1001 | | - with data_lock: |
1002 | | - thread_errors.append(f"Thread {thread_id}: Registration failed: {e}") |
| 996 | + with self.data_lock: |
| 997 | + self.thread_errors.append(f"Thread {thread_id}: Registration failed: {e}") |
1003 | 998 | return None |
1004 | 999 |
|
1005 | | - def remove_handler(callback_id, thread_id): |
| 1000 | + def remove_handler(self, callback_id, thread_id): |
1006 | 1001 | try: |
1007 | | - driver.browsing_context.remove_event_handler("context_created", callback_id) |
| 1002 | + self.driver.browsing_context.remove_event_handler("context_created", callback_id) |
1008 | 1003 | except Exception as e: |
1009 | | - with data_lock: |
1010 | | - thread_errors.append(f"Thread {thread_id}: Removal failed: {e}") |
| 1004 | + with self.data_lock: |
| 1005 | + self.thread_errors.append(f"Thread {thread_id}: Removal failed: {e}") |
| 1006 | + |
1011 | 1007 |
|
1012 | | - initial_context = driver.browsing_context.create(type=WindowTypes.TAB) |
| 1008 | +def test_concurrent_event_handler_registration(driver): |
| 1009 | + helper = _EventHandlerTestHelper(driver) |
1013 | 1010 |
|
1014 | | - # Concurrent registration |
1015 | 1011 | with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: |
1016 | | - futures = {} |
1017 | | - for i in range(5): |
1018 | | - future = executor.submit(register_handler, f"reg-{i}") |
1019 | | - futures[future] = f"reg-{i}" |
| 1012 | + futures = [executor.submit(helper.register_handler, f"reg-{i}") for i in range(5)] |
| 1013 | + for future in futures: |
| 1014 | + future.result(timeout=15) |
| 1015 | + |
| 1016 | + helper.registration_complete.wait(timeout=5) |
| 1017 | + assert len(helper.callback_ids) == 5, f"Expected 5 handlers, got {len(helper.callback_ids)}" |
| 1018 | + assert not helper.thread_errors, "Errors during registration: \n" + "\n".join(helper.thread_errors) |
| 1019 | + |
| 1020 | + |
| 1021 | +def test_event_callback_data_consistency(driver): |
| 1022 | + helper = _EventHandlerTestHelper(driver) |
| 1023 | + |
| 1024 | + for i in range(5): |
| 1025 | + helper.register_handler(f"reg-{i}") |
| 1026 | + |
| 1027 | + test_contexts = [] |
| 1028 | + for _ in range(3): |
| 1029 | + context = driver.browsing_context.create(type=WindowTypes.TAB) |
| 1030 | + test_contexts.append(context) |
| 1031 | + |
| 1032 | + for ctx in test_contexts: |
| 1033 | + driver.browsing_context.close(ctx) |
| 1034 | + |
| 1035 | + with helper.data_lock: |
| 1036 | + assert not helper.consistency_errors, "Consistency errors: " + str(helper.consistency_errors) |
| 1037 | + assert len(helper.events_received) > 0, "No events received" |
| 1038 | + assert len(helper.events_received) == sum(helper.context_counts.values()) |
| 1039 | + assert len(helper.events_received) == sum(helper.event_type_counts.values()) |
| 1040 | + assert len(helper.events_received) == len(helper.processing_times) |
1020 | 1041 |
|
| 1042 | + |
| 1043 | +def test_concurrent_event_handler_removal(driver): |
| 1044 | + helper = _EventHandlerTestHelper(driver) |
| 1045 | + |
| 1046 | + for i in range(5): |
| 1047 | + helper.register_handler(f"reg-{i}") |
| 1048 | + |
| 1049 | + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: |
| 1050 | + futures = [ |
| 1051 | + executor.submit(helper.remove_handler, callback_id, f"rem-{i}") |
| 1052 | + for i, callback_id in enumerate(helper.callback_ids) |
| 1053 | + ] |
1021 | 1054 | for future in futures: |
1022 | | - thread_id = futures[future] |
1023 | | - try: |
1024 | | - future.result(timeout=15) |
1025 | | - except concurrent.futures.TimeoutError: |
1026 | | - with data_lock: |
1027 | | - thread_errors.append(f"Thread {thread_id}: Registration timed out") |
1028 | | - except Exception as e: |
1029 | | - with data_lock: |
1030 | | - thread_errors.append(f"Thread {thread_id}: Registration exception: {e}") |
1031 | | - |
1032 | | - registration_complete.wait(timeout=5) |
1033 | | - |
1034 | | - with data_lock: |
1035 | | - successful_registrations = len(callback_ids) |
1036 | | - |
1037 | | - # Trigger events while handlers are active |
1038 | | - if successful_registrations > 0: |
1039 | | - test_contexts = [] |
1040 | | - for i in range(3): |
1041 | | - try: |
1042 | | - context = driver.browsing_context.create(type=WindowTypes.TAB) |
1043 | | - test_contexts.append(context) |
1044 | | - time.sleep(0.1) |
1045 | | - except Exception as e: |
1046 | | - thread_errors.append(f"Failed to create test context {i}: {e}") |
1047 | | - |
1048 | | - time.sleep(1.0) # Allow event processing |
1049 | | - |
1050 | | - for context in test_contexts: |
1051 | | - try: |
1052 | | - driver.browsing_context.close(context) |
1053 | | - except Exception: |
1054 | | - pass |
1055 | | - |
1056 | | - # Concurrent removal |
1057 | | - if callback_ids: |
1058 | | - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: |
1059 | | - futures = {} |
1060 | | - for i, callback_id in enumerate(callback_ids): |
1061 | | - future = executor.submit(remove_handler, callback_id, f"rem-{i}") |
1062 | | - futures[future] = f"rem-{i}" |
1063 | | - |
1064 | | - for future in futures: |
1065 | | - thread_id = futures[future] |
1066 | | - try: |
1067 | | - future.result(timeout=15) |
1068 | | - except concurrent.futures.TimeoutError: |
1069 | | - with data_lock: |
1070 | | - thread_errors.append(f"Thread {thread_id}: Removal timed out") |
1071 | | - except Exception as e: |
1072 | | - with data_lock: |
1073 | | - thread_errors.append(f"Thread {thread_id}: Removal exception: {e}") |
1074 | | - |
1075 | | - time.sleep(0.5) |
1076 | | - |
1077 | | - # Verify handlers are removed |
1078 | | - with data_lock: |
1079 | | - events_before_removal_test = len(events_received) |
| 1055 | + future.result(timeout=15) |
1080 | 1056 |
|
1081 | | - try: |
1082 | | - post_removal_context = driver.browsing_context.create(type=WindowTypes.TAB) |
1083 | | - time.sleep(0.8) |
1084 | | - driver.browsing_context.close(post_removal_context) |
1085 | | - except Exception as e: |
1086 | | - thread_errors.append(f"Failed to create post-removal test context: {e}") |
| 1057 | + assert not helper.thread_errors, "Errors during removal: \n" + "\n".join(helper.thread_errors) |
1087 | 1058 |
|
1088 | | - with data_lock: |
1089 | | - events_after_removal = len(events_received) - events_before_removal_test |
1090 | 1059 |
|
1091 | | - # Cleanup |
1092 | | - try: |
1093 | | - driver.browsing_context.close(initial_context) |
1094 | | - except Exception as e: |
1095 | | - thread_errors.append(f"Cleanup error: {e}") |
| 1060 | +def test_no_event_after_handler_removal(driver): |
| 1061 | + helper = _EventHandlerTestHelper(driver) |
1096 | 1062 |
|
1097 | | - # Assertions |
1098 | | - all_errors = thread_errors + consistency_errors |
1099 | | - if all_errors: |
1100 | | - pytest.fail("Thread safety test failed with errors:\n" + "\n".join(all_errors)) |
| 1063 | + for i in range(5): |
| 1064 | + helper.register_handler(f"reg-{i}") |
1101 | 1065 |
|
1102 | | - assert successful_registrations > 0, f"No handlers were successfully registered (got {successful_registrations})" |
1103 | | - assert len(events_received) > 0, "No events were received during test" |
| 1066 | + context = driver.browsing_context.create(type=WindowTypes.TAB) |
| 1067 | + driver.browsing_context.close(context) |
1104 | 1068 |
|
1105 | | - # Verify data consistency across multiple counters |
1106 | | - with data_lock: |
1107 | | - total_context_events = sum(context_counts.values()) if context_counts else 0 |
1108 | | - total_type_events = sum(event_type_counts.values()) if event_type_counts else 0 |
| 1069 | + events_before = len(helper.events_received) |
1109 | 1070 |
|
1110 | | - assert len(events_received) == total_context_events, ( |
1111 | | - f"Context count mismatch: {len(events_received)} vs {total_context_events}" |
1112 | | - ) |
1113 | | - assert len(events_received) == total_type_events, ( |
1114 | | - f"Type count mismatch: {len(events_received)} vs {total_type_events}" |
1115 | | - ) |
1116 | | - assert len(events_received) == len(processing_times), ( |
1117 | | - f"Processing time count mismatch: {len(events_received)} vs {len(processing_times)}" |
1118 | | - ) |
| 1071 | + for i, callback_id in enumerate(helper.callback_ids): |
| 1072 | + helper.remove_handler(callback_id, f"rem-{i}") |
| 1073 | + |
| 1074 | + post_context = driver.browsing_context.create(type=WindowTypes.TAB) |
| 1075 | + driver.browsing_context.close(post_context) |
1119 | 1076 |
|
1120 | | - # Verify handlers were properly removed |
1121 | | - assert events_after_removal == 0, f"Handlers still active after removal! Got {events_after_removal} events" |
| 1077 | + with helper.data_lock: |
| 1078 | + new_events = len(helper.events_received) - events_before |
1122 | 1079 |
|
1123 | | - # Verify event object |
1124 | | - for i, event in enumerate(events_received): |
1125 | | - assert hasattr(event, "context"), f"Event {i} missing 'context' attribute" |
1126 | | - assert isinstance(event.context, str), f"Event {i} 'context' is not string: {type(event.context)}" |
| 1080 | + assert new_events == 0, f"Expected 0 new events after removal, got {new_events}" |
0 commit comments