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)