"""Comprehensive tests for the Executor module. Tests cover: - Executor initialization - add_function method - add_coroutine method - add_strategy and add_strategies methods - run_strategy static method (async and sync strategies) - run_coroutine_tasks method - run_coroutine_task static method - run_function static method - sigint_handle method - exit method - execute method - Integration tests """ import asyncio import inspect from concurrent.futures import ThreadPoolExecutor from unittest.mock import MagicMock, AsyncMock, patch, call import pytest from aiomql.lib.executor import Executor from aiomql.lib.strategy import Strategy from aiomql.core.config import Config class MockAsyncStrategy: """Mock async strategy for testing.""" def __init__(self): self.running = True async def run_strategy(self): """Async run_strategy method.""" pass class MockSyncStrategy: """Mock sync strategy for testing.""" def __init__(self): self.running = True def run_strategy(self): """Sync run_strategy method.""" pass class TestExecutorInitialization: """Test Executor class initialization.""" @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_creates_empty_strategy_runners(self, mock_config, mock_signal): """Test Executor init creates empty strategy_runners list.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.strategy_runners == [] @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_creates_empty_coroutines(self, mock_config, mock_signal): """Test Executor init creates empty coroutines dict.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.coroutines == {} @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_creates_empty_coroutine_threads(self, mock_config, mock_signal): """Test Executor init creates empty coroutine_threads dict.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.coroutine_threads == {} @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_creates_empty_functions(self, mock_config, mock_signal): """Test Executor init creates empty functions dict.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.functions == {} @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_creates_config(self, mock_config, mock_signal): """Test Executor init creates config.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.config is not None @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_sets_timeout_none(self, mock_config, mock_signal): """Test Executor init sets timeout to None.""" config = MagicMock() mock_config.return_value = config executor = Executor() assert executor.timeout is None @patch('aiomql.lib.executor.signal') @patch.object(Config, '__new__') def test_init_registers_signal_handler(self, mock_config, mock_signal): """Test Executor init registers SIGINT handler.""" config = MagicMock() mock_config.return_value = config executor = Executor() mock_signal.assert_called() class TestAddFunction: """Test Executor add_function method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() mock_config.return_value = config return Executor() def test_add_function_without_kwargs(self, executor): """Test add_function without kwargs.""" def my_function(): pass executor.add_function(function=my_function) assert my_function in executor.functions assert executor.functions[my_function] == {} def test_add_function_with_kwargs(self, executor): """Test add_function with kwargs.""" def my_function(a, b): pass kwargs = {"a": 1, "b": 2} executor.add_function(function=my_function, kwargs=kwargs) assert my_function in executor.functions assert executor.functions[my_function] == kwargs def test_add_multiple_functions(self, executor): """Test adding multiple functions.""" def func1(): pass def func2(): pass executor.add_function(function=func1, kwargs={"x": 1}) executor.add_function(function=func2, kwargs={"y": 2}) assert len(executor.functions) == 2 assert func1 in executor.functions assert func2 in executor.functions class TestAddCoroutine: """Test Executor add_coroutine method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() mock_config.return_value = config return Executor() def test_add_coroutine_without_kwargs(self, executor): """Test add_coroutine without kwargs.""" async def my_coroutine(): pass executor.add_coroutine(coroutine=my_coroutine) assert my_coroutine in executor.coroutines assert executor.coroutines[my_coroutine] == {} def test_add_coroutine_with_kwargs(self, executor): """Test add_coroutine with kwargs.""" async def my_coroutine(a, b): pass kwargs = {"a": 1, "b": 2} executor.add_coroutine(coroutine=my_coroutine, kwargs=kwargs) assert my_coroutine in executor.coroutines assert executor.coroutines[my_coroutine] == kwargs def test_add_coroutine_on_separate_thread(self, executor): """Test add_coroutine with on_separate_thread=True.""" async def my_coroutine(): pass executor.add_coroutine(coroutine=my_coroutine, on_separate_thread=True) assert my_coroutine in executor.coroutine_threads assert my_coroutine not in executor.coroutines def test_add_coroutine_default_not_separate_thread(self, executor): """Test add_coroutine defaults to not separate thread.""" async def my_coroutine(): pass executor.add_coroutine(coroutine=my_coroutine) assert my_coroutine in executor.coroutines assert my_coroutine not in executor.coroutine_threads class TestAddStrategy: """Test Executor add_strategy and add_strategies methods.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() mock_config.return_value = config return Executor() def test_add_strategy(self, executor): """Test add_strategy adds single strategy.""" strategy = MagicMock(spec=Strategy) executor.add_strategy(strategy=strategy) assert len(executor.strategy_runners) == 1 assert strategy in executor.strategy_runners def test_add_multiple_strategies_one_by_one(self, executor): """Test adding multiple strategies one by one.""" strategy1 = MagicMock(spec=Strategy) strategy2 = MagicMock(spec=Strategy) executor.add_strategy(strategy=strategy1) executor.add_strategy(strategy=strategy2) assert len(executor.strategy_runners) == 2 assert strategy1 in executor.strategy_runners assert strategy2 in executor.strategy_runners def test_add_strategies_batch(self, executor): """Test add_strategies adds multiple strategies at once.""" strategy1 = MagicMock(spec=Strategy) strategy2 = MagicMock(spec=Strategy) strategy3 = MagicMock(spec=Strategy) executor.add_strategies(strategies=(strategy1, strategy2, strategy3)) assert len(executor.strategy_runners) == 3 assert strategy1 in executor.strategy_runners assert strategy2 in executor.strategy_runners assert strategy3 in executor.strategy_runners def test_add_strategies_extends_existing(self, executor): """Test add_strategies extends existing strategies.""" strategy1 = MagicMock(spec=Strategy) strategy2 = MagicMock(spec=Strategy) executor.add_strategy(strategy=strategy1) executor.add_strategies(strategies=(strategy2,)) assert len(executor.strategy_runners) == 2 class TestRunStrategy: """Test Executor run_strategy static method.""" def test_run_strategy_async(self): """Test run_strategy with async strategy.""" strategy = MockAsyncStrategy() strategy.run_strategy = AsyncMock() with patch('asyncio.run') as mock_asyncio_run: Executor.run_strategy(strategy) mock_asyncio_run.assert_called_once() def test_run_strategy_sync(self): """Test run_strategy with sync strategy.""" strategy = MockSyncStrategy() strategy.run_strategy = MagicMock() with patch('asyncio.run') as mock_asyncio_run: Executor.run_strategy(strategy) # asyncio.run should NOT be called for sync mock_asyncio_run.assert_not_called() strategy.run_strategy.assert_called_once() def test_run_strategy_detects_async_correctly(self): """Test run_strategy correctly detects async method.""" async_strategy = MockAsyncStrategy() assert inspect.iscoroutinefunction(async_strategy.run_strategy) def test_run_strategy_detects_sync_correctly(self): """Test run_strategy correctly detects sync method.""" sync_strategy = MockSyncStrategy() assert not inspect.iscoroutinefunction(sync_strategy.run_strategy) class TestRunCoroutineTasks: """Test Executor run_coroutine_tasks method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() mock_config.return_value = config return Executor() async def test_run_coroutine_tasks_runs_all(self, executor): """Test run_coroutine_tasks runs all coroutines.""" call_tracker = {"coro1": False, "coro2": False} async def coro1(): call_tracker["coro1"] = True async def coro2(): call_tracker["coro2"] = True executor.add_coroutine(coroutine=coro1) executor.add_coroutine(coroutine=coro2) await executor.run_coroutine_tasks() assert call_tracker["coro1"] is True assert call_tracker["coro2"] is True async def test_run_coroutine_tasks_passes_kwargs(self, executor): """Test run_coroutine_tasks passes kwargs to coroutines.""" received_kwargs = {} async def my_coro(a, b): received_kwargs["a"] = a received_kwargs["b"] = b executor.add_coroutine(coroutine=my_coro, kwargs={"a": 1, "b": 2}) await executor.run_coroutine_tasks() assert received_kwargs["a"] == 1 assert received_kwargs["b"] == 2 async def test_run_coroutine_tasks_handles_exception(self, executor): """Test run_coroutine_tasks handles exceptions gracefully.""" async def failing_coro(): raise Exception("Test error") executor.add_coroutine(coroutine=failing_coro) # Should not raise await executor.run_coroutine_tasks() async def test_run_coroutine_tasks_empty(self, executor): """Test run_coroutine_tasks with no coroutines.""" # Should not raise await executor.run_coroutine_tasks() class TestRunCoroutineTask: """Test Executor run_coroutine_task static method.""" def test_run_coroutine_task_runs_with_asyncio(self): """Test run_coroutine_task uses asyncio.run.""" async def my_coro(x): return x with patch('asyncio.run') as mock_asyncio_run: Executor.run_coroutine_task(my_coro, {"x": 42}) mock_asyncio_run.assert_called_once() class TestRunFunction: """Test Executor run_function static method.""" def test_run_function_calls_function(self): """Test run_function calls the function.""" mock_func = MagicMock() Executor.run_function(mock_func, {}) mock_func.assert_called_once_with() def test_run_function_passes_kwargs(self): """Test run_function passes kwargs.""" mock_func = MagicMock() kwargs = {"a": 1, "b": "test"} Executor.run_function(mock_func, kwargs) mock_func.assert_called_once_with(a=1, b="test") class TestSigintHandle: """Test Executor sigint_handle method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() config.shutdown = False mock_config.return_value = config return Executor() def test_sigint_handle_sets_shutdown(self, executor): """Test sigint_handle sets config.shutdown to True.""" executor.sigint_handle(None, None) assert executor.config.shutdown is True class TestExit: """Test Executor exit method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() config.shutdown = False config.force_shutdown = False config.backtest_engine = None config.task_queue = MagicMock() mock_config.return_value = config exec = Executor() exec.executor = MagicMock(spec=ThreadPoolExecutor) return exec def test_exit_with_timeout(self, executor): """Test exit respects timeout.""" executor.timeout = 0.1 executor.config.shutdown = False executor.exit() assert executor.config.shutdown is True def test_exit_stops_strategies(self, executor): """Test exit sets running=False on all strategies.""" strategy1 = MagicMock() strategy1.running = True strategy2 = MagicMock() strategy2.running = True executor.strategy_runners = [strategy1, strategy2] executor.timeout = 0.1 executor.exit() assert strategy1.running is False assert strategy2.running is False def test_exit_cancels_task_queue(self, executor): """Test exit cancels task queue.""" executor.timeout = 0.1 executor.exit() executor.config.task_queue.cancel.assert_called_once() def test_exit_shuts_down_executor(self, executor): """Test exit shuts down thread pool executor.""" executor.timeout = 0.1 executor.exit() executor.executor.shutdown.assert_called_once_with(wait=False, cancel_futures=False) def test_exit_stops_backtest_engine(self, executor): """Test exit stops backtest engine if present.""" mock_engine = MagicMock() mock_engine.stop_testing = False executor.config.backtest_engine = mock_engine executor.timeout = 0.1 executor.exit() assert mock_engine.stop_testing is True def test_exit_force_shutdown(self, executor): """Test exit with force_shutdown.""" executor.config.force_shutdown = True executor.timeout = 0.1 with patch('os._exit') as mock_exit: executor.exit() mock_exit.assert_called_once_with(1) class TestExecute: """Test Executor execute method.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() config.shutdown = True # Set to True to exit immediately config.force_shutdown = False config.backtest_engine = None config.task_queue = MagicMock() mock_config.return_value = config return Executor() def test_execute_calculates_workers(self, executor): """Test execute calculates minimum workers correctly.""" strategy = MagicMock() executor.add_strategy(strategy=strategy) def func(): pass executor.add_function(function=func) async def coro(): pass executor.add_coroutine(coroutine=coro, on_separate_thread=True) # Should need: 1 strategy + 1 function + 1 coroutine_thread + 3 = 6 workers with patch.object(ThreadPoolExecutor, '__init__', return_value=None) as mock_init: with patch.object(ThreadPoolExecutor, '__enter__', return_value=MagicMock()): with patch.object(ThreadPoolExecutor, '__exit__', return_value=None): try: executor.execute(workers=2) except: pass # Workers should be max(2, 6) = 6 # But the actual implementation uses max(workers, workers_) def test_execute_uses_minimum_workers(self, executor): """Test execute uses at least the calculated number of workers.""" # With no strategies/functions, need at least 3 workers (for internal tasks) with patch('aiomql.lib.executor.ThreadPoolExecutor') as mock_pool: mock_executor = MagicMock() mock_pool.return_value.__enter__.return_value = mock_executor try: executor.execute(workers=1) except: pass # Check that max_workers was at least 3 class TestIntegration: """Integration tests for Executor.""" @pytest.fixture def executor(self): """Create an Executor for testing.""" with patch('aiomql.lib.executor.signal'): with patch.object(Config, '__new__') as mock_config: config = MagicMock() config.shutdown = False config.force_shutdown = False config.backtest_engine = None config.task_queue = MagicMock() mock_config.return_value = config return Executor() def test_full_setup(self, executor): """Test complete executor setup.""" # Add strategies strategy1 = MagicMock(spec=Strategy) strategy2 = MagicMock(spec=Strategy) executor.add_strategies(strategies=(strategy1, strategy2)) # Add functions def my_func(x): pass executor.add_function(function=my_func, kwargs={"x": 1}) # Add coroutines async def my_coro(): pass executor.add_coroutine(coroutine=my_coro) async def my_thread_coro(): pass executor.add_coroutine(coroutine=my_thread_coro, on_separate_thread=True) assert len(executor.strategy_runners) == 2 assert len(executor.functions) == 1 assert len(executor.coroutines) == 1 assert len(executor.coroutine_threads) == 1 def test_async_and_sync_strategies(self, executor): """Test executor handles both async and sync strategies.""" async_strategy = MockAsyncStrategy() sync_strategy = MockSyncStrategy() executor.add_strategy(strategy=async_strategy) executor.add_strategy(strategy=sync_strategy) assert len(executor.strategy_runners) == 2 # Both should be runnable via run_strategy with patch('asyncio.run'): # Should not raise for either Executor.run_strategy(async_strategy) Executor.run_strategy(sync_strategy) async def test_coroutines_with_different_kwargs(self, executor): """Test running coroutines with different kwargs.""" results = [] async def collector(value): results.append(value) executor.add_coroutine(coroutine=collector, kwargs={"value": 1}) executor.add_coroutine(coroutine=collector, kwargs={"value": 2}) # Note: This won't work as expected because dicts can't have duplicate keys # This tests the behavior with a single coroutine function await executor.run_coroutine_tasks() # Only the last one will be in the dict assert 2 in results def test_timeout_functionality(self, executor): """Test timeout functionality in exit.""" executor.timeout = 0.05 executor.executor = MagicMock(spec=ThreadPoolExecutor) import time start = time.time() executor.exit() elapsed = time.time() - start # Should exit within timeout + small buffer assert elapsed < 0.2 assert executor.config.shutdown is True