testsAndMisc-archive/python_pkg/puzzle_solver/tests/test_main.py
Krzysztof kuhy Rudnicki 996617d4a0 test: achieve 100% branch coverage across all python_pkg packages
- Add comprehensive tests for all packages (3572 tests, 100% branch coverage)
- Split oversized test files to stay under 500-line limit
- Add per-file ruff ignores for test-appropriate suppressions
- Fix _cache_decks.py to properly convert JSON lists to tuples
- Add session-scoped conftest fixture for logging handler cleanup (Python 3.14)
- Update ruff pre-commit hook to v0.15.2
- Add codespell ignore words for test data
- Add generated output files to .gitignore
2026-03-21 17:51:36 +01:00

353 lines
12 KiB
Python

"""Tests for python_pkg.puzzle_solver.main and __main__ modules."""
from __future__ import annotations
import json
import sys
from typing import Any
from unittest.mock import MagicMock, mock_open, patch
import pytest
# Ensure cv2 and numpy are available as mocks before importing main
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())
from python_pkg.puzzle_solver.main import (
cmd_debug,
cmd_parse,
cmd_run,
cmd_solve,
main,
)
def _minimal_puzzle_data() -> dict[str, Any]:
return {
"squares": [
{"pos": [0, 0], "type": "player"},
{"pos": [0, 1], "type": "normal"},
{"pos": [0, 2], "type": "goal"},
],
}
# ── cmd_parse ────────────────────────────────────────────────────────
class TestCmdParse:
@patch("python_pkg.puzzle_solver.main.save_puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_with_output(self, mock_parse: MagicMock, mock_save: MagicMock) -> None:
mock_parse.return_value = {"squares": [], "notes": []}
args = MagicMock()
args.image = "test.png"
args.output = "out.json"
args.threshold = 55
cmd_parse(args)
mock_save.assert_called_once_with({"squares": [], "notes": []}, "out.json")
@patch("python_pkg.puzzle_solver.main.save_puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_default_output(self, mock_parse: MagicMock, mock_save: MagicMock) -> None:
mock_parse.return_value = {"squares": [], "notes": []}
args = MagicMock()
args.image = "screenshot.png"
args.output = None
args.threshold = 55
cmd_parse(args)
mock_save.assert_called_once_with(
{"squares": [], "notes": []}, "screenshot_puzzle.json"
)
@patch("python_pkg.puzzle_solver.main.save_puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_with_notes(self, mock_parse: MagicMock, mock_save: MagicMock) -> None:
mock_parse.return_value = {
"squares": [],
"notes": ["note1", "note2"],
}
args = MagicMock()
args.image = "test.png"
args.output = "out.json"
args.threshold = 55
cmd_parse(args)
@patch("python_pkg.puzzle_solver.main.save_puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_no_notes(self, mock_parse: MagicMock, mock_save: MagicMock) -> None:
mock_parse.return_value = {"squares": []}
args = MagicMock()
args.image = "test.png"
args.output = "out.json"
args.threshold = 55
cmd_parse(args)
# ── cmd_solve ────────────────────────────────────────────────────────
class TestCmdSolve:
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
def test_solvable(
self,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
data = _minimal_puzzle_data()
m = mock_open(read_data=json.dumps(data))
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
with patch("pathlib.Path.open", m):
args = MagicMock()
args.puzzle = "test.json"
cmd_solve(args)
mock_print_sol.assert_called_once()
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
def test_unsolvable(
self,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
) -> None:
data = _minimal_puzzle_data()
m = mock_open(read_data=json.dumps(data))
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = None
args = MagicMock()
args.puzzle = "test.json"
with patch("pathlib.Path.open", m), pytest.raises(SystemExit):
cmd_solve(args)
# ── cmd_run ──────────────────────────────────────────────────────────
class TestCmdRun:
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_solvable(
self,
mock_parse: MagicMock,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
mock_parse.return_value = _minimal_puzzle_data()
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
args = MagicMock()
args.image = "test.png"
args.threshold = 55
cmd_run(args)
mock_print_sol.assert_called_once()
@patch("python_pkg.puzzle_solver.main.save_puzzle")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_unsolvable(
self,
mock_parse: MagicMock,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_save: MagicMock,
) -> None:
mock_parse.return_value = _minimal_puzzle_data()
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = None
args = MagicMock()
args.image = "test.png"
args.threshold = 55
with pytest.raises(SystemExit):
cmd_run(args)
mock_save.assert_called_once()
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_with_notes(
self,
mock_parse: MagicMock,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
data = _minimal_puzzle_data()
data["notes"] = ["note1"]
mock_parse.return_value = data
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
args = MagicMock()
args.image = "test.png"
args.threshold = 55
cmd_run(args)
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_no_notes_key(
self,
mock_parse: MagicMock,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
data = _minimal_puzzle_data()
# no "notes" key at all
mock_parse.return_value = data
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
args = MagicMock()
args.image = "test.png"
args.threshold = 55
cmd_run(args)
# ── cmd_debug ────────────────────────────────────────────────────────
class TestCmdDebug:
@patch("python_pkg.puzzle_solver.main.draw_debug")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_with_output(self, mock_parse: MagicMock, mock_draw: MagicMock) -> None:
mock_parse.return_value = {
"squares": [
{"type": "normal"},
{"type": "normal"},
{"type": "goal"},
],
}
args = MagicMock()
args.image = "test.png"
args.output = "debug.png"
args.threshold = 55
cmd_debug(args)
mock_draw.assert_called_once_with(
"test.png", mock_parse.return_value, "debug.png"
)
@patch("python_pkg.puzzle_solver.main.draw_debug")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_default_output(self, mock_parse: MagicMock, mock_draw: MagicMock) -> None:
mock_parse.return_value = {
"squares": [{"type": "normal"}],
}
args = MagicMock()
args.image = "screenshot.png"
args.output = None
args.threshold = 55
cmd_debug(args)
mock_draw.assert_called_once_with(
"screenshot.png", mock_parse.return_value, "screenshot_debug.png"
)
# ── main ─────────────────────────────────────────────────────────────
class TestMain:
@patch("python_pkg.puzzle_solver.main.parse_image")
@patch("python_pkg.puzzle_solver.main.save_puzzle")
def test_parse_command(self, mock_save: MagicMock, mock_parse: MagicMock) -> None:
mock_parse.return_value = {"squares": [], "notes": []}
with patch("sys.argv", ["prog", "parse", "img.png", "-o", "out.json"]):
main()
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
def test_solve_command(
self,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
data = _minimal_puzzle_data()
m = mock_open(read_data=json.dumps(data))
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
with (
patch("pathlib.Path.open", m),
patch("sys.argv", ["prog", "solve", "puzzle.json"]),
):
main()
@patch("python_pkg.puzzle_solver.main.print_solution")
@patch("python_pkg.puzzle_solver.main.solve")
@patch("python_pkg.puzzle_solver.main.print_puzzle")
@patch("python_pkg.puzzle_solver.main.Puzzle")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_run_command(
self,
mock_parse: MagicMock,
mock_puzzle_cls: MagicMock,
mock_print: MagicMock,
mock_solve: MagicMock,
mock_print_sol: MagicMock,
) -> None:
mock_parse.return_value = _minimal_puzzle_data()
mock_puzzle = MagicMock()
mock_puzzle_cls.from_json.return_value = mock_puzzle
mock_solve.return_value = ["right"]
with patch("sys.argv", ["prog", "run", "img.png"]):
main()
@patch("python_pkg.puzzle_solver.main.draw_debug")
@patch("python_pkg.puzzle_solver.main.parse_image")
def test_debug_command(self, mock_parse: MagicMock, mock_draw: MagicMock) -> None:
mock_parse.return_value = {"squares": [{"type": "normal"}]}
with patch("sys.argv", ["prog", "debug", "img.png", "-o", "d.png"]):
main()
# ── __main__.py ──────────────────────────────────────────────────────
class TestDunderMain:
@patch("python_pkg.puzzle_solver.main.main")
def test_main_called(self, mock_main: MagicMock) -> None:
import importlib
import python_pkg.puzzle_solver.__main__ as mod
importlib.reload(mod)
mock_main.assert_called()