"""debug_capture just needs to reliably write what it's given and never raise into caller code -- these are format/plumbing checks, not vision tests.""" import json import numpy as np import pytest from PIL import Image from fenigma import debug_capture @pytest.fixture(autouse=True) def _isolated_debug_dir(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) return tmp_path def _tiny_png_bytes() -> bytes: import io buf = io.BytesIO() Image.new("RGB", (4, 4), (10, 20, 30)).save(buf, format="PNG") return buf.getvalue() def test_save_map_read_failure_writes_png_and_reason(_isolated_debug_dir): path = debug_capture.save_map_read_failure(_tiny_png_bytes(), "too few grid line families") assert path is not None assert path.exists() meta = json.loads(path.with_suffix(".json").read_text()) assert meta["reason"] == "too few grid line families" def test_save_maybe_map_accepts_numpy_bgr_array(_isolated_debug_dir): bgr = np.zeros((4, 4, 3), dtype=np.uint8) bgr[..., 0] = 200 # blue channel, would come out red if BGR/RGB got swapped path = debug_capture.save_maybe_map(bgr) assert path is not None saved = Image.open(path) assert saved.getpixel((0, 0)) == (0, 0, 200) # still blue, not swapped to red def test_save_grid_correction_writes_both_solutions(_isolated_debug_dir): class FakeSolution: def __init__(self, du): self.H = np.eye(3) self.si, self.sj, self.du, self.dv = 1, 1, du, 0 path = debug_capture.save_grid_correction(_tiny_png_bytes(), FakeSolution(0.0), FakeSolution(0.3)) assert path is not None meta = json.loads(path.with_suffix(".json").read_text()) assert meta["original"]["du"] == 0.0 assert meta["corrected"]["du"] == 0.3 def test_unsupported_image_type_returns_none_without_raising(_isolated_debug_dir): assert debug_capture.save_maybe_map(object()) is None def _proposal(**overrides): from fenigma.map_import import Proposal defaults = dict(side="hostile", label="K8", sub_x=3, sub_y=4, unit="Armor_Tank.png", centre=(0, 0), box=(0, 0, 0, 0)) defaults.update(overrides) return Proposal(**defaults) def test_marker_ground_truth_records_verdict_per_proposal(_isolated_debug_dir): accepted = _proposal(accepted=True, confirmed_type="TANK") rejected = _proposal(label="K9", rejected=True) undecided = _proposal(label="L1") path = debug_capture.save_marker_ground_truth(_tiny_png_bytes(), [accepted, rejected, undecided]) assert path is not None meta = json.loads(path.with_suffix(".json").read_text()) by_label = {p["label"]: p for p in meta["proposals"]} assert by_label["K8"]["verdict"] == "accepted" assert by_label["K8"]["confirmed_type"] == "TANK" assert by_label["K9"]["verdict"] == "rejected" assert by_label["L1"]["verdict"] == "undecided" def test_marker_ground_truth_records_units_with_no_matching_proposal(_isolated_debug_dir): from fenigma.models import Board, Coord, TargetType board = Board() manual_target = board.add_target(TargetType.TANK, Coord("K", 8, 3, 4)) manual_ally = board.add_ally(TargetType.INFANTRY, Coord("K", 9, 0, 0)) path = debug_capture.save_marker_ground_truth( _tiny_png_bytes(), [], added_targets=[manual_target], added_allies=[manual_ally]) assert path is not None meta = json.loads(path.with_suffix(".json").read_text()) kinds = {(u["kind"], u["type"], u["coord"]) for u in meta["added_units"]} assert ("target", "TANK", "K8 3:4") in kinds assert ("ally", "INFANTRY", "K9 0:0") in kinds def test_marker_ground_truth_skips_when_nothing_to_say(_isolated_debug_dir): assert debug_capture.save_marker_ground_truth(_tiny_png_bytes(), []) is None