189 lines
5.5 KiB
Python
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
|