Add error handling for process communication failures in IF save/restore
This commit is contained in:
parent
bb1f9bbbd8
commit
76488139c8
2 changed files with 128 additions and 28 deletions
|
|
@ -111,6 +111,7 @@ class IFSession:
|
||||||
if not self.process or not self.process.stdin:
|
if not self.process or not self.process.stdin:
|
||||||
return "error: game not running"
|
return "error: game not running"
|
||||||
|
|
||||||
|
try:
|
||||||
# Ensure save directory exists
|
# Ensure save directory exists
|
||||||
self._ensure_save_dir()
|
self._ensure_save_dir()
|
||||||
|
|
||||||
|
|
@ -132,6 +133,8 @@ class IFSession:
|
||||||
# Mark as saved
|
# Mark as saved
|
||||||
self._saved = True
|
self._saved = True
|
||||||
return confirmation
|
return confirmation
|
||||||
|
except Exception as e:
|
||||||
|
return f"error: save failed ({e})"
|
||||||
|
|
||||||
async def _do_restore(self) -> str:
|
async def _do_restore(self) -> str:
|
||||||
"""Restore game state from disk. Returns game text or empty string."""
|
"""Restore game state from disk. Returns game text or empty string."""
|
||||||
|
|
@ -142,6 +145,7 @@ class IFSession:
|
||||||
if not self.process or not self.process.stdin:
|
if not self.process or not self.process.stdin:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
# Send "restore" command to dfrotz
|
# Send "restore" command to dfrotz
|
||||||
self.process.stdin.write(b"restore\n")
|
self.process.stdin.write(b"restore\n")
|
||||||
await self.process.stdin.drain()
|
await self.process.stdin.drain()
|
||||||
|
|
@ -157,6 +161,9 @@ class IFSession:
|
||||||
# Read game text response
|
# Read game text response
|
||||||
game_text = await self._read_response()
|
game_text = await self._read_response()
|
||||||
return game_text
|
return game_text
|
||||||
|
except Exception:
|
||||||
|
# If restore fails, return empty string (player starts fresh)
|
||||||
|
return ""
|
||||||
|
|
||||||
async def _read_response(self) -> str:
|
async def _read_response(self) -> str:
|
||||||
"""Read dfrotz output until the '>' prompt appears."""
|
"""Read dfrotz output until the '>' prompt appears."""
|
||||||
|
|
|
||||||
|
|
@ -683,3 +683,96 @@ async def test_regular_input_resets_save_flag(tmp_path):
|
||||||
call for call in mock_process.stdin.write.call_args_list if call[0][0] == b"save\n"
|
call for call in mock_process.stdin.write.call_args_list if call[0][0] == b"save\n"
|
||||||
]
|
]
|
||||||
assert len(save_calls) == 2, "should be able to save again after regular input"
|
assert len(save_calls) == 2, "should be able to save again after regular input"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_save_handles_process_communication_failure(tmp_path):
|
||||||
|
"""_do_save() returns error message when process communication fails."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "tester"
|
||||||
|
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Mock process that raises an exception
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock(side_effect=BrokenPipeError("process died"))
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
result = await session._do_save()
|
||||||
|
|
||||||
|
assert "error" in result.lower()
|
||||||
|
assert "save" in result.lower() or "failed" in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_save_handles_stdout_read_failure(tmp_path):
|
||||||
|
"""_do_save() returns error message when reading response fails."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "tester"
|
||||||
|
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Mock process where writing succeeds but reading fails
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock()
|
||||||
|
mock_process.stdin.drain = AsyncMock()
|
||||||
|
mock_process.stdout = AsyncMock()
|
||||||
|
mock_process.stdout.read = AsyncMock(side_effect=OSError("read failed"))
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
result = await session._do_save()
|
||||||
|
|
||||||
|
assert "error" in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_restore_handles_process_communication_failure(tmp_path):
|
||||||
|
"""_do_restore() returns empty string when process communication fails."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "tester"
|
||||||
|
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Create a save file so we get past the existence check
|
||||||
|
session._ensure_save_dir()
|
||||||
|
session.save_path.write_text("fake save data")
|
||||||
|
|
||||||
|
# Mock process that raises an exception
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock(side_effect=BrokenPipeError("process died"))
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
result = await session._do_restore()
|
||||||
|
|
||||||
|
# Should return empty string on failure (player starts fresh)
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_restore_handles_stdout_read_failure(tmp_path):
|
||||||
|
"""_do_restore() returns empty string when reading response fails."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "tester"
|
||||||
|
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Create a save file
|
||||||
|
session._ensure_save_dir()
|
||||||
|
session.save_path.write_text("fake save data")
|
||||||
|
|
||||||
|
# Mock process where writing succeeds but reading fails
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock()
|
||||||
|
mock_process.stdin.drain = AsyncMock()
|
||||||
|
mock_process.stdout = AsyncMock()
|
||||||
|
mock_process.stdout.read = AsyncMock(side_effect=OSError("read failed"))
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
result = await session._do_restore()
|
||||||
|
|
||||||
|
# Should return empty string on failure (player starts fresh)
|
||||||
|
assert result == ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue