Spaces:
Running
Running
| 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) | |