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)