Kacemath's picture
feat: update with latest changes
47bba68
"""Tests for core modules (delivery_search, delivery_planner)."""
import pytest
from app.core.delivery_search import DeliverySearch
from app.core.delivery_planner import DeliveryPlanner
from app.core.node import SearchNode
from app.core.frontier import QueueFrontier, StackFrontier, PriorityQueueFrontier
from app.models.grid import Grid
from app.models.entities import Store, Destination, Tunnel
class TestSearchNode:
"""Tests for SearchNode."""
def test_node_creation(self):
"""Test basic node creation."""
node = SearchNode(state=(0, 0), path_cost=0, depth=0)
assert node.state == (0, 0)
assert node.path_cost == 0
assert node.depth == 0
assert node.parent is None
assert node.action is None
def test_node_with_parent(self):
"""Test node with parent."""
parent = SearchNode(state=(0, 0), path_cost=0, depth=0)
child = SearchNode(
state=(1, 0),
parent=parent,
action="right",
path_cost=1,
depth=1,
)
assert child.parent == parent
assert child.action == "right"
assert child.depth == 1
def test_get_path(self):
"""Test path reconstruction."""
root = SearchNode(state=(0, 0), path_cost=0, depth=0)
child1 = SearchNode(state=(1, 0), parent=root, action="right", path_cost=1, depth=1)
child2 = SearchNode(state=(2, 0), parent=child1, action="right", path_cost=2, depth=2)
path = child2.get_path()
assert path == [(0, 0), (1, 0), (2, 0)]
def test_get_solution(self):
"""Test solution string generation."""
root = SearchNode(state=(0, 0), path_cost=0, depth=0)
child1 = SearchNode(state=(1, 0), parent=root, action="right", path_cost=1, depth=1)
child2 = SearchNode(state=(1, 1), parent=child1, action="up", path_cost=2, depth=2)
solution = child2.get_solution()
assert solution == "right,up"
def test_get_solution_single_node(self):
"""Test solution for single node (start = goal)."""
root = SearchNode(state=(0, 0), path_cost=0, depth=0)
assert root.get_solution() == ""
class TestFrontiers:
"""Tests for frontier data structures."""
def test_queue_frontier_fifo(self):
"""Test queue frontier is FIFO."""
frontier = QueueFrontier()
node1 = SearchNode(state=(0, 0), path_cost=0, depth=0)
node2 = SearchNode(state=(1, 0), path_cost=1, depth=1)
node3 = SearchNode(state=(2, 0), path_cost=2, depth=2)
frontier.push(node1)
frontier.push(node2)
frontier.push(node3)
assert frontier.pop().state == (0, 0)
assert frontier.pop().state == (1, 0)
assert frontier.pop().state == (2, 0)
def test_stack_frontier_lifo(self):
"""Test stack frontier is LIFO."""
frontier = StackFrontier()
node1 = SearchNode(state=(0, 0), path_cost=0, depth=0)
node2 = SearchNode(state=(1, 0), path_cost=1, depth=1)
node3 = SearchNode(state=(2, 0), path_cost=2, depth=2)
frontier.push(node1)
frontier.push(node2)
frontier.push(node3)
assert frontier.pop().state == (2, 0)
assert frontier.pop().state == (1, 0)
assert frontier.pop().state == (0, 0)
def test_priority_queue_frontier(self):
"""Test priority queue frontier orders by priority."""
frontier = PriorityQueueFrontier()
# Priority is stored in node.priority attribute
node1 = SearchNode(state=(0, 0), path_cost=5, depth=0)
node1.priority = 5
node2 = SearchNode(state=(1, 0), path_cost=1, depth=1)
node2.priority = 1
node3 = SearchNode(state=(2, 0), path_cost=3, depth=2)
node3.priority = 3
frontier.push(node1)
frontier.push(node2)
frontier.push(node3)
assert frontier.pop().state == (1, 0) # Lowest priority first
assert frontier.pop().state == (2, 0)
assert frontier.pop().state == (0, 0)
def test_frontier_is_empty(self):
"""Test frontier empty check."""
frontier = QueueFrontier()
assert frontier.is_empty() is True
frontier.push(SearchNode(state=(0, 0), path_cost=0, depth=0))
assert frontier.is_empty() is False
frontier.pop()
assert frontier.is_empty() is True
def test_frontier_contains_state(self):
"""Test frontier state containment check."""
frontier = QueueFrontier()
frontier.push(SearchNode(state=(0, 0), path_cost=0, depth=0))
frontier.push(SearchNode(state=(1, 0), path_cost=1, depth=1))
assert frontier.contains_state((0, 0)) is True
assert frontier.contains_state((1, 0)) is True
assert frontier.contains_state((2, 0)) is False
def test_frontier_get_states(self):
"""Test getting all states in frontier."""
frontier = QueueFrontier()
frontier.push(SearchNode(state=(0, 0), path_cost=0, depth=0))
frontier.push(SearchNode(state=(1, 0), path_cost=1, depth=1))
states = frontier.get_states()
assert (0, 0) in states
assert (1, 0) in states
class TestDeliverySearch:
"""Tests for DeliverySearch."""
def test_initial_state(self, simple_grid):
"""Test initial state is set correctly."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
assert search.initial_state() == (0, 0)
def test_goal_test(self, simple_grid):
"""Test goal test function."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
assert search.goal_test((2, 2)) is True
assert search.goal_test((0, 0)) is False
assert search.goal_test((1, 1)) is False
def test_actions(self, simple_grid):
"""Test available actions."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
# Corner has 2 actions
actions = search.actions((0, 0))
assert "up" in actions
assert "right" in actions
assert "down" not in actions
assert "left" not in actions
# Center has 4 actions
actions = search.actions((1, 1))
assert len(actions) == 4
def test_actions_with_tunnel(self, simple_grid):
"""Test tunnel action availability."""
tunnel = Tunnel(entrance1=(0, 0), entrance2=(2, 2))
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [tunnel])
actions = search.actions((0, 0))
assert "tunnel" in actions
actions = search.actions((1, 1))
assert "tunnel" not in actions
def test_result_movement(self, simple_grid):
"""Test result of movement actions."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
assert search.result((1, 1), "up") == (1, 2)
assert search.result((1, 1), "down") == (1, 0)
assert search.result((1, 1), "left") == (0, 1)
assert search.result((1, 1), "right") == (2, 1)
def test_result_tunnel(self, simple_grid):
"""Test result of tunnel action."""
tunnel = Tunnel(entrance1=(0, 0), entrance2=(2, 2))
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [tunnel])
assert search.result((0, 0), "tunnel") == (2, 2)
assert search.result((2, 2), "tunnel") == (0, 0)
def test_step_cost_movement(self, grid_with_varied_traffic):
"""Test step cost for movement."""
search = DeliverySearch(grid_with_varied_traffic, (0, 0), (2, 2), [])
# Cost should equal traffic level
cost = search.step_cost((0, 0), "right", (1, 0))
assert cost == grid_with_varied_traffic.get_traffic((0, 0), (1, 0))
def test_step_cost_tunnel(self, simple_grid):
"""Test step cost for tunnel (Manhattan distance)."""
tunnel = Tunnel(entrance1=(0, 0), entrance2=(2, 2))
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [tunnel])
cost = search.step_cost((0, 0), "tunnel", (2, 2))
assert cost == tunnel.cost # Manhattan distance = 4
def test_solve_all_strategies(self, simple_grid):
"""Test solve method with all strategies."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
strategies = ["BF", "DF", "ID", "UC", "GR1", "GR2", "AS1", "AS2"]
for strategy in strategies:
result, _ = search.solve(strategy, visualize=False)
assert result.path[-1] == (2, 2), f"Strategy {strategy} failed"
def test_solve_invalid_strategy(self, simple_grid):
"""Test solve with invalid strategy."""
search = DeliverySearch(simple_grid, (0, 0), (2, 2), [])
with pytest.raises(ValueError):
search.solve("INVALID", visualize=False)
def test_path_static_method(self, simple_grid):
"""Test static path method."""
result, steps = DeliverySearch.path(
simple_grid,
(0, 0),
(2, 2),
[],
"BF",
visualize=False,
)
assert result.path[0] == (0, 0)
assert result.path[-1] == (2, 2)
class TestDeliveryPlanner:
"""Tests for DeliveryPlanner."""
def test_planner_single_destination(self, simple_grid):
"""Test planner with single destination."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, _ = planner.plan("BF", visualize=False)
assert len(result.assignments) == 1
assert result.assignments[0].store_id == 1
assert result.assignments[0].destination_id == 1
assert result.total_cost > 0
def test_planner_multiple_destinations(self, simple_grid):
"""Test planner with multiple destinations."""
stores = [Store(id=1, position=(0, 0))]
destinations = [
Destination(id=1, position=(2, 0)),
Destination(id=2, position=(0, 2)),
Destination(id=3, position=(2, 2)),
]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, _ = planner.plan("BF", visualize=False)
assert len(result.assignments) == 3
assert result.total_cost > 0
def test_planner_multiple_stores(self, simple_grid):
"""Test planner assigns to nearest store."""
stores = [
Store(id=1, position=(0, 0)),
Store(id=2, position=(2, 2)),
]
destinations = [Destination(id=1, position=(2, 0))]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, _ = planner.plan("UC", visualize=False)
# Should assign to store 1 (closer to destination (2,0))
assert len(result.assignments) == 1
def test_planner_with_tunnels(self, simple_grid):
"""Test planner uses tunnels."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
tunnels = [Tunnel(entrance1=(0, 0), entrance2=(2, 2))]
planner = DeliveryPlanner(simple_grid, stores, destinations, tunnels)
result, _ = planner.plan("UC", visualize=False)
# Tunnel cost is 4 (Manhattan distance)
# Without tunnel, minimum cost would be 4 (each segment has traffic 1)
assert result.total_cost == 4
def test_planner_from_state(self, simple_grid):
"""Test static plan_from_state method."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
result, _ = DeliveryPlanner.plan_from_state(
simple_grid,
stores,
destinations,
[],
"BF",
visualize=False,
)
assert len(result.assignments) == 1
assert result.total_cost > 0
def test_planner_total_nodes(self, simple_grid):
"""Test total nodes expanded tracking."""
stores = [Store(id=1, position=(0, 0))]
destinations = [
Destination(id=1, position=(2, 0)),
Destination(id=2, position=(0, 2)),
]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, _ = planner.plan("BF", visualize=False)
assert result.total_nodes_expanded > 0
def test_planner_visualization(self, simple_grid):
"""Test planner with visualization."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, viz_data = planner.plan("BF", visualize=True)
assert viz_data is not None
assert 1 in viz_data # Destination ID 1 should have steps
def test_planner_result_to_string(self, simple_grid):
"""Test plan result string formatting."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
planner = DeliveryPlanner(simple_grid, stores, destinations, [])
result, _ = planner.plan("BF", visualize=False)
output = result.to_string()
assert isinstance(output, str)
assert len(output) > 0
def test_planner_all_strategies(self, simple_grid):
"""Test planner works with all strategies."""
stores = [Store(id=1, position=(0, 0))]
destinations = [Destination(id=1, position=(2, 2))]
strategies = ["BF", "DF", "ID", "UC", "GR1", "GR2", "AS1", "AS2"]
for strategy in strategies:
result, _ = DeliveryPlanner.plan_from_state(
simple_grid, stores, destinations, [], strategy, False
)
assert len(result.assignments) == 1, f"Strategy {strategy} failed"