2026-rff_mp/skorohodovsa/task_2/test/test_strategies.py

163 lines
6.6 KiB
Python
Raw Normal View History

2026-05-25 07:23:00 +00:00
import pytest
from source.models.base import Maze, Cell
from source.settings import cell_mapping
from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy
def make_open_maze(width: int = 5, height: int = 5) -> Maze:
"""Открытый лабиринт без внутренних стен, S в углу, E в противоположном."""
maze = Maze(size=(width, height))
maze[0, 0] = cell_mapping["start"]
maze[height - 1, width - 1] = cell_mapping["exit"]
return maze
def make_blocked_maze() -> Maze:
"""Лабиринт где S и E разделены сплошной стеной — пути нет."""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping["start"]
maze[4, 4] = cell_mapping["exit"]
for col in range(5):
maze[2, col] = cell_mapping["wall"]
return maze
def make_corridor_maze() -> Maze:
"""Узкий коридор 1×5: S → . → . → . → E."""
maze = Maze(size=(5, 1))
maze[0, 0] = cell_mapping["start"]
maze[0, 4] = cell_mapping["exit"]
return maze
STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy]
STRATEGY_IDS = ["BFS", "DFS", "A*"]
# ---------------------------------------------------------------------------- #
# Общие тесты для всех стратегий #
# ---------------------------------------------------------------------------- #
class TestAllStrategies:
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_returns_list(self, StrategyClass):
"""find_path всегда возвращает список."""
maze = make_open_maze()
result = StrategyClass().find_path(maze)
assert isinstance(result, list)
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_starts_with_start(self, StrategyClass):
"""Первая клетка пути — старт."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert path[0] is maze.start
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_ends_with_exit(self, StrategyClass):
"""Последняя клетка пути — выход."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert path[-1] is maze.exit
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_cells_are_passable(self, StrategyClass):
"""Все клетки пути проходимы."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert all(cell.is_possible() for cell in path)
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_cells_are_neighbors(self, StrategyClass):
"""Каждая следующая клетка пути — сосед предыдущей."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
for a, b in zip(path, path[1:]):
assert abs(a.x - b.x) + abs(a.y - b.y) == 1
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_no_path_returns_empty(self, StrategyClass):
"""Если пути нет — возвращает пустой список."""
maze = make_blocked_maze()
path = StrategyClass().find_path(maze)
assert path == []
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_corridor_path_length(self, StrategyClass):
"""В коридоре 1×5 путь содержит ровно 5 клеток."""
maze = make_corridor_maze()
path = StrategyClass().find_path(maze)
assert len(path) == 5
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_maze_not_modified(self, StrategyClass):
"""Алгоритм не изменяет состояние лабиринта."""
maze = make_open_maze()
before = str(maze)
StrategyClass().find_path(maze)
assert str(maze) == before
# ---------------------------------------------------------------------------- #
# Тесты специфичные для BFS и A* (оптимальность пути) #
# ---------------------------------------------------------------------------- #
class TestOptimalStrategies:
@pytest.mark.parametrize(
"StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"]
)
def test_shortest_path_in_corridor(self, StrategyClass):
"""BFS и A* находят кратчайший путь в коридоре."""
maze = make_corridor_maze()
path = StrategyClass().find_path(maze)
assert len(path) == 5
@pytest.mark.parametrize(
"StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"]
)
def test_bfs_and_astar_same_length(self, StrategyClass):
"""BFS и A* возвращают путь одинаковой длины на открытом лабиринте."""
maze = make_open_maze(7, 7)
bfs_len = len(BFSStrategy().find_path(maze))
astar_len = len(AStarStrategy().find_path(maze))
assert bfs_len == astar_len
# ---------------------------------------------------------------------------- #
# Тесты с явной передачей start / exit #
# ---------------------------------------------------------------------------- #
class TestExplicitStartExit:
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_explicit_start_and_exit(self, StrategyClass):
"""find_path работает с явно переданными start и exit."""
maze = Maze(size=(5, 5))
start = maze.get_cell(0, 0)
exit = maze.get_cell(4, 4)
maze[0, 0] = cell_mapping["start"]
maze[4, 4] = cell_mapping["exit"]
path = StrategyClass().find_path(maze, start=start, exit=exit)
assert path[0] is start
assert path[-1] is exit
class TestEdgeCases:
def test_no_start_raises(self):
"""Если нет старта — ValueError."""
maze = Maze(size=(5, 5))
maze[4, 4] = cell_mapping["exit"]
with pytest.raises(ValueError):
BFSStrategy().find_path(maze)
def test_no_exit_raises(self):
"""Если нет выхода — ValueError."""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping["start"]
with pytest.raises(ValueError):
BFSStrategy().find_path(maze)