From 68e01390e007bb5dd24402b1c4879852bdaed4f0 Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Wed, 21 Feb 2024 10:19:07 -0500 Subject: [PATCH 1/6] Init test for canceling timer --- src/test_timer.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/test_timer.py diff --git a/src/test_timer.py b/src/test_timer.py new file mode 100644 index 0000000..c240e69 --- /dev/null +++ b/src/test_timer.py @@ -0,0 +1,122 @@ +import asyncio +import pygame +from typing import Union +import uuid + +# do no change import order for *thread* +# patching threading.Thread +import aio.gthread + +# patched module +from threading import Thread + +THREADS = {} + + +def patch_set_timer(event: Union[int, pygame.event.Event], millis: int, loops: int = 0): + """Patches the pygame.time.set_timer function to use gthreads""" + print("In patch_set_timer") + dlay = float(millis) / 1000 + cevent = pygame.event.Event(event) + event_loop = asyncio.get_event_loop() + + async def fire_event(thread_uuid): + """The thread's target function to handle the timer + + Early exit conditions: + - event loop is closed + - event type is no longer in THREADS dictionary + - the thread's uuid is not the latest one + - Max loop iterations if loops param is not zero + """ + loop_counter = 0 + while True: + await asyncio.sleep(dlay) + if ( + event_loop.is_closed() + or event not in THREADS + or THREADS[event] != thread_uuid + or (loops and loop_counter >= loops) + ): + break + + pygame.event.post(cevent) + loop_counter += 1 if loops else 0 + + if dlay > 0: + # uuid is used to track the latest thread, + # stale threads will be terminated + thread_uuid = uuid.uuid4() + Thread(target=fire_event, args=[thread_uuid]).start() + THREADS[event] = thread_uuid + + else: + # This cancels the timer for the event + if event in THREADS: + del THREADS[event] + + +def cancel_timer_test(event, timer_event, set_timer_func, val, max_val): + if event.type == timer_event: + print("timer_event!") + if val >= 0: + val += 1 + print(f"[{timer_event}] timer_val: {val}") + if val == max_val: + # attempt to cancel timer + set_timer_func(timer_event, 0) + elif val > max_val: + val = -1 + print("Failed to cancel timer") + return val + + +async def test_cancel_original_timer(start): + print("Test canceling original timer after 3 secs...") + timer_event = pygame.USEREVENT + pygame.time.set_timer(timer_event, 1000) + timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 4000: + break + # event loop + for event in pygame.event.get(): + timer_val = cancel_timer_test( + event, timer_event, pygame.time.set_timer, timer_val, 3 + ) + + await asyncio.sleep(0) + return timer_val + + +async def test_cancel_patched_timer(start): + print("Test canceling patched timer after 3 secs...") + patched_timer_event = pygame.USEREVENT + 1 + patch_set_timer(patched_timer_event, 1000) + patched_timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 4000: + break + # event loop + for event in pygame.event.get(): + patched_timer_val = cancel_timer_test( + event, patched_timer_event, patch_set_timer, patched_timer_val, 3 + ) + + await asyncio.sleep(0) + return patched_timer_val + + +async def main(): + start = pygame.time.get_ticks() + org_timer_val = await test_cancel_original_timer(start) + print(f"RETURN VAL: {org_timer_val}") + assert org_timer_val == -1 + + start = pygame.time.get_ticks() + patch_timer_val = await test_cancel_patched_timer(start) + assert patch_timer_val == 3 + + +if __name__ == "__main__": + asyncio.run(main()) From e4e5fc089aa77206a0c6d61013046cf8d140b275 Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Wed, 21 Feb 2024 22:38:31 -0500 Subject: [PATCH 2/6] init tests for repeated calls --- src/test_timer.py | 57 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/test_timer.py b/src/test_timer.py index c240e69..842f45b 100644 --- a/src/test_timer.py +++ b/src/test_timer.py @@ -15,7 +15,6 @@ def patch_set_timer(event: Union[int, pygame.event.Event], millis: int, loops: int = 0): """Patches the pygame.time.set_timer function to use gthreads""" - print("In patch_set_timer") dlay = float(millis) / 1000 cevent = pygame.event.Event(event) event_loop = asyncio.get_event_loop() @@ -107,16 +106,70 @@ async def test_cancel_patched_timer(start): return patched_timer_val +async def test_repeated_original_timer(start): + print("Test repeating original timer after 10 secs...") + timer_event = pygame.USEREVENT + 2 + pygame.time.set_timer(timer_event, 1000) + timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 10_000: + break + # event loop + for event in pygame.event.get(): + if event.type == timer_event: + timer_val += 1 + if timer_val == 2: + pygame.time.set_timer(timer_event, 2000) + print(timer_val) + + await asyncio.sleep(0) + return timer_val + + +async def test_repeated_patched_timer(start): + print("Test repeating patched timer after 10 secs...") + timer_event = pygame.USEREVENT + 3 + patch_set_timer(timer_event, 1000) + timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 10_000: + break + # event loop + for event in pygame.event.get(): + if event.type == timer_event: + timer_val += 1 + if timer_val == 2: + patch_set_timer(timer_event, 2000) + print(timer_val) + + await asyncio.sleep(0) + return timer_val + + async def main(): + print("===> CANCEL TEST 1: Original vs Patched Timer") + # Original timer creates multiple duplicate timers with delay of 0 start = pygame.time.get_ticks() org_timer_val = await test_cancel_original_timer(start) - print(f"RETURN VAL: {org_timer_val}") assert org_timer_val == -1 + # Patched timer cancels the timer correctly start = pygame.time.get_ticks() patch_timer_val = await test_cancel_patched_timer(start) assert patch_timer_val == 3 + print("===> REPEAT TEST 2: Original vs Patched Timer") + # Original timer will create a duplicate timer instead of + # canceling the current one + start = pygame.time.get_ticks() + org_timer_val = await test_repeated_original_timer(start) + assert org_timer_val > 10 + + # Patched timer replaces the existing one + start = pygame.time.get_ticks() + patch_timer_val = await test_repeated_patched_timer(start) + assert patch_timer_val < 10 + if __name__ == "__main__": asyncio.run(main()) From 596178d5c051834ac50095dd0358fd7694730325 Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Thu, 22 Feb 2024 07:22:36 -0500 Subject: [PATCH 3/6] Updated patch; added test for fixed iterations and event object type --- src/test_timer.py | 84 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/src/test_timer.py b/src/test_timer.py index 842f45b..0367c72 100644 --- a/src/test_timer.py +++ b/src/test_timer.py @@ -10,13 +10,25 @@ # patched module from threading import Thread +# Global var to keep track of timer threads +# - key: event type +# - value: thread uuid THREADS = {} def patch_set_timer(event: Union[int, pygame.event.Event], millis: int, loops: int = 0): - """Patches the pygame.time.set_timer function to use gthreads""" + """repeatedly create an event on the event queue + + Patches the pygame.time.set_timer function to use gthreads + """ dlay = float(millis) / 1000 - cevent = pygame.event.Event(event) + if isinstance(event, pygame.event.Event): + event_type = event.type + cevent = event + else: + event_type = int(event) + cevent = pygame.event.Event(event) + event_loop = asyncio.get_event_loop() async def fire_event(thread_uuid): @@ -33,8 +45,8 @@ async def fire_event(thread_uuid): await asyncio.sleep(dlay) if ( event_loop.is_closed() - or event not in THREADS - or THREADS[event] != thread_uuid + or event_type not in THREADS + or THREADS[event_type] != thread_uuid or (loops and loop_counter >= loops) ): break @@ -47,12 +59,12 @@ async def fire_event(thread_uuid): # stale threads will be terminated thread_uuid = uuid.uuid4() Thread(target=fire_event, args=[thread_uuid]).start() - THREADS[event] = thread_uuid + THREADS[event_type] = thread_uuid else: # This cancels the timer for the event if event in THREADS: - del THREADS[event] + del THREADS[event_type] def cancel_timer_test(event, timer_event, set_timer_func, val, max_val): @@ -146,29 +158,85 @@ async def test_repeated_patched_timer(start): return timer_val +async def test_fixed_iterations_patched_timer(start, num_iter): + print(f"Test looping timer {num_iter} times") + timer_event = pygame.USEREVENT + 4 + patch_set_timer(timer_event, 1000, num_iter) + timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 4000: + break + # event loop + for event in pygame.event.get(): + if event.type == timer_event: + timer_val += 1 + print(timer_val) + + await asyncio.sleep(0) + return timer_val + + +async def test_event_object_param_patched_timer(start, num_iter): + print(f"Test looping timer {num_iter} times using event object") + timer_event = pygame.USEREVENT + 5 + timer_event_obj = pygame.event.Event(timer_event) + patch_set_timer(timer_event_obj, 1000, num_iter) + timer_val = 0 + while True: + if pygame.time.get_ticks() - start > 4000: + break + # event loop + for event in pygame.event.get(): + if event.type == timer_event: + timer_val += 1 + print(timer_val) + + await asyncio.sleep(0) + return timer_val + + async def main(): - print("===> CANCEL TEST 1: Original vs Patched Timer") + print("===== CANCEL TEST 1: Original vs Patched Timer =====") # Original timer creates multiple duplicate timers with delay of 0 start = pygame.time.get_ticks() org_timer_val = await test_cancel_original_timer(start) assert org_timer_val == -1 + print("===> TEST CASE 1 Complete") # Patched timer cancels the timer correctly start = pygame.time.get_ticks() patch_timer_val = await test_cancel_patched_timer(start) assert patch_timer_val == 3 + print("===> TEST CASE 2 Complete") - print("===> REPEAT TEST 2: Original vs Patched Timer") + print("===== REPEAT TEST 2: Original vs Patched Timer =====") # Original timer will create a duplicate timer instead of # canceling the current one start = pygame.time.get_ticks() org_timer_val = await test_repeated_original_timer(start) assert org_timer_val > 10 + print("===> TEST CASE 3 Complete") # Patched timer replaces the existing one start = pygame.time.get_ticks() patch_timer_val = await test_repeated_patched_timer(start) assert patch_timer_val < 10 + print("===> TEST CASE 4 Complete") + + # The following tests are for the patched timer only + print("===== FIXED ITERATION TEST 3: Patched Timer =====") + start = pygame.time.get_ticks() + patch_timer_val = await test_fixed_iterations_patched_timer(start, 3) + assert patch_timer_val == 3 + print("===> TEST CASE 5 Complete") + + print("===== EVENT OBJ PARM TEST 4: Patched Timer =====") + start = pygame.time.get_ticks() + patch_timer_val = await test_event_object_param_patched_timer(start, 3) + assert patch_timer_val == 3 + print("===> TEST CASE 6 Complete") + + print("ALL DONE!") if __name__ == "__main__": From 930f81160bd494e3184ad31e77cb9827e844f445 Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Thu, 22 Feb 2024 07:36:39 -0500 Subject: [PATCH 4/6] Added missing pygame.init call --- src/test_timer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test_timer.py b/src/test_timer.py index 0367c72..5c2607d 100644 --- a/src/test_timer.py +++ b/src/test_timer.py @@ -3,6 +3,8 @@ from typing import Union import uuid +pygame.init() + # do no change import order for *thread* # patching threading.Thread import aio.gthread From 600a0a7971b9c6aafdfcb0d796541e093c3c1c6f Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Thu, 22 Feb 2024 08:25:57 -0500 Subject: [PATCH 5/6] Added test_timer to testsuite --- src/testsuite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testsuite.py b/src/testsuite.py index 545468d..c63342e 100644 --- a/src/testsuite.py +++ b/src/testsuite.py @@ -330,7 +330,7 @@ test_xml_dom_minicompat test_xml_etree test_xml_etree_c test_xmlrpc test_xmlrpc_net test_xxlimited test_xxtestfuzz test_yield_from test_zipapp test_zipfile test_zipfile64 -test_zipimport test_zipimport_support test_zlib test_zoneinfo +test_zipimport test_zipimport_support test_zlib test_zoneinfo test_timer """ #============================================================================ From b236972f725fa53f9820037906c75c6852e38111 Mon Sep 17 00:00:00 2001 From: Eric Dong Date: Thu, 22 Feb 2024 08:28:10 -0500 Subject: [PATCH 6/6] Added events to block list if test is done to avoid queue filling up --- src/test_timer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test_timer.py b/src/test_timer.py index 5c2607d..b02484f 100644 --- a/src/test_timer.py +++ b/src/test_timer.py @@ -99,6 +99,8 @@ async def test_cancel_original_timer(start): ) await asyncio.sleep(0) + pygame.event.set_blocked(timer_event) + pygame.event.clear() return timer_val @@ -117,6 +119,8 @@ async def test_cancel_patched_timer(start): ) await asyncio.sleep(0) + pygame.event.set_blocked(patched_timer_event) + pygame.event.clear() return patched_timer_val @@ -137,6 +141,8 @@ async def test_repeated_original_timer(start): print(timer_val) await asyncio.sleep(0) + pygame.event.set_blocked(timer_event) + pygame.event.clear() return timer_val @@ -157,6 +163,8 @@ async def test_repeated_patched_timer(start): print(timer_val) await asyncio.sleep(0) + pygame.event.set_blocked(timer_event) + pygame.event.clear() return timer_val @@ -175,6 +183,8 @@ async def test_fixed_iterations_patched_timer(start, num_iter): print(timer_val) await asyncio.sleep(0) + pygame.event.set_blocked(timer_event) + pygame.event.clear() return timer_val @@ -194,6 +204,8 @@ async def test_event_object_param_patched_timer(start, num_iter): print(timer_val) await asyncio.sleep(0) + pygame.event.set_blocked(timer_event) + pygame.event.clear() return timer_val