diegobeyl/backtesting
2
1"""Tests for Configuration Manager"""
2import pytest
3import sys
4import json
5import tempfile
6import os
7from pathlib import Path
8from unittest.mock import MagicMock
9import threading
10
11sys.path.insert(0, str(Path(__file__).parent.parent))
12
13from api.config_manager import ConfigManager, ConfigFileHandler, get_config_manager
14from api.schemas import BacktestConfig, Timeframe
15
16
17class TestConfigManager:
18 """Tests for ConfigManager class"""
19
20 @pytest.fixture
21 def temp_config_file(self):
22 """Create a temporary config file"""
23 with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
24 json.dump({"symbol": "BTCUSD", "timeframe": "D1", "donchian_period": 1}, f)
25 temp_path = f.name
26 yield temp_path
27 if os.path.exists(temp_path):
28 os.remove(temp_path)
29
30 def test_init_with_existing_file(self, temp_config_file):
31 """Test initialization with existing config file"""
32 manager = ConfigManager(config_path=temp_config_file)
33 config = manager.get_config()
34 assert config.symbol == "BTCUSD"
35
36 def test_update_config(self, temp_config_file):
37 """Test updating configuration"""
38 manager = ConfigManager(config_path=temp_config_file)
39 new_config = BacktestConfig(symbol="ETHUSD", timeframe="W1")
40 manager.update_config(new_config)
41 assert manager.get_config().symbol == "ETHUSD"
42
43 def test_update_partial(self, temp_config_file):
44 """Test partial configuration update"""
45 manager = ConfigManager(config_path=temp_config_file)
46 manager.update_partial({"risk_percent": 2.5, "leverage": 50})
47 config = manager.get_config()
48 assert config.risk_percent == 2.5
49 assert config.leverage == 50
50
51 def test_reset_to_defaults(self, temp_config_file):
52 """Test resetting to defaults"""
53 manager = ConfigManager(config_path=temp_config_file)
54 manager.update_partial({"symbol": "ETHUSD", "donchian_period": 10})
55 manager.reset_to_defaults()
56 config = manager.get_config()
57 assert config.symbol == "BTCUSD"
58 assert config.donchian_period == 1
59
60 def test_listener_notification(self, temp_config_file):
61 """Test listener notifications"""
62 manager = ConfigManager(config_path=temp_config_file)
63 callback_called = []
64 manager.add_listener(lambda c: callback_called.append(c))
65 manager.update_partial({"risk_percent": 3.0})
66 assert len(callback_called) == 1
67
68 def test_thread_safety(self, temp_config_file):
69 """Test thread safety"""
70 manager = ConfigManager(config_path=temp_config_file)
71 errors = []
72 def worker(val):
73 try:
74 for _ in range(10):
75 manager.update_partial({"risk_percent": val})
76 manager.get_config()
77 except Exception as e:
78 errors.append(e)
79 # Use valid risk_percent values (min 0.1)
80 threads = [threading.Thread(target=worker, args=(i+0.5,)) for i in range(3)]
81 for t in threads: t.start()
82 for t in threads: t.join()
83 assert len(errors) == 0
84
85
86class TestConfigFileHandler:
87 """Tests for ConfigFileHandler"""
88
89 def test_on_modified_debounce(self):
90 """Test debounce of rapid changes"""
91 mock_manager = MagicMock()
92 handler = ConfigFileHandler(mock_manager)
93 from watchdog.events import FileModifiedEvent
94 event = FileModifiedEvent(src_path="/path/config.json")
95 handler.on_modified(event)
96 handler.on_modified(event) # Should be debounced
97 assert mock_manager.reload_config.call_count == 1
98
99 def test_ignores_non_json(self):
100 """Test non-JSON files are ignored"""
101 mock_manager = MagicMock()
102 handler = ConfigFileHandler(mock_manager)
103 from watchdog.events import FileModifiedEvent
104 handler.on_modified(FileModifiedEvent(src_path="/path/file.txt"))
105 mock_manager.reload_config.assert_not_called()
106
107
108if __name__ == "__main__":
109 pytest.main([__file__, "-v"])
110 