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,27 +111,30 @@ class IFSession:
|
|||
if not self.process or not self.process.stdin:
|
||||
return "error: game not running"
|
||||
|
||||
# Ensure save directory exists
|
||||
self._ensure_save_dir()
|
||||
try:
|
||||
# Ensure save directory exists
|
||||
self._ensure_save_dir()
|
||||
|
||||
# Send "save" command to dfrotz
|
||||
self.process.stdin.write(b"save\n")
|
||||
await self.process.stdin.drain()
|
||||
# Send "save" command to dfrotz
|
||||
self.process.stdin.write(b"save\n")
|
||||
await self.process.stdin.drain()
|
||||
|
||||
# Read filename prompt
|
||||
await self._read_response()
|
||||
# Read filename prompt
|
||||
await self._read_response()
|
||||
|
||||
# Send save file path
|
||||
save_path_str = str(self.save_path)
|
||||
self.process.stdin.write(f"{save_path_str}\n".encode())
|
||||
await self.process.stdin.drain()
|
||||
# Send save file path
|
||||
save_path_str = str(self.save_path)
|
||||
self.process.stdin.write(f"{save_path_str}\n".encode())
|
||||
await self.process.stdin.drain()
|
||||
|
||||
# Read confirmation
|
||||
confirmation = await self._read_response()
|
||||
# Read confirmation
|
||||
confirmation = await self._read_response()
|
||||
|
||||
# Mark as saved
|
||||
self._saved = True
|
||||
return confirmation
|
||||
# Mark as saved
|
||||
self._saved = True
|
||||
return confirmation
|
||||
except Exception as e:
|
||||
return f"error: save failed ({e})"
|
||||
|
||||
async def _do_restore(self) -> str:
|
||||
"""Restore game state from disk. Returns game text or empty string."""
|
||||
|
|
@ -142,21 +145,25 @@ class IFSession:
|
|||
if not self.process or not self.process.stdin:
|
||||
return ""
|
||||
|
||||
# Send "restore" command to dfrotz
|
||||
self.process.stdin.write(b"restore\n")
|
||||
await self.process.stdin.drain()
|
||||
try:
|
||||
# Send "restore" command to dfrotz
|
||||
self.process.stdin.write(b"restore\n")
|
||||
await self.process.stdin.drain()
|
||||
|
||||
# Read filename prompt
|
||||
await self._read_response()
|
||||
# Read filename prompt
|
||||
await self._read_response()
|
||||
|
||||
# Send save file path
|
||||
save_path_str = str(self.save_path)
|
||||
self.process.stdin.write(f"{save_path_str}\n".encode())
|
||||
await self.process.stdin.drain()
|
||||
# Send save file path
|
||||
save_path_str = str(self.save_path)
|
||||
self.process.stdin.write(f"{save_path_str}\n".encode())
|
||||
await self.process.stdin.drain()
|
||||
|
||||
# Read game text response
|
||||
game_text = await self._read_response()
|
||||
return game_text
|
||||
# Read game text response
|
||||
game_text = await self._read_response()
|
||||
return game_text
|
||||
except Exception:
|
||||
# If restore fails, return empty string (player starts fresh)
|
||||
return ""
|
||||
|
||||
async def _read_response(self) -> str:
|
||||
"""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"
|
||||
]
|
||||
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