mud/tests/test_server.py

189 lines
5.5 KiB
Python

"""Tests for the server module."""
import asyncio
import contextlib
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mudlib import server
from mudlib.store import init_db
from mudlib.world.terrain import World
from mudlib.zone import Zone
@pytest.fixture
def temp_db():
"""Create a temporary database for testing."""
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f:
db_path = f.name
init_db(db_path)
yield db_path
# Cleanup
os.unlink(db_path)
def test_port_constant():
assert server.PORT == 6789
assert isinstance(server.PORT, int)
def test_shell_exists():
assert callable(server.shell)
assert asyncio.iscoroutinefunction(server.shell)
def test_run_server_exists():
assert callable(server.run_server)
assert asyncio.iscoroutinefunction(server.run_server)
def test_find_passable_start():
world = World(seed=42, width=100, height=100)
zone = Zone(
name="test", width=100, height=100, terrain=world.terrain, toroidal=True
)
x, y = server.find_passable_start(zone, 50, 50)
assert isinstance(x, int)
assert isinstance(y, int)
assert zone.is_passable(x, y)
@pytest.mark.asyncio
async def test_shell_greets_and_accepts_commands(temp_db):
world = World(seed=42, width=100, height=100)
zone = Zone(
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
)
server._overworld = zone
reader = AsyncMock()
writer = MagicMock()
writer.is_closing.side_effect = [False, False, False, True]
writer.drain = AsyncMock()
writer.close = MagicMock()
readline = "mudlib.server.readline2"
with patch(readline, new_callable=AsyncMock) as mock_readline:
# Simulate: name, create (y), pass, confirm pass, description, look, quit
mock_readline.side_effect = [
"TestPlayer",
"y",
"password",
"password",
"A test character",
"look",
"quit",
]
await server.shell(reader, writer)
calls = [str(call) for call in writer.write.call_args_list]
assert any("Welcome" in call for call in calls)
assert any("TestPlayer" in call for call in calls)
writer.close.assert_called()
@pytest.mark.asyncio
async def test_shell_handles_eof():
world = World(seed=42, width=100, height=100)
zone = Zone(
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
)
server._overworld = zone
reader = AsyncMock()
writer = MagicMock()
writer.is_closing.return_value = False
writer.drain = AsyncMock()
writer.close = MagicMock()
readline = "mudlib.server.readline2"
with patch(readline, new_callable=AsyncMock) as mock_readline:
mock_readline.return_value = None
await server.shell(reader, writer)
writer.close.assert_called_once()
@pytest.mark.asyncio
async def test_shell_handles_quit(temp_db):
world = World(seed=42, width=100, height=100)
zone = Zone(
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
)
server._overworld = zone
reader = AsyncMock()
writer = MagicMock()
writer.is_closing.side_effect = [False, False, True]
writer.drain = AsyncMock()
writer.close = MagicMock()
readline = "mudlib.server.readline2"
with patch(readline, new_callable=AsyncMock) as mock_readline:
# Simulate: name, create (y), pass, confirm pass, description, quit
mock_readline.side_effect = [
"TestPlayer",
"y",
"password",
"password",
"A test character",
"quit",
]
await server.shell(reader, writer)
calls = [str(call) for call in writer.write.call_args_list]
assert any("Goodbye" in call for call in calls)
writer.close.assert_called()
def test_load_world_config():
"""Config loader returns expected values from worlds/earth/config.toml."""
config = server.load_world_config()
assert config["world"]["seed"] == 42
assert config["world"]["width"] == 1000
assert config["world"]["height"] == 1000
def test_load_world_config_missing():
"""Config loader raises FileNotFoundError for nonexistent world."""
with pytest.raises(FileNotFoundError):
server.load_world_config("nonexistent")
def test_tick_constants():
"""Tick rate and interval are configured correctly."""
assert server.TICK_RATE == 10
assert pytest.approx(0.1) == server.TICK_INTERVAL
def test_game_loop_exists():
"""Game loop is an async callable."""
assert callable(server.game_loop)
assert asyncio.iscoroutinefunction(server.game_loop)
@pytest.mark.asyncio
async def test_game_loop_calls_clear_expired():
"""Game loop calls clear_expired each tick."""
with (
patch("mudlib.server.clear_expired") as mock_clear,
patch("mudlib.server.process_combat", new_callable=AsyncMock),
patch("mudlib.server.process_mobs", new_callable=AsyncMock),
patch("mudlib.server.process_resting", new_callable=AsyncMock),
patch("mudlib.server.process_unconscious", new_callable=AsyncMock),
patch("mudlib.server.get_game_hour", return_value=12),
patch("mudlib.server.process_schedules"),
):
task = asyncio.create_task(server.game_loop())
await asyncio.sleep(0.25)
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
assert mock_clear.call_count >= 1