Skip to main content
Glama
test_emulator.py•11.5 kB
""" Integration tests for PyBoy emulator wrapper. Tests the PyBoyEmulator class with real PyBoy instances using synthetic ROMs. Focuses on testing our wrapper's logic, error handling, and LLM-friendly interface. """ from pathlib import Path import pytest from mcp_server.emulator import ( EmulatorStateError, PyBoyEmulator, ROMError, ) from tests.mcp_server.fixtures.rom_generation import create_test_rom_data @pytest.fixture def synthetic_rom(tmp_path: Path) -> Path: """Create a minimal valid GB ROM for testing.""" rom_file = tmp_path / "test.gb" rom_data = create_test_rom_data(size_kb=32) rom_file.write_bytes(rom_data) return rom_file @pytest.fixture def invalid_rom(tmp_path: Path) -> Path: """Create an invalid ROM file for error testing.""" rom_file = tmp_path / "invalid.gb" rom_file.write_bytes(b"INVALID ROM DATA") return rom_file @pytest.fixture def non_gb_file(tmp_path: Path) -> Path: """Create a non-GB file for extension testing.""" file_path = tmp_path / "not_a_rom.txt" file_path.write_text("This is not a ROM file") return file_path @pytest.mark.integration class TestPyBoyEmulatorInitialization: """Test emulator initialization and configuration.""" def test_init_headless_by_default(self): """Test that emulator initializes in headless mode by default.""" emulator = PyBoyEmulator() assert emulator.headless is True assert emulator.pyboy is None assert emulator.current_rom_path is None assert emulator.is_running is False def test_init_with_gui_mode(self): """Test emulator initialization with GUI mode.""" emulator = PyBoyEmulator(headless=False) assert emulator.headless is False assert not emulator.is_ready() @pytest.mark.integration class TestPyBoyEmulatorLifecycle: """Test emulator start/stop lifecycle.""" def test_start_when_not_running(self): """Test starting emulator when not running.""" emulator = PyBoyEmulator() emulator.start() assert emulator.is_running is True def test_start_when_already_running(self): """Test error when starting already running emulator.""" emulator = PyBoyEmulator() emulator.start() with pytest.raises(EmulatorStateError) as exc_info: emulator.start() assert "already running" in str(exc_info.value) assert "stop() first" in str(exc_info.value) def test_stop_when_running(self, synthetic_rom): """Test stopping running emulator.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) assert emulator.is_running is True assert emulator.pyboy is not None emulator.stop() assert emulator.is_running is False assert emulator.pyboy is None assert emulator.current_rom_path is None def test_stop_when_not_running(self): """Test that stopping non-running emulator is safe.""" emulator = PyBoyEmulator() # Should not raise any errors emulator.stop() assert emulator.is_running is False def test_multiple_stops_are_safe(self, synthetic_rom): """Test that calling stop multiple times is safe.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) # Stop multiple times emulator.stop() emulator.stop() emulator.stop() assert emulator.is_running is False @pytest.mark.integration @pytest.mark.emulator class TestPyBoyEmulatorROMLoading: """Test ROM loading functionality.""" def test_load_rom_success(self, synthetic_rom): """Test successful ROM loading with real PyBoy.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) assert emulator.is_running is True assert emulator.current_rom_path == synthetic_rom assert emulator.pyboy is not None assert emulator.is_ready() is True # Verify real PyBoy instance was created assert hasattr(emulator.pyboy, "tick") assert hasattr(emulator.pyboy, "screen") # Cleanup emulator.stop() def test_load_rom_with_gui_mode(self, synthetic_rom): """Test ROM loading in GUI mode (headless=False).""" emulator = PyBoyEmulator(headless=False) emulator.load_rom(synthetic_rom) assert emulator.is_running is True assert emulator.pyboy is not None # Cleanup emulator.stop() def test_load_rom_file_not_found(self): """Test error when ROM file doesn't exist.""" emulator = PyBoyEmulator() with pytest.raises(ROMError) as exc_info: emulator.load_rom("nonexistent.gb") error_msg = str(exc_info.value) assert "not found" in error_msg assert "Check the file path" in error_msg def test_load_rom_invalid_extension(self, non_gb_file): """Test error when file has invalid extension.""" emulator = PyBoyEmulator() with pytest.raises(ROMError) as exc_info: emulator.load_rom(non_gb_file) error_msg = str(exc_info.value) assert "Invalid ROM file extension" in error_msg assert ".gb and .gbc files are supported" in error_msg def test_load_rom_replaces_existing(self, synthetic_rom, tmp_path): """Test that loading a new ROM stops the previous one.""" emulator = PyBoyEmulator() # Load first ROM emulator.load_rom(synthetic_rom) first_pyboy = emulator.pyboy # Create second ROM second_rom = tmp_path / "second.gb" second_rom.write_bytes(create_test_rom_data()) # Load second ROM emulator.load_rom(second_rom) assert emulator.current_rom_path == second_rom assert emulator.pyboy != first_pyboy # Cleanup emulator.stop() def test_load_rom_with_invalid_data(self, invalid_rom): """Test handling of ROMs with invalid data.""" emulator = PyBoyEmulator() # PyBoy should handle invalid ROM data gracefully # Our wrapper should catch and convert to LLM-friendly error with pytest.raises((ROMError, EmulatorStateError)): emulator.load_rom(invalid_rom) @pytest.mark.integration class TestPyBoyEmulatorInfo: """Test ROM info retrieval.""" def test_get_rom_info_success(self, synthetic_rom): """Test getting ROM info when loaded.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) info = emulator.get_rom_info() assert info["name"] == "test.gb" assert info["path"] == str(synthetic_rom) assert info["size"] == 32 * 1024 # 32KB assert info["extension"] == ".gb" assert info["is_running"] is True # Cleanup emulator.stop() def test_get_rom_info_no_rom_loaded(self): """Test error when getting info with no ROM.""" emulator = PyBoyEmulator() with pytest.raises(EmulatorStateError) as exc_info: emulator.get_rom_info() error_msg = str(exc_info.value) assert "No ROM is currently loaded" in error_msg assert "load_rom()" in error_msg @pytest.mark.integration @pytest.mark.emulator class TestPyBoyEmulatorAccess: """Test PyBoy instance access.""" def test_get_pyboy_instance_success(self, synthetic_rom): """Test getting PyBoy instance when running.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) pyboy = emulator.get_pyboy_instance() assert pyboy is not None # Verify it's a real PyBoy instance assert hasattr(pyboy, "tick") assert hasattr(pyboy, "screen") assert hasattr(pyboy, "button") # Cleanup emulator.stop() def test_get_pyboy_instance_not_running(self): """Test error when getting instance with no emulator.""" emulator = PyBoyEmulator() with pytest.raises(EmulatorStateError) as exc_info: emulator.get_pyboy_instance() error_msg = str(exc_info.value) assert "Emulator is not running" in error_msg assert "load_rom()" in error_msg @pytest.mark.integration class TestPyBoyEmulatorContextManager: """Test context manager functionality.""" def test_context_manager_cleanup(self, synthetic_rom): """Test that context manager cleans up resources.""" with PyBoyEmulator() as emulator: emulator.load_rom(synthetic_rom) assert emulator.is_running is True # After exiting context, emulator should be stopped assert emulator.is_running is False assert emulator.pyboy is None def test_context_manager_with_exception(self, synthetic_rom): """Test cleanup even when exception occurs.""" emulator = PyBoyEmulator() with pytest.raises(ValueError): with emulator: emulator.load_rom(synthetic_rom) raise ValueError("Test exception") # Cleanup should still happen assert emulator.is_running is False @pytest.mark.unit class TestLLMFriendlyErrors: """Test that errors are LLM-friendly with actionable suggestions.""" def test_rom_not_found_suggestion(self): """Test helpful suggestion for missing ROM.""" emulator = PyBoyEmulator() with pytest.raises(ROMError) as exc_info: emulator.load_rom("/fake/path/game.gb") error_msg = str(exc_info.value) # Should contain actionable suggestion assert "Check the file path" in error_msg assert "ensure the ROM file exists" in error_msg def test_invalid_extension_suggestion(self, non_gb_file): """Test helpful suggestion for invalid file types.""" emulator = PyBoyEmulator() with pytest.raises(ROMError) as exc_info: emulator.load_rom(non_gb_file) error_msg = str(exc_info.value) assert "Invalid ROM file extension" in error_msg assert ".gb and .gbc files are supported" in error_msg @pytest.mark.integration @pytest.mark.emulator @pytest.mark.slow class TestPyBoyIntegration: """Test integration with real PyBoy functionality.""" def test_pyboy_basic_operations(self, synthetic_rom): """Test basic PyBoy operations work through our wrapper.""" emulator = PyBoyEmulator() emulator.load_rom(synthetic_rom) pyboy = emulator.get_pyboy_instance() # Test basic PyBoy operations pyboy.tick() # Should not raise # Test screen access assert hasattr(pyboy.screen, "ndarray") screen_data = pyboy.screen.ndarray assert screen_data.shape == (144, 160, 4) # Game Boy screen dimensions (RGBA) # Test button input pyboy.button("a") # Should not raise # Cleanup emulator.stop() def test_headless_mode_works(self, synthetic_rom): """Test that headless mode works correctly.""" emulator = PyBoyEmulator(headless=True) emulator.load_rom(synthetic_rom) # Should create PyBoy instance successfully assert emulator.pyboy is not None assert emulator.is_ready() is True # Basic operations should work emulator.pyboy.tick() screen = emulator.pyboy.screen.ndarray assert screen is not None # Cleanup emulator.stop()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ssimonitch/mcp-pyboy'

If you have feedback or need assistance with the MCP directory API, please join our Discord server