SearchAlgorithms / backend /tests /test_algorithms.py
Kacemath's picture
feat: update with latest changes
47bba68
"""Tests for search algorithms (BFS, DFS, UCS, A*, Greedy, IDS)."""
import pytest
from app.algorithms.bfs import bfs_search
from app.algorithms.dfs import dfs_search
from app.algorithms.ucs import ucs_search
from app.algorithms.astar import astar_search
from app.algorithms.greedy import greedy_search
from app.algorithms.ids import ids_search
from app.heuristics import manhattan_heuristic, euclidean_heuristic
from app.core.delivery_search import DeliverySearch
from app.models.grid import Grid
class TestBFS:
"""Tests for Breadth-First Search."""
def test_bfs_finds_path(self, simple_grid):
"""Test BFS finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, steps = bfs_search(search, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
assert len(result.path) > 0
assert result.path[0] == (0, 0)
assert result.path[-1] == (2, 2)
def test_bfs_shortest_path(self, simple_grid):
"""Test BFS finds shortest path by steps."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = bfs_search(search, visualize=False)
# BFS should find path with minimum steps
# From (0,0) to (2,2) needs at least 4 steps
assert len(result.path) == 5 # 5 nodes including start and end
def test_bfs_no_path(self):
"""Test BFS when no path exists."""
grid = Grid(width=3, height=3)
# Create disconnected grid - only add vertical segment
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = bfs_search(search, visualize=False)
assert result.plan == ""
assert result.cost == float("inf")
assert len(result.path) == 0
def test_bfs_same_start_goal(self, simple_grid):
"""Test BFS when start equals goal."""
search = DeliverySearch(simple_grid, (1, 1), (1, 1), [])
result, _ = bfs_search(search, visualize=False)
assert result.cost == 0
assert len(result.path) == 1
assert result.path[0] == (1, 1)
def test_bfs_visualization(self, simple_grid):
"""Test BFS with visualization enabled."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, steps = bfs_search(search, visualize=True)
assert steps is not None
assert len(steps) > 0
# First step should have start node
assert steps[0].current_node == (0, 0)
class TestDFS:
"""Tests for Depth-First Search."""
def test_dfs_finds_path(self, simple_grid):
"""Test DFS finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, steps = dfs_search(search, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
assert len(result.path) > 0
assert result.path[0] == (0, 0)
assert result.path[-1] == (2, 2)
def test_dfs_no_path(self):
"""Test DFS when no path exists."""
grid = Grid(width=3, height=3)
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = dfs_search(search, visualize=False)
assert result.plan == ""
assert result.cost == float("inf")
def test_dfs_same_start_goal(self, simple_grid):
"""Test DFS when start equals goal."""
search = DeliverySearch(simple_grid, (1, 1), (1, 1), [])
result, _ = dfs_search(search, visualize=False)
assert result.cost == 0
assert len(result.path) == 1
class TestUCS:
"""Tests for Uniform Cost Search."""
def test_ucs_finds_optimal_path(self, grid_with_varied_traffic):
"""Test UCS finds optimal (minimum cost) path."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
result, _ = ucs_search(search, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
def test_ucs_prefers_low_traffic(self, grid_with_varied_traffic):
"""Test UCS prefers lower traffic segments."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
ucs_result, _ = ucs_search(search, visualize=False)
bfs_result, _ = bfs_search(search, visualize=False)
# UCS should find equal or better cost than BFS
assert ucs_result.cost <= bfs_result.cost
def test_ucs_no_path(self):
"""Test UCS when no path exists."""
grid = Grid(width=3, height=3)
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = ucs_search(search, visualize=False)
assert result.cost == float("inf")
def test_ucs_same_start_goal(self, simple_grid):
"""Test UCS when start equals goal."""
search = DeliverySearch(simple_grid, (1, 1), (1, 1), [])
result, _ = ucs_search(search, visualize=False)
assert result.cost == 0
class TestAStar:
"""Tests for A* Search."""
def test_astar_manhattan_finds_path(self, simple_grid):
"""Test A* with Manhattan heuristic finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = astar_search(search, manhattan_heuristic, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
assert result.path[0] == (0, 0)
assert result.path[-1] == (2, 2)
def test_astar_euclidean_finds_path(self, simple_grid):
"""Test A* with Euclidean heuristic finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = astar_search(search, euclidean_heuristic, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
def test_astar_optimal(self, grid_with_varied_traffic):
"""Test A* finds optimal path (same as UCS with admissible heuristic)."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
astar_result, _ = astar_search(search, manhattan_heuristic, visualize=False)
ucs_result, _ = ucs_search(search, visualize=False)
# A* with admissible heuristic should find same cost as UCS
assert astar_result.cost == ucs_result.cost
def test_astar_fewer_nodes_than_ucs(self, larger_grid):
"""Test A* expands fewer nodes than UCS."""
search = DeliverySearch(larger_grid, (0, 0), (4, 4), [])
astar_result, _ = astar_search(search, manhattan_heuristic, visualize=False)
ucs_result, _ = ucs_search(search, visualize=False)
# A* should expand fewer or equal nodes
assert astar_result.nodes_expanded <= ucs_result.nodes_expanded
def test_astar_no_path(self):
"""Test A* when no path exists."""
grid = Grid(width=3, height=3)
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = astar_search(search, manhattan_heuristic, visualize=False)
assert result.cost == float("inf")
class TestGreedy:
"""Tests for Greedy Best-First Search."""
def test_greedy_manhattan_finds_path(self, simple_grid):
"""Test Greedy with Manhattan heuristic finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = greedy_search(search, manhattan_heuristic, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
def test_greedy_euclidean_finds_path(self, simple_grid):
"""Test Greedy with Euclidean heuristic finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = greedy_search(search, euclidean_heuristic, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
def test_greedy_fast_but_suboptimal(self, grid_with_varied_traffic):
"""Test Greedy is fast but may not find optimal path."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
greedy_result, _ = greedy_search(search, manhattan_heuristic, visualize=False)
ucs_result, _ = ucs_search(search, visualize=False)
# Greedy may find suboptimal path
assert greedy_result.cost >= ucs_result.cost
def test_greedy_no_path(self):
"""Test Greedy when no path exists."""
grid = Grid(width=3, height=3)
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = greedy_search(search, manhattan_heuristic, visualize=False)
assert result.cost == float("inf")
class TestIDS:
"""Tests for Iterative Deepening Search."""
def test_ids_finds_path(self, simple_grid):
"""Test IDS finds a path."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
result, _ = ids_search(search, visualize=False)
assert result.plan != ""
assert result.cost < float("inf")
assert result.path[0] == (0, 0)
assert result.path[-1] == (2, 2)
def test_ids_optimal_steps(self, simple_grid):
"""Test IDS finds path with optimal number of steps."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
ids_result, _ = ids_search(search, visualize=False)
bfs_result, _ = bfs_search(search, visualize=False)
# IDS should find same path length as BFS
assert len(ids_result.path) == len(bfs_result.path)
def test_ids_no_path(self):
"""Test IDS when no path exists."""
grid = Grid(width=3, height=3)
grid.add_segment((0, 0), (0, 1), 1)
search = DeliverySearch(grid, (0, 0), (2, 2), [])
result, _ = ids_search(search, visualize=False)
assert result.cost == float("inf")
def test_ids_same_start_goal(self, simple_grid):
"""Test IDS when start equals goal."""
search = DeliverySearch(simple_grid, (1, 1), (1, 1), [])
result, _ = ids_search(search, visualize=False)
assert result.cost == 0
assert len(result.path) == 1
class TestAlgorithmComparison:
"""Tests comparing different algorithms."""
def test_all_algorithms_find_same_goal(self, simple_grid):
"""Test all algorithms reach the same goal."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
bfs_result, _ = bfs_search(search, visualize=False)
dfs_result, _ = dfs_search(search, visualize=False)
ucs_result, _ = ucs_search(search, visualize=False)
astar_result, _ = astar_search(search, manhattan_heuristic, visualize=False)
greedy_result, _ = greedy_search(search, manhattan_heuristic, visualize=False)
ids_result, _ = ids_search(search, visualize=False)
# All should find a path ending at goal
assert bfs_result.path[-1] == (2, 2)
assert dfs_result.path[-1] == (2, 2)
assert ucs_result.path[-1] == (2, 2)
assert astar_result.path[-1] == (2, 2)
assert greedy_result.path[-1] == (2, 2)
assert ids_result.path[-1] == (2, 2)
def test_optimal_algorithms_same_cost(self, grid_with_varied_traffic):
"""Test UCS and A* find same optimal cost."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
ucs_result, _ = ucs_search(search, visualize=False)
astar_result, _ = astar_search(search, manhattan_heuristic, visualize=False)
assert ucs_result.cost == astar_result.cost
def test_visualization_consistency(self, simple_grid):
"""Test visualization steps are consistent across algorithms."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
_, bfs_steps = bfs_search(search, visualize=True)
_, ucs_steps = ucs_search(search, visualize=True)
# Both should have visualization steps
assert bfs_steps is not None
assert ucs_steps is not None
assert len(bfs_steps) > 0
assert len(ucs_steps) > 0