Download brain/tests/test_allocation.py from MouleeswaranM/FairRelay: direct link, hf CLI and curl.
- Browser
- Download file 9.74 kB
-
https://huggingface.co/MouleeswaranM/FairRelay/resolve/refs%2Fpr%2F21/brain/tests/test_allocation.py
- Command line
-
hf download hf://MouleeswaranM/FairRelay@refs/pr/21/brain/tests/test_allocation.py
-
curl -L -o test_allocation.py https://huggingface.co/MouleeswaranM/FairRelay/resolve/refs%2Fpr%2F21/brain/tests/test_allocation.py
9.74 kB
| """ | |
| Unit tests for allocation logic. | |
| Tests route clustering and driver-route assignment. | |
| """ | |
| import pytest | |
| from app.services.clustering import cluster_packages, order_stops_by_nearest_neighbor, haversine_distance | |
| from app.services.allocation import allocate_routes, build_cost_matrix, greedy_allocate | |
| class TestClustering: | |
| """Tests for package clustering.""" | |
| def test_cluster_single_package(self): | |
| """Single package should create single cluster.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 2.0, "address": "Addr 1"} | |
| ] | |
| result = cluster_packages(packages, num_drivers=3) | |
| assert len(result) == 1 | |
| assert result[0].num_packages == 1 | |
| def test_cluster_multiple_packages(self): | |
| """Multiple packages should create appropriate clusters.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 2.0, "address": "Addr 1"}, | |
| {"latitude": 12.98, "longitude": 77.60, "weight_kg": 3.0, "address": "Addr 2"}, | |
| {"latitude": 12.99, "longitude": 77.61, "weight_kg": 1.5, "address": "Addr 3"}, | |
| {"latitude": 13.00, "longitude": 77.62, "weight_kg": 2.5, "address": "Addr 4"}, | |
| ] | |
| result = cluster_packages(packages, num_drivers=2, target_per_route=2) | |
| # Should create 2 clusters with 2 packages each | |
| assert len(result) == 2 | |
| total_packages = sum(c.num_packages for c in result) | |
| assert total_packages == 4 | |
| def test_cluster_weight_calculation(self): | |
| """Cluster should correctly sum package weights.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 2.0, "address": "Addr 1"}, | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 3.0, "address": "Addr 2"}, | |
| ] | |
| result = cluster_packages(packages, num_drivers=1, target_per_route=10) | |
| assert len(result) == 1 | |
| assert result[0].total_weight_kg == 5.0 | |
| def test_cluster_unique_stops(self): | |
| """Cluster should count unique addresses as stops.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 2.0, "address": "Same Address"}, | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 3.0, "address": "Same Address"}, | |
| {"latitude": 12.98, "longitude": 77.60, "weight_kg": 1.0, "address": "Different Address"}, | |
| ] | |
| result = cluster_packages(packages, num_drivers=1, target_per_route=10) | |
| assert len(result) == 1 | |
| assert result[0].num_packages == 3 | |
| assert result[0].num_stops == 2 # Only 2 unique addresses | |
| def test_cluster_empty_packages(self): | |
| """Empty package list should return empty clusters.""" | |
| result = cluster_packages([], num_drivers=3) | |
| assert len(result) == 0 | |
| def test_cluster_more_drivers_than_packages(self): | |
| """Should create at most num_packages clusters.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "weight_kg": 2.0, "address": "Addr 1"}, | |
| {"latitude": 12.98, "longitude": 77.60, "weight_kg": 3.0, "address": "Addr 2"}, | |
| ] | |
| result = cluster_packages(packages, num_drivers=5, target_per_route=1) | |
| assert len(result) <= 2 | |
| class TestStopOrdering: | |
| """Tests for stop ordering using nearest neighbor.""" | |
| def test_order_single_package(self): | |
| """Single package should remain unchanged.""" | |
| packages = [ | |
| {"latitude": 12.97, "longitude": 77.59, "address": "Addr 1"} | |
| ] | |
| result = order_stops_by_nearest_neighbor(packages, 12.90, 77.50) | |
| assert len(result) == 1 | |
| assert result[0]["address"] == "Addr 1" | |
| def test_order_nearest_first(self): | |
| """Should visit nearest package first.""" | |
| packages = [ | |
| {"latitude": 13.00, "longitude": 77.60, "address": "Far"}, | |
| {"latitude": 12.91, "longitude": 77.51, "address": "Near"}, | |
| ] | |
| result = order_stops_by_nearest_neighbor(packages, 12.90, 77.50) | |
| # Near should be first | |
| assert result[0]["address"] == "Near" | |
| assert result[1]["address"] == "Far" | |
| def test_order_empty_list(self): | |
| """Empty list should return empty.""" | |
| result = order_stops_by_nearest_neighbor([], 12.90, 77.50) | |
| assert len(result) == 0 | |
| class TestHaversineDistance: | |
| """Tests for haversine distance calculation.""" | |
| def test_distance_same_point(self): | |
| """Same point should have zero distance.""" | |
| result = haversine_distance(12.97, 77.59, 12.97, 77.59) | |
| assert result == 0.0 | |
| def test_distance_known_locations(self): | |
| """Test with known approximate distance.""" | |
| # Bangalore to Chennai is approximately 350 km | |
| result = haversine_distance(12.9716, 77.5946, 13.0827, 80.2707) | |
| assert 280 < result < 320 # Approximate range | |
| def test_distance_symmetric(self): | |
| """Distance should be symmetric.""" | |
| dist1 = haversine_distance(12.97, 77.59, 13.00, 77.60) | |
| dist2 = haversine_distance(13.00, 77.60, 12.97, 77.59) | |
| assert abs(dist1 - dist2) < 0.001 | |
| class TestAllocation: | |
| """Tests for driver-route allocation.""" | |
| def test_allocate_equal_drivers_routes(self): | |
| """Equal drivers and routes should match 1:1.""" | |
| drivers = [ | |
| {"external_id": "d1", "vehicle_capacity_kg": 100}, | |
| {"external_id": "d2", "vehicle_capacity_kg": 100}, | |
| {"external_id": "d3", "vehicle_capacity_kg": 100}, | |
| ] | |
| routes = [ | |
| {"workload_score": 50.0, "total_weight_kg": 30.0}, | |
| {"workload_score": 60.0, "total_weight_kg": 40.0}, | |
| {"workload_score": 55.0, "total_weight_kg": 35.0}, | |
| ] | |
| result = allocate_routes(drivers, routes) | |
| assert len(result) == 3 | |
| driver_indices = {r.driver_index for r in result} | |
| route_indices = {r.route_index for r in result} | |
| assert driver_indices == {0, 1, 2} | |
| assert route_indices == {0, 1, 2} | |
| def test_allocate_more_drivers(self): | |
| """More drivers than routes should leave some unassigned.""" | |
| drivers = [ | |
| {"external_id": "d1", "vehicle_capacity_kg": 100}, | |
| {"external_id": "d2", "vehicle_capacity_kg": 100}, | |
| {"external_id": "d3", "vehicle_capacity_kg": 100}, | |
| ] | |
| routes = [ | |
| {"workload_score": 50.0, "total_weight_kg": 30.0}, | |
| {"workload_score": 60.0, "total_weight_kg": 40.0}, | |
| ] | |
| result = allocate_routes(drivers, routes) | |
| assert len(result) == 2 # Only 2 routes assigned | |
| def test_allocate_more_routes(self): | |
| """More routes than drivers should leave some routes unassigned.""" | |
| drivers = [ | |
| {"external_id": "d1", "vehicle_capacity_kg": 100}, | |
| ] | |
| routes = [ | |
| {"workload_score": 50.0, "total_weight_kg": 30.0}, | |
| {"workload_score": 60.0, "total_weight_kg": 40.0}, | |
| {"workload_score": 55.0, "total_weight_kg": 35.0}, | |
| ] | |
| result = allocate_routes(drivers, routes) | |
| assert len(result) == 1 # Only 1 driver available | |
| def test_allocate_empty_inputs(self): | |
| """Empty inputs should return empty results.""" | |
| assert allocate_routes([], []) == [] | |
| assert allocate_routes([{"external_id": "d1", "vehicle_capacity_kg": 100}], []) == [] | |
| assert allocate_routes([], [{"workload_score": 50.0, "total_weight_kg": 30.0}]) == [] | |
| class TestCostMatrix: | |
| """Tests for cost matrix building.""" | |
| def test_cost_matrix_shape(self): | |
| """Cost matrix should have correct shape.""" | |
| drivers = [ | |
| {"external_id": "d1", "vehicle_capacity_kg": 100}, | |
| {"external_id": "d2", "vehicle_capacity_kg": 100}, | |
| ] | |
| routes = [ | |
| {"workload_score": 50.0, "total_weight_kg": 30.0}, | |
| {"workload_score": 60.0, "total_weight_kg": 40.0}, | |
| {"workload_score": 55.0, "total_weight_kg": 35.0}, | |
| ] | |
| result = build_cost_matrix(drivers, routes) | |
| assert result.shape == (2, 3) | |
| def test_cost_matrix_values(self): | |
| """Cost should be based on workload score.""" | |
| drivers = [{"external_id": "d1", "vehicle_capacity_kg": 100}] | |
| routes = [{"workload_score": 42.0, "total_weight_kg": 30.0}] | |
| result = build_cost_matrix(drivers, routes) | |
| assert result[0, 0] == 42.0 | |
| def test_cost_matrix_capacity_penalty(self): | |
| """Over-capacity should add penalty.""" | |
| drivers = [{"external_id": "d1", "vehicle_capacity_kg": 20}] # Low capacity | |
| routes = [{"workload_score": 50.0, "total_weight_kg": 50.0}] # Heavy route | |
| result = build_cost_matrix(drivers, routes) | |
| # Should be penalized: 50 + (50-20)*10 = 50 + 300 = 350 | |
| assert result[0, 0] > 50.0 | |
| class TestGreedyAllocate: | |
| """Tests for greedy allocation fallback.""" | |
| def test_greedy_basic(self): | |
| """Greedy should match in order.""" | |
| drivers = [ | |
| {"external_id": "d1"}, | |
| {"external_id": "d2"}, | |
| ] | |
| routes = [ | |
| {"workload_score": 50.0}, | |
| {"workload_score": 60.0}, | |
| ] | |
| result = greedy_allocate(drivers, routes) | |
| assert len(result) == 2 | |
| assert result[0].driver_index == 0 | |
| assert result[0].route_index == 0 | |
| assert result[1].driver_index == 1 | |
| assert result[1].route_index == 1 | |