testspace / tests /test_verification.py
Mayug Maniparambil
Isolate flow_free verification in a timeout-bounded subprocess
30865cf
Raw
History Blame Contribute Delete
7.19 kB
from __future__ import annotations
import subprocess
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
from space_app.verification import VerificationService
RLP_PATH = Path(__file__).resolve().parents[1] / "submodules" / "rlp"
if str(RLP_PATH) not in sys.path:
sys.path.insert(0, str(RLP_PATH))
from rlp.ascii_parser import parse_ascii_pattern # type: ignore
class VerificationTests(unittest.TestCase):
def test_flow_free_example_if_solver_available(self) -> None:
verifier = VerificationService()
problem = "BA...A\n..E...\n..D.F.\n..F..D\n..C.CE\nB....."
solution = "BAAAAA\nBEEDDD\nBEDDFD\nBEFFFD\nBECCCE\nBEEEEE"
try:
result = verifier.verify(
puzzle_type="flow_free",
problem_ascii=problem,
board_ascii=solution,
args="6x6",
)
except BaseException as caught: # pragma: no cover - best effort native test
self.skipTest(f"Native verifier unavailable locally: {caught}")
return
self.assertTrue(result["correct"])
def test_undead_verification_timeout_returns_clean_error(self) -> None:
verifier = VerificationService()
board = "\n".join(
[
"G: 1 V: 1 Z: 1",
"",
" 1 1 1 1 ",
" 1 . . . . 1",
" 1 . . . . 1",
" 1 . . . . 1",
" 1 . . . . 1",
" 1 1 1 1 ",
]
)
with patch("space_app.verification.subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 15)):
result = verifier.verify(
puzzle_type="undead",
problem_ascii=board,
board_ascii=board,
args="4x4de",
)
self.assertFalse(result["correct"])
self.assertEqual(result.get("error"), "Undead verification timed out.")
def test_pattern_parser_keeps_single_digit_top_clue_rows(self) -> None:
board = "\n".join(
[
" 1 ",
" 1 2 3 3 3 ",
" +--+--+--+--+--+",
"2 1|##|##|..|##|..|",
" +--+--+--+--+--+",
" 1|..|##|..|..|..|",
" +--+--+--+--+--+",
" 3|..|..|##|##|##|",
" +--+--+--+--+--+",
" 3|..|..|##|##|##|",
" +--+--+--+--+--+",
" 3|..|..|##|##|##|",
" +--+--+--+--+--+",
]
)
state = parse_ascii_pattern(board)
self.assertEqual(state["common"]["rowlen"][:5], [1, 1, 1, 2, 1])
self.assertEqual(state["common"]["rowdata"][:6], [1, 2, 3, 1, 3, 3])
def test_loopy_runs_in_isolated_subprocess(self) -> None:
# Loopy verification must go through the subprocess path so a native
# crash/hang in the RLP C library cannot take down the web worker.
verifier = VerificationService()
problem = "\n".join(
[
"+++++++++++++",
"+ +",
"+ 3 3 +",
"+ +",
"+ 2 1 0 1 2 +",
"+ +",
"+ 2 1 0 +",
"+ +",
"+ 3 +",
"+ +",
"+ 2 2 +",
"+ +",
"+++++++++++++",
]
)
solution = "\n".join(
[
" - x - x x ",
"|3| |3| x x",
" x - x - - ",
"|2x1x0x1x2|",
" - x x x x ",
"x2| x1x0x |",
" x x - x - ",
"x | | | |3x",
" - x x x - ",
"| x2| |2x |",
" - - x - -",
]
)
with patch(
"space_app.verification.subprocess.run", wraps=subprocess.run
) as run_spy:
try:
result = verifier.verify(
puzzle_type="loopy",
problem_ascii=problem,
board_ascii=solution,
args="5x5de",
)
except BaseException as caught: # pragma: no cover - native best effort
self.skipTest(f"Native verifier unavailable locally: {caught}")
return
run_spy.assert_called_once()
self.assertTrue(result["correct"])
def test_loopy_verification_timeout_returns_clean_error(self) -> None:
verifier = VerificationService()
solution = " - x - x x \n|3| |3| x x"
with patch(
"space_app.verification.subprocess.run",
side_effect=subprocess.TimeoutExpired("cmd", 20),
):
result = verifier.verify(
puzzle_type="loopy",
problem_ascii=solution,
board_ascii=solution,
args="5x5de",
)
self.assertFalse(result["correct"])
self.assertEqual(result.get("error"), "Loopy verification timed out.")
def test_flow_free_runs_in_isolated_subprocess(self) -> None:
verifier = VerificationService()
problem = "BA...A\n..E...\n..D.F.\n..F..D\n..C.CE\nB....."
solution = "BAAAAA\nBEEDDD\nBEDDFD\nBEFFFD\nBECCCE\nBEEEEE"
with patch(
"space_app.verification.subprocess.run", wraps=subprocess.run
) as run_spy:
try:
result = verifier.verify(
puzzle_type="flow_free",
problem_ascii=problem,
board_ascii=solution,
args="6x6",
)
except BaseException as caught: # pragma: no cover - native best effort
self.skipTest(f"FlowFree solver unavailable locally: {caught}")
return
run_spy.assert_called_once()
self.assertTrue(result["correct"])
def test_flow_free_verification_timeout_returns_clean_error(self) -> None:
verifier = VerificationService()
with patch(
"space_app.verification.subprocess.run",
side_effect=subprocess.TimeoutExpired("cmd", 30),
):
result = verifier.verify(
puzzle_type="flow_free",
problem_ascii="A..A",
board_ascii="AAAA",
args="2x2",
)
self.assertFalse(result["correct"])
self.assertEqual(result.get("error"), "Flow_free verification timed out.")
def test_all_native_rlp_families_are_subprocess_isolated(self) -> None:
# Every puzzle family verified via the in-process RLP C library must be
# routed through the crash-isolating subprocess path. flow_free is
# excluded (it shells out to its own compiled solver binary).
from space_app.verification import SUBPROCESS_RLP_PUZZLES
for family in ("loopy", "bridges", "galaxies", "pattern", "undead"):
self.assertIn(family, SUBPROCESS_RLP_PUZZLES)