import asyncio import concurrent.futures import pytest from openhands.utils.async_utils import ( AsyncException, call_async_from_sync, call_sync_from_async, run_in_loop, wait_all, ) @pytest.mark.asyncio async def test_await_all(): # Mock function demonstrating some calculation - always takes a minimum of 0.1 seconds async def dummy(value: int): await asyncio.sleep(0.1) return value * 2 # wait for 10 calculations - serially this would take 1 second coro = wait_all(dummy(i) for i in range(10)) # give the task only 0.3 seconds to complete (This verifies they occur in parallel) task = asyncio.create_task(coro) await asyncio.wait([task], timeout=0.3) # validate the results (We need to sort because they can return in any order) results = list(await task) expected = [i * 2 for i in range(10)] assert expected == results @pytest.mark.asyncio async def test_await_all_single_exception(): # Mock function demonstrating some calculation - always takes a minimum of 0.1 seconds async def dummy(value: int): await asyncio.sleep(0.1) if value == 1: raise ValueError('Invalid value 1') # Throw an exception on every odd value return value * 2 # expect an exception to be raised. with pytest.raises(ValueError, match='Invalid value 1'): await wait_all(dummy(i) for i in range(10)) @pytest.mark.asyncio async def test_await_all_multi_exception(): # Mock function demonstrating some calculation - always takes a minimum of 0.1 seconds async def dummy(value: int): await asyncio.sleep(0.1) if value & 1: raise ValueError( f'Invalid value {value}' ) # Throw an exception on every odd value return value * 2 # expect an exception to be raised. with pytest.raises(AsyncException): await wait_all(dummy(i) for i in range(10)) @pytest.mark.asyncio async def test_await_all_timeout(): result = 0 # Mock function updates a nonlocal variable after a delay async def dummy(value: int): nonlocal result await asyncio.sleep(0.2) result += value # expect an exception to be raised. with pytest.raises(asyncio.TimeoutError): await wait_all((dummy(i) for i in range(10)), 0.1) # Wait and then check the shared result - this makes sure that pending tasks were cancelled. asyncio.sleep(0.2) assert result == 0 @pytest.mark.asyncio async def test_call_sync_from_async(): def dummy(value: int = 2): return value * 2 result = await call_sync_from_async(dummy) assert result == 4 result = await call_sync_from_async(dummy, 3) assert result == 6 result = await call_sync_from_async(dummy, value=5) assert result == 10 @pytest.mark.asyncio async def test_call_sync_from_async_error(): def dummy(): raise ValueError() with pytest.raises(ValueError): await call_sync_from_async(dummy) def test_call_async_from_sync(): async def dummy(value: int): return value * 2 result = call_async_from_sync(dummy, 0, 3) assert result == 6 def test_call_async_from_sync_error(): async def dummy(value: int): raise ValueError() with pytest.raises(ValueError): call_async_from_sync(dummy, 0, 3) def test_call_async_from_sync_background_tasks(): events = [] async def bg_task(): # This background task should finish after the dummy task events.append('bg_started') asyncio.sleep(0.2) events.append('bg_finished') async def dummy(value: int): events.append('dummy_started') # This coroutine kicks off a background task asyncio.create_task(bg_task()) events.append('dummy_started') call_async_from_sync(dummy, 0, 3) # We check that the function did not return until all coroutines completed # (Even though some of these were started as background tasks) expected = ['dummy_started', 'dummy_started', 'bg_started', 'bg_finished'] assert expected == events @pytest.mark.asyncio async def test_run_in_loop_same_loop(): """Test run_in_loop when the target loop is the same as the current loop.""" async def dummy_coro(value: int): await asyncio.sleep(0.01) # Small delay to make it actually async return value * 2 # Get the current running loop current_loop = asyncio.get_running_loop() # Create a coroutine and run it in the same loop coro = dummy_coro(5) result = await run_in_loop(coro, current_loop) assert result == 10 @pytest.mark.asyncio async def test_run_in_loop_different_loop(): """Test run_in_loop when the target loop is different from the current loop.""" import queue import threading async def dummy_coro(value: int): await asyncio.sleep(0.01) # Small delay to make it actually async return value * 3 queue.Queue() loop_queue = queue.Queue() def run_in_new_loop(): """Create and run a new event loop in a separate thread.""" new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) loop_queue.put(new_loop) # Share the loop with the main thread try: # Keep the loop running for a short time new_loop.run_until_complete(asyncio.sleep(2.0)) except Exception: pass # Expected when we stop the loop finally: new_loop.close() # Start the new loop in a separate thread thread = threading.Thread(target=run_in_new_loop, daemon=True) thread.start() # Get the new loop from the thread await asyncio.sleep(0.1) # Give thread time to start new_loop = loop_queue.get(timeout=1.0) try: # Create a coroutine and run it in the different loop coro = dummy_coro(7) result = await run_in_loop(coro, new_loop) assert result == 21 finally: # Clean up: stop the loop new_loop.call_soon_threadsafe(new_loop.stop) @pytest.mark.asyncio async def test_run_in_loop_with_exception(): """Test run_in_loop when the coroutine raises an exception.""" async def failing_coro(): await asyncio.sleep(0.01) raise ValueError('Test exception') current_loop = asyncio.get_running_loop() coro = failing_coro() with pytest.raises(ValueError, match='Test exception'): await run_in_loop(coro, current_loop) @pytest.mark.asyncio async def test_run_in_loop_with_timeout(): """Test run_in_loop with a timeout when using different loops.""" import queue import threading async def slow_coro(): await asyncio.sleep(1.0) # Sleep longer than timeout return 'should not reach here' loop_queue = queue.Queue() def run_in_new_loop(): """Create and run a new event loop in a separate thread.""" new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) loop_queue.put(new_loop) try: # Keep the loop running for a short time new_loop.run_until_complete(asyncio.sleep(2.0)) except Exception: pass # Expected when we stop the loop finally: new_loop.close() # Start the new loop in a separate thread thread = threading.Thread(target=run_in_new_loop, daemon=True) thread.start() # Get the new loop from the thread await asyncio.sleep(0.1) # Give thread time to start new_loop = loop_queue.get(timeout=1.0) try: coro = slow_coro() # Test with a short timeout - this should raise a timeout exception with pytest.raises( (TimeoutError, concurrent.futures.TimeoutError) ): # Could be TimeoutError or concurrent.futures.TimeoutError await run_in_loop(coro, new_loop, timeout=0.1) finally: # Clean up: stop the loop new_loop.call_soon_threadsafe(new_loop.stop) @pytest.mark.asyncio async def test_run_in_loop_same_loop_no_timeout(): """Test that run_in_loop doesn't apply timeout when using the same loop.""" async def quick_coro(): await asyncio.sleep(0.01) return 'completed' current_loop = asyncio.get_running_loop() coro = quick_coro() # Even with a very short timeout, this should work because # timeout is only applied when using different loops result = await run_in_loop(coro, current_loop, timeout=0.001) assert result == 'completed' @pytest.mark.asyncio async def test_run_in_loop_return_value(): """Test that run_in_loop properly returns the coroutine result.""" async def return_dict(): await asyncio.sleep(0.01) return {'key': 'value', 'number': 42} current_loop = asyncio.get_running_loop() coro = return_dict() result = await run_in_loop(coro, current_loop) assert isinstance(result, dict) assert result['key'] == 'value' assert result['number'] == 42 @pytest.mark.asyncio async def test_run_in_loop_with_args_and_kwargs(): """Test run_in_loop with a coroutine that uses arguments.""" async def coro_with_args(a, b, multiplier=1): await asyncio.sleep(0.01) return (a + b) * multiplier current_loop = asyncio.get_running_loop() # Create coroutine with args and kwargs coro = coro_with_args(5, 10, multiplier=2) result = await run_in_loop(coro, current_loop) assert result == 30 # (5 + 10) * 2 def test_run_in_loop_sync_context(): """Test run_in_loop behavior when called from a synchronous context.""" async def dummy_coro(value: int): await asyncio.sleep(0.01) return value * 4 def sync_function(): """Function that runs in a new event loop.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: coro = dummy_coro(6) # This simulates the scenario where we have a different target loop return loop.run_until_complete(coro) finally: loop.close() # Test the function in a synchronous context result = sync_function() assert result == 24