|
|
|
"""
|
|
Comprehensive Pytest configuration file for MMORPG test suite.
|
|
Provides shared fixtures, test configuration, and testing utilities.
|
|
"""
|
|
|
|
import pytest
|
|
import sys
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
from typing import Generator, Dict, Any, List
|
|
from unittest.mock import Mock, patch
|
|
import threading
|
|
import time
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
|
|
from src.core.game_engine import GameEngine
|
|
from src.facades.game_facade import GameFacade
|
|
|
|
|
|
|
|
from src.core.player import Player
|
|
from src.core.world import GameWorld
|
|
|
|
|
|
class TestGameEngine(GameEngine):
|
|
"""Test version of GameEngine that can be reset between tests."""
|
|
|
|
def __new__(cls):
|
|
|
|
if not hasattr(cls, '_test_instance') or cls._test_instance is None:
|
|
with cls._lock:
|
|
if not hasattr(cls, '_test_instance') or cls._test_instance is None:
|
|
cls._test_instance = object.__new__(cls)
|
|
return cls._test_instance
|
|
|
|
def __init__(self):
|
|
if hasattr(self, '_test_initialized'):
|
|
return
|
|
|
|
with self._lock:
|
|
if hasattr(self, '_test_initialized'):
|
|
return
|
|
|
|
self._test_initialized = True
|
|
self._running = False
|
|
self._services: Dict[str, Any] = {}
|
|
|
|
|
|
from src.core.world import GameWorld
|
|
self._game_world = GameWorld()
|
|
|
|
|
|
self._initialize_services_no_threads()
|
|
|
|
def _initialize_services_no_threads(self):
|
|
"""Initialize services without background threads for testing."""
|
|
from src.services.player_service import PlayerService
|
|
from src.services.chat_service import ChatService
|
|
from src.services.npc_service import NPCService
|
|
from src.services.mcp_service import MCPService
|
|
from src.services.plugin_service import PluginService
|
|
|
|
|
|
try:
|
|
self._services['player'] = PlayerService(self._game_world)
|
|
self._services['plugin'] = PluginService()
|
|
self._services['chat'] = ChatService(self._game_world, plugin_service=self._services['plugin'])
|
|
|
|
|
|
npc_service = NPCService(self._game_world)
|
|
|
|
npc_service.start_movement_system = lambda: None
|
|
self._services['npc'] = npc_service
|
|
|
|
self._services['mcp'] = MCPService(
|
|
player_service=self._services['player'],
|
|
npc_service=self._services['npc'],
|
|
chat_service=self._services['chat'],
|
|
game_world=self._game_world
|
|
)
|
|
except Exception as e:
|
|
print(f"Error initializing test services: {e}")
|
|
|
|
@classmethod
|
|
def reset_instance(cls):
|
|
"""Reset the singleton instance for testing."""
|
|
with cls._lock:
|
|
if hasattr(cls, '_test_instance') and cls._test_instance and hasattr(cls._test_instance, '_services'):
|
|
|
|
for service_name, service in cls._test_instance._services.items():
|
|
if hasattr(service, 'stop_movement_system'):
|
|
try:
|
|
service.stop_movement_system()
|
|
except Exception as e:
|
|
print(f"Warning: Error stopping {service_name} movement system: {e}")
|
|
cls._test_instance = None
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def setup_test_environment():
|
|
"""Set up the test environment and clean up after all tests."""
|
|
|
|
test_dir = tempfile.mkdtemp(prefix="mmorpg_test_")
|
|
os.environ['MMORPG_TEST_DIR'] = test_dir
|
|
|
|
yield test_dir
|
|
|
|
|
|
if os.path.exists(test_dir):
|
|
shutil.rmtree(test_dir)
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def game_engine() -> Generator[GameEngine, None, None]:
|
|
"""Provide a fresh game engine instance for each test."""
|
|
|
|
TestGameEngine.reset_instance()
|
|
|
|
|
|
with patch('src.core.game_engine.GameEngine', TestGameEngine):
|
|
engine = TestGameEngine()
|
|
yield engine
|
|
|
|
|
|
engine.stop()
|
|
TestGameEngine.reset_instance()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def game_facade(game_engine) -> Generator[GameFacade, None, None]:
|
|
"""Provide a game facade instance for tests."""
|
|
facade = GameFacade()
|
|
yield facade
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def game_world() -> Generator[GameWorld, None, None]:
|
|
"""Provide a fresh game world instance for tests."""
|
|
world = GameWorld()
|
|
yield world
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def ui_components(game_facade) -> Generator[tuple, None, None]:
|
|
"""Provide UI components for tests."""
|
|
from unittest.mock import Mock, MagicMock
|
|
|
|
|
|
ui = Mock()
|
|
ui.game_facade = game_facade
|
|
ui.get_keyboard_script = Mock(return_value="""
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'w' || event.key === 'W') {
|
|
event.preventDefault();
|
|
}
|
|
if (event.key === 's' || event.key === 'S') {
|
|
event.preventDefault();
|
|
}
|
|
if (event.key === 'a' || event.key === 'A') {
|
|
event.preventDefault();
|
|
}
|
|
if (event.key === 'd' || event.key === 'D') {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
""")
|
|
ui.generate_world_html = Mock(return_value="<div>Mock World HTML</div>")
|
|
ui.create_interface = Mock(return_value=MagicMock())
|
|
|
|
interface_manager = Mock()
|
|
interface_manager.game_facade = game_facade
|
|
interface_manager.ui = ui
|
|
interface_manager.handle_movement = Mock(return_value={"success": True})
|
|
interface_manager.handle_chat = Mock(return_value={"success": True})
|
|
interface_manager.setup_event_handlers = Mock()
|
|
|
|
yield ui, interface_manager
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def test_player(game_facade) -> Generator[str, None, None]:
|
|
"""Create a test player and clean up after test."""
|
|
player_id = game_facade.join_game("TestPlayer")
|
|
yield player_id
|
|
|
|
|
|
try:
|
|
game_facade.leave_game(player_id)
|
|
except:
|
|
pass
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def multiple_test_players(game_facade) -> Generator[List[str], None, None]:
|
|
"""Create multiple test players for multiplayer testing."""
|
|
player_ids = []
|
|
|
|
for i in range(3):
|
|
player_id = game_facade.join_game(f"TestPlayer{i+1}")
|
|
player_ids.append(player_id)
|
|
|
|
yield player_ids
|
|
|
|
|
|
for player_id in player_ids:
|
|
try:
|
|
game_facade.leave_game(player_id)
|
|
except:
|
|
pass
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def sample_player_data() -> Dict[str, Any]:
|
|
"""Provide sample player data for testing."""
|
|
return {
|
|
"name": "TestPlayer",
|
|
"x": 100,
|
|
"y": 150,
|
|
"health": 100,
|
|
"level": 1,
|
|
"experience": 0,
|
|
"gold": 50,
|
|
"type": "human"
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def sample_npc_data() -> List[Dict[str, Any]]:
|
|
"""Provide sample NPC data for testing."""
|
|
return [
|
|
{
|
|
"id": "test_npc_1",
|
|
"name": "Test Merchant",
|
|
"x": 200,
|
|
"y": 200,
|
|
"char": "🏪",
|
|
"personality": "friendly"
|
|
},
|
|
{
|
|
"id": "test_npc_2",
|
|
"name": "Test Guard",
|
|
"x": 300,
|
|
"y": 100,
|
|
"char": "🛡️",
|
|
"personality": "stern"
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def sample_chat_messages() -> List[Dict[str, Any]]:
|
|
"""Provide sample chat messages for testing."""
|
|
return [
|
|
{
|
|
"sender": "TestPlayer1",
|
|
"message": "Hello everyone!",
|
|
"type": "public",
|
|
"timestamp": "2024-01-01 10:00:00"
|
|
},
|
|
{
|
|
"sender": "TestPlayer2",
|
|
"message": "Hi there!",
|
|
"type": "public",
|
|
"timestamp": "2024-01-01 10:01:00"
|
|
},
|
|
{
|
|
"sender": "TestPlayer1",
|
|
"message": "Secret message",
|
|
"type": "private",
|
|
"recipient": "TestPlayer2",
|
|
"timestamp": "2024-01-01 10:02:00"
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def mock_gradio_components():
|
|
"""Provide mock Gradio components for UI testing."""
|
|
mock_components = {}
|
|
|
|
|
|
mock_components['textbox'] = Mock()
|
|
mock_components['button'] = Mock()
|
|
mock_components['html'] = Mock()
|
|
mock_components['json'] = Mock()
|
|
mock_components['dataframe'] = Mock()
|
|
mock_components['chatbot'] = Mock()
|
|
mock_components['dropdown'] = Mock()
|
|
mock_components['checkbox'] = Mock()
|
|
|
|
return mock_components
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def mock_file_system():
|
|
"""Provide a mock file system for testing."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
|
plugins_dir = os.path.join(temp_dir, "plugins")
|
|
os.makedirs(plugins_dir, exist_ok=True)
|
|
|
|
|
|
sample_plugin = os.path.join(plugins_dir, "test_plugin.py")
|
|
with open(sample_plugin, 'w') as f:
|
|
f.write("""
|
|
class TestPlugin:
|
|
def __init__(self):
|
|
self.name = "test_plugin"
|
|
|
|
def activate(self):
|
|
return True
|
|
|
|
def deactivate(self):
|
|
return True
|
|
""")
|
|
|
|
yield temp_dir
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def mock_mcp_client():
|
|
"""Provide a mock MCP client for testing."""
|
|
mock_client = Mock()
|
|
mock_client.connect.return_value = True
|
|
mock_client.disconnect.return_value = True
|
|
mock_client.send_message.return_value = {"status": "success"}
|
|
mock_client.receive_message.return_value = {"type": "response", "data": {}}
|
|
|
|
return mock_client
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def performance_timer():
|
|
"""Provide a performance timer for benchmarking tests."""
|
|
class PerformanceTimer:
|
|
def __init__(self):
|
|
self.start_time = None
|
|
self.end_time = None
|
|
|
|
def start(self):
|
|
self.start_time = time.time()
|
|
|
|
def stop(self):
|
|
self.end_time = time.time()
|
|
return self.end_time - self.start_time
|
|
|
|
def elapsed(self):
|
|
if self.start_time and self.end_time:
|
|
return self.end_time - self.start_time
|
|
return None
|
|
|
|
return PerformanceTimer()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def thread_pool():
|
|
"""Provide a thread pool for concurrency testing."""
|
|
threads = []
|
|
|
|
def create_thread(target, *args, **kwargs):
|
|
thread = threading.Thread(target=target, args=args, kwargs=kwargs)
|
|
threads.append(thread)
|
|
return thread
|
|
|
|
yield create_thread
|
|
|
|
|
|
for thread in threads:
|
|
if thread.is_alive():
|
|
thread.join(timeout=5.0)
|
|
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Configure pytest markers."""
|
|
config.addinivalue_line("markers", "unit: Unit tests")
|
|
config.addinivalue_line("markers", "integration: Integration tests")
|
|
config.addinivalue_line("markers", "e2e: End-to-end tests")
|
|
config.addinivalue_line("markers", "performance: Performance tests")
|
|
config.addinivalue_line("markers", "security: Security tests")
|
|
config.addinivalue_line("markers", "smoke: Smoke tests")
|
|
config.addinivalue_line("markers", "slow: Slow running tests")
|
|
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
"""Modify test collection to add automatic markers."""
|
|
for item in items:
|
|
|
|
if "unit" in str(item.fspath):
|
|
item.add_marker(pytest.mark.unit)
|
|
elif "integration" in str(item.fspath):
|
|
item.add_marker(pytest.mark.integration)
|
|
elif "e2e" in str(item.fspath):
|
|
item.add_marker(pytest.mark.e2e)
|
|
elif "performance" in str(item.fspath):
|
|
item.add_marker(pytest.mark.performance)
|
|
item.add_marker(pytest.mark.slow)
|
|
elif "security" in str(item.fspath):
|
|
item.add_marker(pytest.mark.security)
|
|
elif "smoke" in str(item.fspath):
|
|
item.add_marker(pytest.mark.smoke)
|
|
|
|
|
|
|
|
class TestUtilities:
|
|
"""Utility functions for testing."""
|
|
|
|
@staticmethod
|
|
def wait_for_condition(condition_func, timeout=5.0, interval=0.1):
|
|
"""Wait for a condition to become true."""
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout:
|
|
if condition_func():
|
|
return True
|
|
time.sleep(interval)
|
|
return False
|
|
|
|
@staticmethod
|
|
def assert_eventually(condition_func, timeout=5.0, message="Condition not met"):
|
|
"""Assert that a condition eventually becomes true."""
|
|
if not TestUtilities.wait_for_condition(condition_func, timeout):
|
|
raise AssertionError(message)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_utils():
|
|
"""Provide test utilities."""
|
|
return TestUtilities
|
|
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def benchmark_tracker():
|
|
"""Provide benchmark tracking for performance tests."""
|
|
class BenchmarkTracker:
|
|
def __init__(self):
|
|
self.metrics = {}
|
|
|
|
def track(self, name, value, unit="ms"):
|
|
self.metrics[name] = {"value": value, "unit": unit}
|
|
|
|
def get_metrics(self):
|
|
return self.metrics
|
|
|
|
def assert_performance(self, name, max_value, message=None):
|
|
if name not in self.metrics:
|
|
raise AssertionError(f"Metric '{name}' not tracked")
|
|
|
|
actual = self.metrics[name]["value"]
|
|
if actual > max_value:
|
|
msg = message or f"Performance regression: {name} = {actual}, expected <= {max_value}"
|
|
raise AssertionError(msg)
|
|
|
|
return BenchmarkTracker()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def npc_service_no_movement(game_engine) -> Generator[Any, None, None]:
|
|
"""Provide an NPC service without movement system for tests."""
|
|
from src.services.npc_service import NPCService
|
|
|
|
|
|
npc_service = game_engine.get_service('npc')
|
|
|
|
|
|
if hasattr(npc_service, 'stop_movement_system'):
|
|
npc_service.stop_movement_system()
|
|
|
|
|
|
original_start = npc_service.start_movement_system
|
|
npc_service.start_movement_system = lambda: None
|
|
|
|
yield npc_service
|
|
|
|
|
|
npc_service.start_movement_system = original_start
|
|
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Configure pytest with custom markers."""
|
|
config.addinivalue_line("markers", "unit: Unit tests")
|
|
config.addinivalue_line("markers", "integration: Integration tests")
|
|
config.addinivalue_line("markers", "e2e: End-to-end tests")
|
|
config.addinivalue_line("markers", "performance: Performance tests")
|
|
config.addinivalue_line("markers", "smoke: Smoke tests")
|
|
config.addinivalue_line("markers", "refactoring: Refactoring completion tests")
|
|
|