mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 22:23:02 +02:00
Fix ruff violations in ~15 source files and ~60+ test files to minimize per-file-ignores in pyproject.toml. Remaining ignores are justified with comments explaining why each suppression is necessary. Source fixes: FBT003 (keyword args), S310 (URL validation), SLF001 (private access), T201 (print→logging), C901 (complexity), E501 (line length), E402 (import order). Test fixes: SIM117 (combined with), FBT (boolean args), PERF203 (try in loop), S310/S607 (URLs/executables), E402/E501 (imports/lines), S108 (tmp paths), PLR0913 (too many args), ARG (unused args), ANN (type annotations), RUF059 (unused unpacked vars), PT019 (fixture naming). Remaining per-file-ignores (with justifications): - Tests: ARG, D, PLC0415, PLR2004, S101, SLF001 - music_gen sources: PLC0415 (heavy ML lazy imports) - moviepy_showcase: PLC0415 (circular dependency) - generate_images: PLR0913 (matplotlib helpers need many params) - praca_magisterska_video: E501, E402 (long paths, mpl.use)
391 lines
14 KiB
Python
391 lines
14 KiB
Python
"""Tests for uncovered branches in python_pkg.puzzle_solver.parse_image."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
|
|
from python_pkg.puzzle_solver.parse_image import (
|
|
_assign_teleporter_and_kl_groups,
|
|
_build_output,
|
|
_classify_all,
|
|
_detect_portal_side,
|
|
_has_interior_feature,
|
|
)
|
|
|
|
CV2 = "python_pkg.puzzle_solver.parse_image.cv2"
|
|
NP = "python_pkg.puzzle_solver.parse_image.np"
|
|
|
|
|
|
# ── _classify_all ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestClassifyAllPart2:
|
|
@patch("python_pkg.puzzle_solver.parse_image._classify_one")
|
|
def test_loop_body_populates_classified(self, mock_classify: MagicMock) -> None:
|
|
mock_classify.return_value = ("normal", {})
|
|
gray = MagicMock()
|
|
grid_map = {(0, 0): (10, 20, 30, 40)}
|
|
result = _classify_all(gray, grid_map)
|
|
assert (0, 0) in result
|
|
d = result[(0, 0)]
|
|
assert d["pos"] == [0, 0]
|
|
assert d["type"] == "normal"
|
|
assert d["pixel_center"] == [10 + 30 // 2, 20 + 40 // 2]
|
|
assert d["pixel_bbox"] == [10, 20, 30, 40]
|
|
|
|
@patch("python_pkg.puzzle_solver.parse_image._classify_one")
|
|
def test_multiple_entries(self, mock_classify: MagicMock) -> None:
|
|
mock_classify.side_effect = [
|
|
("player", {}),
|
|
("goal", {}),
|
|
]
|
|
gray = MagicMock()
|
|
grid_map = {
|
|
(0, 0): (0, 0, 20, 20),
|
|
(1, 1): (50, 50, 20, 20),
|
|
}
|
|
result = _classify_all(gray, grid_map)
|
|
assert len(result) == 2
|
|
assert result[(0, 0)]["type"] == "player"
|
|
assert result[(1, 1)]["type"] == "goal"
|
|
|
|
@patch("python_pkg.puzzle_solver.parse_image._classify_one")
|
|
def test_extra_dict_merged(self, mock_classify: MagicMock) -> None:
|
|
mock_classify.return_value = ("portal", {"side": "left"})
|
|
gray = MagicMock()
|
|
grid_map = {(2, 3): (100, 100, 40, 40)}
|
|
result = _classify_all(gray, grid_map)
|
|
assert result[(2, 3)]["side"] == "left"
|
|
|
|
|
|
# ── _detect_portal_side ──────────────────────────────────────────────
|
|
|
|
|
|
class TestDetectPortalSide:
|
|
def test_too_small_height(self) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (3, 20)
|
|
assert _detect_portal_side(interior) is None
|
|
|
|
def test_too_small_width(self) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (20, 3)
|
|
assert _detect_portal_side(interior) is None
|
|
|
|
@patch(NP)
|
|
def test_clear_best_side_left(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
# thirds_w=10, thirds_h=10
|
|
# Regions: left gets high value, others low
|
|
mock_np.mean.side_effect = [
|
|
50.0, # left
|
|
5.0, # right
|
|
5.0, # up
|
|
5.0, # down
|
|
]
|
|
result = _detect_portal_side(interior)
|
|
assert result == "left"
|
|
|
|
@patch(NP)
|
|
def test_clear_best_side_right(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
mock_np.mean.side_effect = [
|
|
5.0, # left
|
|
50.0, # right
|
|
5.0, # up
|
|
5.0, # down
|
|
]
|
|
result = _detect_portal_side(interior)
|
|
assert result == "right"
|
|
|
|
@patch(NP)
|
|
def test_clear_best_side_up(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
mock_np.mean.side_effect = [
|
|
5.0, # left
|
|
5.0, # right
|
|
50.0, # up
|
|
5.0, # down
|
|
]
|
|
result = _detect_portal_side(interior)
|
|
assert result == "up"
|
|
|
|
@patch(NP)
|
|
def test_clear_best_side_down(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
mock_np.mean.side_effect = [
|
|
5.0, # left
|
|
5.0, # right
|
|
5.0, # up
|
|
50.0, # down
|
|
]
|
|
result = _detect_portal_side(interior)
|
|
assert result == "down"
|
|
|
|
@patch(NP)
|
|
def test_no_clear_winner_returns_none(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
# All regions similar → best is not > max(opp*2.5, 8)
|
|
mock_np.mean.side_effect = [
|
|
6.0, # left
|
|
5.0, # right (opposite of left)
|
|
5.0, # up
|
|
5.0, # down
|
|
]
|
|
# best = left (6.0), opp = right (5.0)
|
|
# condition: 6.0 > max(5.0*2.5, 8) = max(12.5, 8) = 12.5 → False
|
|
result = _detect_portal_side(interior)
|
|
assert result is None
|
|
|
|
@patch(NP)
|
|
def test_best_above_threshold_8(self, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.shape = (30, 30)
|
|
# best > max(opp*2.5, 8) where opp is very small
|
|
mock_np.mean.side_effect = [
|
|
10.0, # left
|
|
1.0, # right (opposite of left)
|
|
1.0, # up
|
|
1.0, # down
|
|
]
|
|
# best = left (10.0), opp = right (1.0)
|
|
# condition: 10.0 > max(1.0*2.5, 8) = max(2.5, 8) = 8 → True
|
|
result = _detect_portal_side(interior)
|
|
assert result == "left"
|
|
|
|
|
|
# ── _has_interior_feature ────────────────────────────────────────────
|
|
|
|
|
|
class TestHasInteriorFeature:
|
|
@patch(NP)
|
|
@patch(CV2)
|
|
def test_feature_present(self, mock_cv2: MagicMock, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.size = 100
|
|
bw = np.zeros((10, 10), dtype=np.uint8)
|
|
mock_cv2.threshold.return_value = (None, bw)
|
|
# total_white > interior.size * 0.06 = 6
|
|
mock_np.sum.return_value = 10
|
|
assert _has_interior_feature(interior) is True
|
|
|
|
@patch(NP)
|
|
@patch(CV2)
|
|
def test_no_feature(self, mock_cv2: MagicMock, mock_np: MagicMock) -> None:
|
|
interior = MagicMock()
|
|
interior.size = 100
|
|
bw = np.zeros((10, 10), dtype=np.uint8)
|
|
mock_cv2.threshold.return_value = (None, bw)
|
|
mock_np.sum.return_value = 3
|
|
assert _has_interior_feature(interior) is False
|
|
|
|
|
|
# ── _assign_teleporter_and_kl_groups ─────────────────────────────────
|
|
|
|
|
|
class TestAssignTeleporterAndKlGroups:
|
|
def test_pair_by_matching_antenna_sides(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(1, 1): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
assert classified[(0, 0)]["group"] == classified[(1, 1)]["group"]
|
|
|
|
def test_skip_already_used_in_inner_loop(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(0, 1): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(1, 0): {"type": "teleporter", "antenna_sides": ["down"]},
|
|
(1, 1): {"type": "teleporter", "antenna_sides": ["down"]},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
# (0,0) pairs with (0,1), (1,0) pairs with (1,1)
|
|
assert classified[(0, 0)]["group"] == classified[(0, 1)]["group"]
|
|
assert classified[(1, 0)]["group"] == classified[(1, 1)]["group"]
|
|
assert classified[(0, 0)]["group"] != classified[(1, 0)]["group"]
|
|
|
|
def test_p1_already_used_skip(self) -> None:
|
|
# 3 teleporters with same sides; first two pair, third is unpaired
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(0, 1): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(0, 2): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
# (0,0) pairs with (0,1) by antenna match
|
|
# (0,2) remains unpaired by antenna, but gets sequential pairing? No,
|
|
# only 1 unpaired, can't pair sequentially (need pairs of 2)
|
|
assert classified[(0, 0)]["group"] == classified[(0, 1)]["group"]
|
|
# (0,2) ends up with no group since unpaired count is 1 (odd)
|
|
assert "group" not in classified[(0, 2)]
|
|
|
|
def test_unpaired_teleporters_sequential(self) -> None:
|
|
# Teleporters with non-matching antenna → no antenna pairing → sequential
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "teleporter", "antenna_sides": ["up"]},
|
|
(0, 1): {"type": "teleporter", "antenna_sides": ["down"]},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
# Neither antenna-pairs with the other, so both go to sequential
|
|
assert classified[(0, 0)]["group"] == classified[(0, 1)]["group"]
|
|
|
|
def test_key_lock_pairing(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "key_or_lock"},
|
|
(0, 1): {"type": "key_or_lock"},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
assert classified[(0, 0)]["type"] == "key"
|
|
assert classified[(0, 0)]["lock_id"] == 1
|
|
assert classified[(0, 1)]["type"] == "lock"
|
|
assert classified[(0, 1)]["lock_id"] == 1
|
|
|
|
def test_key_lock_odd_one_out(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "key_or_lock"},
|
|
(0, 1): {"type": "key_or_lock"},
|
|
(0, 2): {"type": "key_or_lock"},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
# First two pair, third becomes unknown
|
|
assert classified[(0, 0)]["type"] == "key"
|
|
assert classified[(0, 1)]["type"] == "lock"
|
|
assert classified[(0, 2)]["type"] == "unknown"
|
|
|
|
def test_no_teleporters_no_kl(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "normal"},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
assert classified[(0, 0)]["type"] == "normal"
|
|
|
|
def test_multiple_key_lock_pairs(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {"type": "key_or_lock"},
|
|
(0, 1): {"type": "key_or_lock"},
|
|
(1, 0): {"type": "key_or_lock"},
|
|
(1, 1): {"type": "key_or_lock"},
|
|
}
|
|
_assign_teleporter_and_kl_groups(classified)
|
|
assert classified[(0, 0)]["lock_id"] == 1
|
|
assert classified[(0, 1)]["lock_id"] == 1
|
|
assert classified[(1, 0)]["lock_id"] == 2
|
|
assert classified[(1, 1)]["lock_id"] == 2
|
|
|
|
|
|
# ── _build_output ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestBuildOutput:
|
|
def test_normal_square(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "normal",
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert len(result["squares"]) == 1
|
|
sq = result["squares"][0]
|
|
assert sq["pos"] == [0, 0]
|
|
assert sq["type"] == "normal"
|
|
assert sq["_pixel_center"] == [10, 10]
|
|
assert sq["_pixel_bbox"] == [0, 0, 20, 20]
|
|
assert result["notes"] == []
|
|
|
|
def test_portal_with_side(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "portal",
|
|
"side": "left",
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert result["squares"][0]["side"] == "left"
|
|
|
|
def test_teleporter_with_group(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "teleporter",
|
|
"group": 1,
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert result["squares"][0]["group"] == 1
|
|
|
|
def test_key_with_lock_id(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "key",
|
|
"lock_id": 1,
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert result["squares"][0]["lock_id"] == 1
|
|
|
|
def test_unknown_generates_note(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "unknown",
|
|
"fill_ratio": 0.2,
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert len(result["notes"]) == 1
|
|
assert "unknown" in result["notes"][0]
|
|
assert "fill=0.2" in result["notes"][0]
|
|
|
|
def test_unknown_no_fill_ratio(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "unknown",
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert "fill=?" in result["notes"][0]
|
|
|
|
def test_sorted_output(self) -> None:
|
|
classified: dict[tuple[int, int], dict[str, Any]] = {
|
|
(1, 0): {
|
|
"pos": [1, 0],
|
|
"type": "normal",
|
|
"pixel_center": [10, 10],
|
|
"pixel_bbox": [0, 0, 20, 20],
|
|
},
|
|
(0, 0): {
|
|
"pos": [0, 0],
|
|
"type": "normal",
|
|
"pixel_center": [5, 5],
|
|
"pixel_bbox": [0, 0, 10, 10],
|
|
},
|
|
}
|
|
result = _build_output(classified)
|
|
assert result["squares"][0]["pos"] == [0, 0]
|
|
assert result["squares"][1]["pos"] == [1, 0]
|