forked from UNN/2026-rff_mp
163 lines
6.6 KiB
Python
163 lines
6.6 KiB
Python
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)
|