2026-rff_mp/skorohodovsa/task_2/test/test_strategies.py
2026-05-25 10:23:00 +03:00

163 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)