Files
2026-02-21 07:43:33 +01:00

1171 lines
42 KiB
Python

"""Comprehensive tests for the Bot module.
Tests cover:
- Bot initialization
- process_pool class method
- start_terminal async method
- start_terminal_sync method
- initialize async method
- initialize_sync method
- add_function method
- add_coroutine method
- execute method
- start method
- add_strategy method
- add_strategies method
- add_strategy_all method
- init_strategy async method
- init_strategies async method
- init_strategy_sync method
- init_strategies_sync method
- Integration tests
"""
import asyncio
import time
from concurrent.futures import ProcessPoolExecutor
from typing import Type
from unittest.mock import MagicMock, AsyncMock, patch, call
import pytest
from aiomql.lib.bot import Bot
from aiomql.lib.executor import Executor
from aiomql.lib.strategy import Strategy
from aiomql.lib.symbol import Symbol
from aiomql.core.config import Config
from aiomql.core.meta_trader import MetaTrader
class MockStrategy:
"""Mock strategy for testing."""
def __init__(self, symbol=None, params=None, **kwargs):
self.symbol = symbol
self.params = params or {}
self.kwargs = kwargs
self.running = True
async def initialize(self):
"""Async initialize method."""
return True
def initialize_sync(self):
"""Sync initialize method."""
return True
async def run_strategy(self):
"""Async run_strategy method."""
pass
class MockFailingStrategy:
"""Mock strategy that fails initialization."""
def __init__(self, symbol=None, params=None, **kwargs):
self.symbol = symbol
self.params = params or {}
self.running = True
async def initialize(self):
"""Async initialize method that fails."""
return False
def initialize_sync(self):
"""Sync initialize method that fails."""
return False
class TestBotInitialization:
"""Test Bot class initialization."""
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_creates_config(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init creates config instance."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
assert bot.config is not None
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_creates_executor(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init creates executor instance."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
assert bot.executor is not None
assert isinstance(bot.executor, Executor)
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_creates_empty_strategies_list(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init creates empty strategies list."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
assert bot.strategies == []
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_sets_initialized_false(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init sets initialized to False."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
assert bot.initialized is False
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_sets_login_false(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init sets login to False."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
assert bot.login is False
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_creates_metatrader_instance(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init creates MetaTrader instance."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
mock_mt = MagicMock()
mock_metatrader.return_value = mock_mt
bot = Bot()
mock_metatrader.assert_called_once()
assert bot.mt5 == mock_mt
@patch('aiomql.lib.executor.signal')
@patch.object(Config, '__new__')
@patch('aiomql.lib.bot.MetaTrader')
def test_init_passes_bot_to_config(self, mock_metatrader, mock_config_new, mock_signal):
"""Test Bot init passes itself to Config constructor."""
mock_config = MagicMock()
mock_config_new.return_value = mock_config
bot = Bot()
# Config is called with bot=self
mock_config_new.assert_called()
class TestProcessPool:
"""Test Bot process_pool class method."""
def test_process_pool_with_processes(self):
"""Test process_pool runs processes in parallel."""
call_tracker = {"called": False}
def mock_process(**kwargs):
call_tracker["called"] = True
call_tracker["kwargs"] = kwargs
with patch.object(ProcessPoolExecutor, '__init__', return_value=None):
with patch.object(ProcessPoolExecutor, '__enter__') as mock_enter:
mock_executor = MagicMock()
mock_enter.return_value = mock_executor
with patch.object(ProcessPoolExecutor, '__exit__', return_value=None):
Bot.process_pool(processes={mock_process: {"arg1": "value1"}}, num_workers=2)
mock_executor.submit.assert_called_once_with(mock_process, arg1="value1")
def test_process_pool_uses_default_workers(self):
"""Test process_pool calculates workers from processes count."""
def mock_process1(**kwargs):
pass
def mock_process2(**kwargs):
pass
with patch('aiomql.lib.bot.ProcessPoolExecutor') as mock_pool:
mock_executor = MagicMock()
mock_pool.return_value.__enter__.return_value = mock_executor
mock_pool.return_value.__exit__ = MagicMock(return_value=None)
processes = {mock_process1: {}, mock_process2: {}}
Bot.process_pool(processes=processes)
# Workers should be len(processes) + 1 = 3
mock_pool.assert_called_once_with(max_workers=3)
def test_process_pool_with_custom_workers(self):
"""Test process_pool uses custom worker count."""
def mock_process(**kwargs):
pass
with patch('aiomql.lib.bot.ProcessPoolExecutor') as mock_pool:
mock_executor = MagicMock()
mock_pool.return_value.__enter__.return_value = mock_executor
mock_pool.return_value.__exit__ = MagicMock(return_value=None)
Bot.process_pool(processes={mock_process: {}}, num_workers=5)
mock_pool.assert_called_once_with(max_workers=5)
def test_process_pool_multiple_processes(self):
"""Test process_pool submits all processes."""
def mock_process1(**kwargs):
pass
def mock_process2(**kwargs):
pass
def mock_process3(**kwargs):
pass
with patch.object(ProcessPoolExecutor, '__init__', return_value=None):
with patch.object(ProcessPoolExecutor, '__enter__') as mock_enter:
mock_executor = MagicMock()
mock_enter.return_value = mock_executor
with patch.object(ProcessPoolExecutor, '__exit__', return_value=None):
processes = {
mock_process1: {"x": 1},
mock_process2: {"y": 2},
mock_process3: {},
}
Bot.process_pool(processes=processes, num_workers=4)
assert mock_executor.submit.call_count == 3
class TestStartTerminal:
"""Test Bot start_terminal and start_terminal_sync methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
bot.mt5 = mock_mt_instance
return bot
async def test_start_terminal_success(self, bot):
"""Test start_terminal with successful login."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
result = await bot.start_terminal()
assert result is True
assert bot.initialized is True
assert bot.login is True
async def test_start_terminal_initialize_fails(self, bot):
"""Test start_terminal when initialize fails."""
bot.mt5.initialize = AsyncMock(return_value=False)
result = await bot.start_terminal()
assert result is False
assert bot.initialized is False
assert bot.login is False
async def test_start_terminal_login_fails(self, bot):
"""Test start_terminal when login fails."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=False)
result = await bot.start_terminal()
assert result is False
assert bot.initialized is True
assert bot.login is False
async def test_start_terminal_calls_initialize_then_login(self, bot):
"""Test start_terminal calls initialize before login."""
call_order = []
bot.mt5.initialize = AsyncMock(return_value=True, side_effect=lambda: call_order.append("init") or True)
bot.mt5.login = AsyncMock(return_value=True, side_effect=lambda: call_order.append("login") or True)
await bot.start_terminal()
assert call_order == ["init", "login"]
async def test_start_terminal_skips_login_when_init_fails(self, bot):
"""Test start_terminal does not call login when initialize fails."""
bot.mt5.initialize = AsyncMock(return_value=False)
bot.mt5.login = AsyncMock(return_value=True)
await bot.start_terminal()
bot.mt5.login.assert_not_called()
def test_start_terminal_sync_success(self, bot):
"""Test start_terminal_sync with successful login."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
result = bot.start_terminal_sync()
assert result is True
assert bot.initialized is True
assert bot.login is True
def test_start_terminal_sync_initialize_fails(self, bot):
"""Test start_terminal_sync when initialize fails."""
bot.mt5.initialize_sync = MagicMock(return_value=False)
result = bot.start_terminal_sync()
assert result is False
assert bot.initialized is False
assert bot.login is False
def test_start_terminal_sync_login_fails(self, bot):
"""Test start_terminal_sync when login fails."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=False)
result = bot.start_terminal_sync()
assert result is False
assert bot.initialized is True
assert bot.login is False
def test_start_terminal_sync_skips_login_when_init_fails(self, bot):
"""Test start_terminal_sync does not call login_sync when initialize fails."""
bot.mt5.initialize_sync = MagicMock(return_value=False)
bot.mt5.login_sync = MagicMock(return_value=True)
bot.start_terminal_sync()
bot.mt5.login_sync.assert_not_called()
class TestInitialize:
"""Test Bot initialize and initialize_sync methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config.shutdown = False
mock_config.task_queue = MagicMock()
mock_config.task_queue.run = AsyncMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
bot.mt5 = mock_mt_instance
return bot
async def test_initialize_successful_login(self, bot):
"""Test initialize with successful login."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
await bot.initialize()
assert bot.login is True
async def test_initialize_failed_login_raises_system_exit(self, bot):
"""Test initialize raises SystemExit on failed login."""
bot.mt5.initialize = AsyncMock(return_value=False)
with pytest.raises(SystemExit):
await bot.initialize()
async def test_initialize_adds_task_queue_coroutine(self, bot):
"""Test initialize adds task_queue.run to executor."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot.executor, 'add_coroutine') as mock_add_coro:
await bot.initialize()
# Check that task_queue.run was added
assert mock_add_coro.called
async def test_initialize_adds_exit_function(self, bot):
"""Test initialize adds executor.exit to functions."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot.executor, 'add_function') as mock_add_func:
await bot.initialize()
mock_add_func.assert_called()
async def test_initialize_no_strategies_sets_shutdown(self, bot):
"""Test initialize sets shutdown when no strategies."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
bot.executor.strategy_runners = []
await bot.initialize()
assert bot.config.shutdown is True
async def test_initialize_with_strategies(self, bot):
"""Test initialize with strategies does not shutdown."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
mock_strategy = MockStrategy()
bot.strategies.append(mock_strategy)
await bot.initialize()
assert bot.config.shutdown is False
async def test_initialize_calls_init_strategies(self, bot):
"""Test initialize calls init_strategies."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot, 'init_strategies', new_callable=AsyncMock) as mock_init_strats:
await bot.initialize()
mock_init_strats.assert_called_once()
def test_initialize_sync_successful_login(self, bot):
"""Test initialize_sync with successful login."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
bot.initialize_sync()
assert bot.login is True
def test_initialize_sync_failed_login_raises_system_exit(self, bot):
"""Test initialize_sync raises SystemExit on failed login."""
bot.mt5.initialize_sync = MagicMock(return_value=False)
with pytest.raises(SystemExit):
bot.initialize_sync()
def test_initialize_sync_adds_task_queue_coroutine(self, bot):
"""Test initialize_sync adds task_queue.run to executor."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot.executor, 'add_coroutine') as mock_add_coro:
bot.initialize_sync()
assert mock_add_coro.called
def test_initialize_sync_adds_exit_function(self, bot):
"""Test initialize_sync adds executor.exit to functions."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot.executor, 'add_function') as mock_add_func:
bot.initialize_sync()
mock_add_func.assert_called()
def test_initialize_sync_no_strategies_sets_shutdown(self, bot):
"""Test initialize_sync sets shutdown when no strategies."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
bot.executor.strategy_runners = []
bot.initialize_sync()
assert bot.config.shutdown is True
def test_initialize_sync_calls_init_strategies_sync(self, bot):
"""Test initialize_sync calls init_strategies_sync."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot, 'init_strategies_sync') as mock_init_strats:
bot.initialize_sync()
mock_init_strats.assert_called_once()
class TestAddFunctionAndCoroutine:
"""Test Bot add_function and add_coroutine methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader'):
return Bot()
def test_add_function_without_kwargs(self, bot):
"""Test add_function without kwargs."""
def my_function():
pass
with patch.object(bot.executor, 'add_function') as mock_add:
bot.add_function(function=my_function)
mock_add.assert_called_once_with(function=my_function, kwargs={})
def test_add_function_with_kwargs(self, bot):
"""Test add_function with kwargs."""
def my_function(a, b):
pass
with patch.object(bot.executor, 'add_function') as mock_add:
bot.add_function(function=my_function, a=1, b=2)
mock_add.assert_called_once_with(function=my_function, kwargs={"a": 1, "b": 2})
def test_add_coroutine_without_kwargs(self, bot):
"""Test add_coroutine without kwargs."""
async def my_coroutine():
pass
with patch.object(bot.executor, 'add_coroutine') as mock_add:
bot.add_coroutine(coroutine=my_coroutine)
mock_add.assert_called_once_with(coroutine=my_coroutine, kwargs={}, on_separate_thread=False)
def test_add_coroutine_with_kwargs(self, bot):
"""Test add_coroutine with kwargs."""
async def my_coroutine(a, b):
pass
with patch.object(bot.executor, 'add_coroutine') as mock_add:
bot.add_coroutine(coroutine=my_coroutine, a=1, b=2)
mock_add.assert_called_once_with(coroutine=my_coroutine, kwargs={"a": 1, "b": 2}, on_separate_thread=False)
def test_add_coroutine_on_separate_thread(self, bot):
"""Test add_coroutine with on_separate_thread=True."""
async def my_coroutine():
pass
with patch.object(bot.executor, 'add_coroutine') as mock_add:
bot.add_coroutine(coroutine=my_coroutine, on_separate_thread=True)
mock_add.assert_called_once_with(coroutine=my_coroutine, kwargs={}, on_separate_thread=True)
class TestExecuteAndStart:
"""Test Bot execute and start methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config.shutdown = False
mock_config.task_queue = MagicMock()
mock_config.task_queue.run = AsyncMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
bot.mt5 = mock_mt_instance
return bot
def test_execute_calls_initialize_sync(self, bot):
"""Test execute calls initialize_sync."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot, 'initialize_sync') as mock_init:
with patch.object(bot.executor, 'execute'):
bot.config.shutdown = True # Exit immediately
bot.execute()
mock_init.assert_called_once()
def test_execute_calls_executor_execute_when_not_shutdown(self, bot):
"""Test execute calls executor.execute when not shutdown."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot, 'initialize_sync'):
with patch.object(bot.executor, 'execute') as mock_exec:
bot.config.shutdown = False
bot.execute()
mock_exec.assert_called_once()
def test_execute_skips_executor_when_shutdown(self, bot):
"""Test execute skips executor.execute when shutdown."""
bot.mt5.initialize_sync = MagicMock(return_value=True)
bot.mt5.login_sync = MagicMock(return_value=True)
with patch.object(bot, 'initialize_sync'):
with patch.object(bot.executor, 'execute') as mock_exec:
bot.config.shutdown = True
bot.execute()
mock_exec.assert_not_called()
async def test_start_calls_initialize(self, bot):
"""Test start calls initialize."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot, 'initialize', new_callable=AsyncMock) as mock_init:
with patch.object(bot.executor, 'execute'):
bot.config.shutdown = True # Exit immediately
await bot.start()
mock_init.assert_called_once()
async def test_start_calls_executor_execute_when_not_shutdown(self, bot):
"""Test start calls executor.execute when not shutdown."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot, 'initialize', new_callable=AsyncMock):
with patch.object(bot.executor, 'execute') as mock_exec:
bot.config.shutdown = False
await bot.start()
mock_exec.assert_called_once()
async def test_start_skips_executor_when_shutdown(self, bot):
"""Test start skips executor.execute when shutdown."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
with patch.object(bot, 'initialize', new_callable=AsyncMock):
with patch.object(bot.executor, 'execute') as mock_exec:
bot.config.shutdown = True
await bot.start()
mock_exec.assert_not_called()
class TestAddStrategy:
"""Test Bot add_strategy, add_strategies, and add_strategy_all methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader'):
return Bot()
def test_add_strategy_appends_to_list(self, bot):
"""Test add_strategy appends strategy to list."""
strategy = MockStrategy()
bot.add_strategy(strategy=strategy)
assert len(bot.strategies) == 1
assert strategy in bot.strategies
def test_add_strategy_multiple(self, bot):
"""Test adding multiple strategies one by one."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.add_strategy(strategy=strategy1)
bot.add_strategy(strategy=strategy2)
assert len(bot.strategies) == 2
assert strategy1 in bot.strategies
assert strategy2 in bot.strategies
def test_add_strategies_batch(self, bot):
"""Test add_strategies adds multiple strategies at once."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
strategy3 = MockStrategy()
bot.add_strategies(strategies=[strategy1, strategy2, strategy3])
assert len(bot.strategies) == 3
assert strategy1 in bot.strategies
assert strategy2 in bot.strategies
assert strategy3 in bot.strategies
def test_add_strategies_extends_existing(self, bot):
"""Test add_strategies extends existing strategies."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.add_strategy(strategy=strategy1)
bot.add_strategies(strategies=[strategy2])
assert len(bot.strategies) == 2
def test_add_strategies_with_tuple(self, bot):
"""Test add_strategies works with tuple input."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.add_strategies(strategies=(strategy1, strategy2))
assert len(bot.strategies) == 2
def test_add_strategies_with_generator(self, bot):
"""Test add_strategies works with generator input."""
def strategy_gen():
yield MockStrategy()
yield MockStrategy()
bot.add_strategies(strategies=strategy_gen())
assert len(bot.strategies) == 2
def test_add_strategy_all_creates_strategy_per_symbol(self, bot):
"""Test add_strategy_all creates strategy for each symbol."""
mock_symbol1 = MagicMock(spec=Symbol)
mock_symbol2 = MagicMock(spec=Symbol)
mock_symbol3 = MagicMock(spec=Symbol)
bot.add_strategy_all(
strategy=MockStrategy,
params={"param1": "value1"},
symbols=[mock_symbol1, mock_symbol2, mock_symbol3]
)
assert len(bot.strategies) == 3
def test_add_strategy_all_passes_params(self, bot):
"""Test add_strategy_all passes params to each strategy."""
mock_symbol = MagicMock(spec=Symbol)
bot.add_strategy_all(
strategy=MockStrategy,
params={"param1": "value1"},
symbols=[mock_symbol]
)
assert bot.strategies[0].params == {"param1": "value1"}
def test_add_strategy_all_passes_kwargs(self, bot):
"""Test add_strategy_all passes additional kwargs."""
mock_symbol = MagicMock(spec=Symbol)
bot.add_strategy_all(
strategy=MockStrategy,
symbols=[mock_symbol],
extra_arg="extra_value"
)
assert bot.strategies[0].kwargs.get("extra_arg") == "extra_value"
def test_add_strategy_all_assigns_correct_symbols(self, bot):
"""Test add_strategy_all assigns the correct symbol to each strategy."""
mock_symbol1 = MagicMock(spec=Symbol)
mock_symbol1.name = "EURUSD"
mock_symbol2 = MagicMock(spec=Symbol)
mock_symbol2.name = "GBPUSD"
bot.add_strategy_all(
strategy=MockStrategy,
symbols=[mock_symbol1, mock_symbol2]
)
assert bot.strategies[0].symbol == mock_symbol1
assert bot.strategies[1].symbol == mock_symbol2
class TestInitStrategy:
"""Test Bot init_strategy, init_strategies, init_strategy_sync, and init_strategies_sync methods."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader'):
return Bot()
async def test_init_strategy_success_adds_to_executor(self, bot):
"""Test init_strategy adds successful strategy to executor."""
strategy = MockStrategy()
with patch.object(bot.executor, 'add_strategy') as mock_add:
result = await bot.init_strategy(strategy=strategy)
assert result is True
mock_add.assert_called_once_with(strategy=strategy)
async def test_init_strategy_failure_does_not_add(self, bot):
"""Test init_strategy does not add failing strategy."""
strategy = MockFailingStrategy()
with patch.object(bot.executor, 'add_strategy') as mock_add:
result = await bot.init_strategy(strategy=strategy)
assert result is False
mock_add.assert_not_called()
async def test_init_strategy_returns_bool(self, bot):
"""Test init_strategy returns boolean result."""
strategy = MockStrategy()
with patch.object(bot.executor, 'add_strategy'):
result = await bot.init_strategy(strategy=strategy)
assert isinstance(result, bool)
async def test_init_strategies_initializes_all(self, bot):
"""Test init_strategies initializes all strategies."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.strategies = [strategy1, strategy2]
with patch.object(bot.executor, 'add_strategy'):
await bot.init_strategies()
# Both strategies should be in executor
assert bot.executor.add_strategy.call_count == 2
async def test_init_strategies_handles_failures(self, bot):
"""Test init_strategies handles failing strategies."""
strategy1 = MockStrategy()
strategy2 = MockFailingStrategy()
bot.strategies = [strategy1, strategy2]
with patch.object(bot.executor, 'add_strategy'):
await bot.init_strategies()
# Only successful strategy should be added
assert bot.executor.add_strategy.call_count == 1
async def test_init_strategies_uses_gather(self, bot):
"""Test init_strategies uses asyncio.gather for concurrent initialization."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
strategy3 = MockStrategy()
bot.strategies = [strategy1, strategy2, strategy3]
with patch.object(bot.executor, 'add_strategy'):
with patch('aiomql.lib.bot.asyncio.gather', new_callable=AsyncMock, return_value=[True, True, True]) as mock_gather:
await bot.init_strategies()
mock_gather.assert_called_once()
def test_init_strategy_sync_success_adds_to_executor(self, bot):
"""Test init_strategy_sync adds successful strategy to executor."""
strategy = MockStrategy()
with patch.object(bot.executor, 'add_strategy') as mock_add:
result = bot.init_strategy_sync(strategy=strategy)
assert result is True
mock_add.assert_called_once_with(strategy=strategy)
def test_init_strategy_sync_failure_does_not_add(self, bot):
"""Test init_strategy_sync does not add failing strategy."""
strategy = MockFailingStrategy()
with patch.object(bot.executor, 'add_strategy') as mock_add:
result = bot.init_strategy_sync(strategy=strategy)
assert result is False
mock_add.assert_not_called()
def test_init_strategies_sync_initializes_all(self, bot):
"""Test init_strategies_sync initializes all strategies."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.strategies = [strategy1, strategy2]
with patch.object(bot.executor, 'add_strategy'):
bot.init_strategies_sync()
# Both strategies should be in executor
assert bot.executor.add_strategy.call_count == 2
def test_init_strategies_sync_handles_failures(self, bot):
"""Test init_strategies_sync handles failing strategies."""
strategy1 = MockStrategy()
strategy2 = MockFailingStrategy()
bot.strategies = [strategy1, strategy2]
with patch.object(bot.executor, 'add_strategy'):
bot.init_strategies_sync()
# Only successful strategy should be added
assert bot.executor.add_strategy.call_count == 1
def test_init_strategies_sync_sequential(self, bot):
"""Test init_strategies_sync initializes strategies sequentially."""
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.strategies = [strategy1, strategy2]
with patch.object(bot.executor, 'add_strategy') as mock_add:
bot.init_strategies_sync()
# Verify both were added
calls = mock_add.call_args_list
assert len(calls) == 2
assert calls[0] == call(strategy=strategy1)
assert calls[1] == call(strategy=strategy2)
class TestIntegration:
"""Integration tests for Bot."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config.shutdown = False
mock_config.task_queue = MagicMock()
mock_config.task_queue.run = AsyncMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
bot.mt5 = mock_mt_instance
return bot
def test_full_setup_with_strategies_functions_coroutines(self, bot):
"""Test complete bot setup with strategies, functions, and coroutines."""
# Add strategies
strategy1 = MockStrategy()
strategy2 = MockStrategy()
bot.add_strategies(strategies=[strategy1, strategy2])
# Add functions
def my_func(x):
pass
bot.add_function(function=my_func, x=1)
# Add coroutines
async def my_coro():
pass
bot.add_coroutine(coroutine=my_coro)
async def my_thread_coro():
pass
bot.add_coroutine(coroutine=my_thread_coro, on_separate_thread=True)
assert len(bot.strategies) == 2
assert my_func in bot.executor.functions
assert my_coro in bot.executor.coroutines
assert my_thread_coro in bot.executor.coroutine_threads
async def test_full_async_workflow(self, bot):
"""Test complete async workflow."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
strategy = MockStrategy()
bot.add_strategy(strategy=strategy)
await bot.init_strategies()
assert len(bot.executor.strategy_runners) == 1
def test_full_sync_workflow(self, bot):
"""Test complete sync workflow."""
strategy = MockStrategy()
bot.add_strategy(strategy=strategy)
bot.init_strategies_sync()
assert len(bot.executor.strategy_runners) == 1
def test_add_strategy_all_workflow(self, bot):
"""Test add_strategy_all creates proper strategies."""
mock_symbol1 = MagicMock(spec=Symbol)
mock_symbol1.name = "EURUSD"
mock_symbol2 = MagicMock(spec=Symbol)
mock_symbol2.name = "GBPUSD"
bot.add_strategy_all(
strategy=MockStrategy,
params={"risk": 0.01},
symbols=[mock_symbol1, mock_symbol2]
)
assert len(bot.strategies) == 2
assert bot.strategies[0].symbol == mock_symbol1
assert bot.strategies[1].symbol == mock_symbol2
assert bot.strategies[0].params == {"risk": 0.01}
assert bot.strategies[1].params == {"risk": 0.01}
async def test_mixed_strategy_initialization(self, bot):
"""Test initialization with mix of successful and failing strategies."""
success_strategy = MockStrategy()
failure_strategy = MockFailingStrategy()
bot.add_strategy(strategy=success_strategy)
bot.add_strategy(strategy=failure_strategy)
await bot.init_strategies()
# Only successful strategy should be added to executor
assert len(bot.executor.strategy_runners) == 1
def test_bot_always_uses_metatrader(self):
"""Test bot always creates MetaTrader instance."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
mock_mt.assert_called_once()
assert bot.mt5 == mock_mt_instance
class TestEdgeCases:
"""Test edge cases and error conditions."""
@pytest.fixture
def bot(self):
"""Create a Bot instance for testing."""
with patch('aiomql.lib.executor.signal'):
with patch.object(Config, '__new__') as mock_config_new:
mock_config = MagicMock()
mock_config.shutdown = False
mock_config.task_queue = MagicMock()
mock_config.task_queue.run = AsyncMock()
mock_config_new.return_value = mock_config
with patch('aiomql.lib.bot.MetaTrader') as mock_mt:
mock_mt_instance = MagicMock()
mock_mt.return_value = mock_mt_instance
bot = Bot()
bot.mt5 = mock_mt_instance
return bot
def test_add_strategies_empty_list(self, bot):
"""Test add_strategies with empty list."""
bot.add_strategies(strategies=[])
assert len(bot.strategies) == 0
def test_add_strategy_all_empty_symbols(self, bot):
"""Test add_strategy_all with empty symbols list."""
bot.add_strategy_all(strategy=MockStrategy, symbols=[])
assert len(bot.strategies) == 0
def test_add_strategy_all_none_params(self, bot):
"""Test add_strategy_all with None params."""
mock_symbol = MagicMock(spec=Symbol)
bot.add_strategy_all(strategy=MockStrategy, params=None, symbols=[mock_symbol])
assert len(bot.strategies) == 1
assert bot.strategies[0].params is None or bot.strategies[0].params == {}
async def test_init_strategies_empty_list(self, bot):
"""Test init_strategies with no strategies."""
bot.strategies = []
await bot.init_strategies()
assert len(bot.executor.strategy_runners) == 0
def test_init_strategies_sync_empty_list(self, bot):
"""Test init_strategies_sync with no strategies."""
bot.strategies = []
bot.init_strategies_sync()
assert len(bot.executor.strategy_runners) == 0
async def test_initialize_exception_handling(self, bot):
"""Test initialize handles exceptions properly."""
bot.mt5.initialize = AsyncMock(side_effect=Exception("Test error"))
with pytest.raises(SystemExit):
await bot.initialize()
def test_initialize_sync_exception_handling(self, bot):
"""Test initialize_sync handles exceptions properly."""
bot.mt5.initialize_sync = MagicMock(side_effect=Exception("Test error"))
with pytest.raises(SystemExit):
bot.initialize_sync()
async def test_multiple_strategy_initialization_all_fail(self, bot):
"""Test init_strategies when all strategies fail."""
strategy1 = MockFailingStrategy()
strategy2 = MockFailingStrategy()
bot.strategies = [strategy1, strategy2]
await bot.init_strategies()
assert len(bot.executor.strategy_runners) == 0
def test_multiple_strategy_sync_initialization_all_fail(self, bot):
"""Test init_strategies_sync when all strategies fail."""
strategy1 = MockFailingStrategy()
strategy2 = MockFailingStrategy()
bot.strategies = [strategy1, strategy2]
bot.init_strategies_sync()
assert len(bot.executor.strategy_runners) == 0
async def test_start_terminal_return_value_propagated(self, bot):
"""Test start_terminal return value is the result of the last operation."""
bot.mt5.initialize = AsyncMock(return_value=True)
bot.mt5.login = AsyncMock(return_value=True)
result = await bot.start_terminal()
assert result is True
def test_execute_checks_shutdown_after_initialize(self, bot):
"""Test execute checks config.shutdown after initialize_sync."""
call_order = []
def mock_init():
call_order.append("init")
bot.config.shutdown = True # Set shutdown during init
with patch.object(bot, 'initialize_sync', side_effect=mock_init):
with patch.object(bot.executor, 'execute') as mock_exec:
bot.execute()
# initialize_sync should be called but executor.execute should not
assert call_order == ["init"]
mock_exec.assert_not_called()