mud/tests/test_content_loader.py
Jared Miller 9c480f8d47
Remove duplicate mock_writer/mock_reader fixtures
Removed identical local copies from 45 test files. These fixtures
are already defined in conftest.py.
2026-02-16 15:29:21 -05:00

300 lines
8.2 KiB
Python

"""Tests for content loading from TOML files."""
import logging
import tempfile
from pathlib import Path
import pytest
from mudlib.content import load_command, load_commands
from mudlib.player import Player
@pytest.fixture
def player(mock_reader, mock_writer):
return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer)
def test_load_command_with_handler_reference():
"""Test loading a command with a Python handler reference."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "shout"
aliases = ["yell"]
help = "shout a message to nearby players"
mode = "normal"
handler = "mudlib.commands.look:cmd_look"
""")
f.flush()
tmp_path = Path(f.name)
try:
defn = load_command(tmp_path)
assert defn.name == "shout"
assert defn.aliases == ["yell"]
assert defn.help == "shout a message to nearby players"
assert defn.mode == "normal"
assert callable(defn.handler)
finally:
tmp_path.unlink()
def test_load_command_with_message():
"""Test loading a command that just sends a message."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "rules"
aliases = ["info"]
help = "display the server rules"
mode = "*"
message = \"\"\"
Welcome to the MUD!
1. Be respectful
2. No cheating
\"\"\"
""")
f.flush()
tmp_path = Path(f.name)
try:
defn = load_command(tmp_path)
assert defn.name == "rules"
assert defn.aliases == ["info"]
assert defn.help == "display the server rules"
assert defn.mode == "*"
assert callable(defn.handler)
finally:
tmp_path.unlink()
@pytest.mark.asyncio
async def test_message_command_sends_text(player, mock_writer):
"""Test that a message command actually sends the text to the player."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "motd"
message = "Welcome to the server!\\nEnjoy your stay."
""")
f.flush()
tmp_path = Path(f.name)
try:
defn = load_command(tmp_path)
await defn.handler(player, "")
assert mock_writer.write.called
written_text = mock_writer.write.call_args[0][0]
assert "Welcome to the server!" in written_text
assert "Enjoy your stay." in written_text
finally:
tmp_path.unlink()
def test_load_commands_from_directory():
"""Test loading multiple command definitions from a directory."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
# Create two command files
(tmp_path / "cmd1.toml").write_text("""
name = "cmd1"
message = "Command 1"
""")
(tmp_path / "cmd2.toml").write_text("""
name = "cmd2"
message = "Command 2"
""")
# Create a non-toml file that should be ignored
(tmp_path / "readme.txt").write_text("This is not a command")
commands = load_commands(tmp_path)
assert len(commands) == 2
names = {cmd.name for cmd in commands}
assert names == {"cmd1", "cmd2"}
def test_load_command_missing_name():
"""Test that missing name field raises an error."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
message = "Hello"
""")
f.flush()
tmp_path = Path(f.name)
try:
with pytest.raises(KeyError):
load_command(tmp_path)
finally:
tmp_path.unlink()
def test_load_command_missing_handler_and_message():
"""Test that a command without handler or message raises an error."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "broken"
""")
f.flush()
tmp_path = Path(f.name)
try:
match_msg = "must specify either 'handler' or 'message'"
with pytest.raises(ValueError, match=match_msg):
load_command(tmp_path)
finally:
tmp_path.unlink()
def test_load_command_bad_handler_reference():
"""Test that an invalid handler reference raises an error."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "broken"
handler = "nonexistent.module:function"
""")
f.flush()
tmp_path = Path(f.name)
try:
with pytest.raises((ImportError, ModuleNotFoundError)):
load_command(tmp_path)
finally:
tmp_path.unlink()
def test_load_command_defaults():
"""Test that optional fields have sensible defaults."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "simple"
message = "Hello"
""")
f.flush()
tmp_path = Path(f.name)
try:
defn = load_command(tmp_path)
assert defn.name == "simple"
assert defn.aliases == []
assert defn.help == ""
assert defn.mode == "normal"
finally:
tmp_path.unlink()
def test_load_command_both_handler_and_message():
"""Test that specifying both handler and message raises an error."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "broken"
handler = "mudlib.commands.look:cmd_look"
message = "Hello"
""")
f.flush()
tmp_path = Path(f.name)
try:
match_msg = "cannot specify both 'handler' and 'message'"
with pytest.raises(ValueError, match=match_msg):
load_command(tmp_path)
finally:
tmp_path.unlink()
def test_load_command_handler_wrong_type():
"""Test that non-string handler field raises TypeError."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "broken"
handler = 123
""")
f.flush()
tmp_path = Path(f.name)
try:
match_msg = "'handler' must be a string"
with pytest.raises(TypeError, match=match_msg):
load_command(tmp_path)
finally:
tmp_path.unlink()
def test_load_command_message_wrong_type():
"""Test that non-string message field raises TypeError."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "broken"
message = 456
""")
f.flush()
tmp_path = Path(f.name)
try:
match_msg = "'message' must be a string"
with pytest.raises(TypeError, match=match_msg):
load_command(tmp_path)
finally:
tmp_path.unlink()
@pytest.mark.asyncio
async def test_message_whitespace_handling(player, mock_writer):
"""Test message stripping removes outer whitespace, keeps internal formatting."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "formatted"
message = \"\"\"
First line
Indented line
Last line
\"\"\"
""")
f.flush()
tmp_path = Path(f.name)
try:
defn = load_command(tmp_path)
await defn.handler(player, "")
assert mock_writer.write.called
written_text = mock_writer.write.call_args[0][0]
# Outer whitespace should be stripped
assert not written_text.startswith("\n")
# But internal formatting should be preserved
assert "First line\n Indented line\nLast line" in written_text
finally:
tmp_path.unlink()
def test_load_commands_logs_broken_files(caplog):
"""Test that load_commands logs warnings for broken TOML files."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
# Create a valid command file
(tmp_path / "valid.toml").write_text("""
name = "valid"
message = "OK"
""")
# Create a broken command file (missing required field)
(tmp_path / "broken.toml").write_text("""
name = "broken"
""")
with caplog.at_level(logging.WARNING):
commands = load_commands(tmp_path)
# Should load the valid one only
assert len(commands) == 1
assert commands[0].name == "valid"
# Should log warning about the broken file
warnings = [r.message for r in caplog.records]
assert any("failed to load command from" in msg for msg in warnings)
assert any("broken.toml" in msg for msg in warnings)