blackopsrepl commited on
Commit
d02874d
·
verified ·
1 Parent(s): db6069c

Delete tests

Browse files
tests/.pytest_cache/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- # Created by pytest automatically.
2
- *
 
 
 
tests/.pytest_cache/CACHEDIR.TAG DELETED
@@ -1,4 +0,0 @@
1
- Signature: 8a477f597d28d172789f06886806bc55
2
- # This file is a cache directory tag created by pytest.
3
- # For information about cache directory tags, see:
4
- # https://bford.info/cachedir/spec.html
 
 
 
 
 
tests/.pytest_cache/README.md DELETED
@@ -1,8 +0,0 @@
1
- # pytest cache directory #
2
-
3
- This directory contains data from the pytest's cache plugin,
4
- which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
-
6
- **Do not** commit this to version control.
7
-
8
- See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
 
 
 
 
 
 
 
 
 
tests/.pytest_cache/v/cache/lastfailed DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "test_constraints.py": true,
3
- "test_feasible.py": true
4
- }
 
 
 
 
 
tests/.pytest_cache/v/cache/nodeids DELETED
@@ -1 +0,0 @@
1
- []
 
 
tests/.pytest_cache/v/cache/stepwise DELETED
@@ -1 +0,0 @@
1
- []
 
 
tests/__pycache__/test_constraints.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (5.24 kB)
 
tests/__pycache__/test_demo_data.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (52.1 kB)
 
tests/__pycache__/test_feasible.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (4.4 kB)
 
tests/__pycache__/test_haversine.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (23.6 kB)
 
tests/__pycache__/test_routing.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (58.2 kB)
 
tests/__pycache__/test_timeline_fields.cpython-312-pytest-8.2.2.pyc DELETED
Binary file (21.7 kB)
 
tests/test_constraints.py DELETED
@@ -1,187 +0,0 @@
1
- from solverforge_legacy.solver.test import ConstraintVerifier
2
-
3
- from vehicle_routing.domain import Location, Vehicle, VehicleRoutePlan, Visit
4
- from vehicle_routing.constraints import (
5
- define_constraints,
6
- vehicle_capacity,
7
- service_finished_after_max_end_time,
8
- minimize_travel_time,
9
- )
10
-
11
- from datetime import datetime, timedelta
12
-
13
- # Driving times calculated using Haversine formula for realistic geographic distances.
14
- # These test coordinates at 50 km/h average speed yield:
15
- # LOCATION_1 to LOCATION_2: 40018 seconds (~11.1 hours, ~556 km)
16
- # LOCATION_2 to LOCATION_3: 40025 seconds (~11.1 hours, ~556 km)
17
- # LOCATION_1 to LOCATION_3: 11322 seconds (~3.1 hours, ~157 km)
18
-
19
- LOCATION_1 = Location(latitude=0, longitude=0)
20
- LOCATION_2 = Location(latitude=3, longitude=4)
21
- LOCATION_3 = Location(latitude=-1, longitude=1)
22
-
23
- DEPARTURE_TIME = datetime(2020, 1, 1)
24
- MIN_START_TIME = DEPARTURE_TIME + timedelta(hours=2)
25
- MAX_END_TIME = DEPARTURE_TIME + timedelta(hours=5)
26
- SERVICE_DURATION = timedelta(hours=1)
27
-
28
- constraint_verifier = ConstraintVerifier.build(
29
- define_constraints, VehicleRoutePlan, Vehicle, Visit
30
- )
31
-
32
-
33
- def test_vehicle_capacity_unpenalized():
34
- vehicleA = Vehicle(
35
- id="1", name="Alpha", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME
36
- )
37
- visit1 = Visit(
38
- id="2",
39
- name="John",
40
- location=LOCATION_2,
41
- demand=80,
42
- min_start_time=MIN_START_TIME,
43
- max_end_time=MAX_END_TIME,
44
- service_duration=SERVICE_DURATION,
45
- )
46
- connect(vehicleA, visit1)
47
-
48
- (
49
- constraint_verifier.verify_that(vehicle_capacity)
50
- .given(vehicleA, visit1)
51
- .penalizes_by(0)
52
- )
53
-
54
-
55
- def test_vehicle_capacity_penalized():
56
- vehicleA = Vehicle(
57
- id="1", name="Alpha", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME
58
- )
59
- visit1 = Visit(
60
- id="2",
61
- name="John",
62
- location=LOCATION_2,
63
- demand=80,
64
- min_start_time=MIN_START_TIME,
65
- max_end_time=MAX_END_TIME,
66
- service_duration=SERVICE_DURATION,
67
- )
68
- visit2 = Visit(
69
- id="3",
70
- name="Paul",
71
- location=LOCATION_3,
72
- demand=40,
73
- min_start_time=MIN_START_TIME,
74
- max_end_time=MAX_END_TIME,
75
- service_duration=SERVICE_DURATION,
76
- )
77
-
78
- connect(vehicleA, visit1, visit2)
79
-
80
- (
81
- constraint_verifier.verify_that(vehicle_capacity)
82
- .given(vehicleA, visit1, visit2)
83
- .penalizes_by(20)
84
- )
85
-
86
-
87
- def test_service_finished_after_max_end_time_unpenalized():
88
- vehicleA = Vehicle(
89
- id="1", name="Alpha", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME
90
- )
91
- visit1 = Visit(
92
- id="2",
93
- name="John",
94
- location=LOCATION_3,
95
- demand=80,
96
- min_start_time=MIN_START_TIME,
97
- max_end_time=MAX_END_TIME,
98
- service_duration=SERVICE_DURATION,
99
- )
100
-
101
- connect(vehicleA, visit1)
102
-
103
- (
104
- constraint_verifier.verify_that(service_finished_after_max_end_time)
105
- .given(vehicleA, visit1)
106
- .penalizes_by(0)
107
- )
108
-
109
-
110
- def test_service_finished_after_max_end_time_penalized():
111
- vehicleA = Vehicle(
112
- id="1", name="Alpha", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME
113
- )
114
- visit1 = Visit(
115
- id="2",
116
- name="John",
117
- location=LOCATION_2,
118
- demand=80,
119
- min_start_time=MIN_START_TIME,
120
- max_end_time=MAX_END_TIME,
121
- service_duration=SERVICE_DURATION,
122
- )
123
-
124
- connect(vehicleA, visit1)
125
-
126
- # With Haversine formula:
127
- # Travel time to LOCATION_2: 40018 seconds = 11.12 hours
128
- # Arrival time: 2020-01-01 11:06:58
129
- # Service duration: 1 hour
130
- # End service: 2020-01-01 12:06:58
131
- # Max end time: 2020-01-01 05:00:00
132
- # Delay: 7 hours 6 minutes 58 seconds = 426.97 minutes, rounded up = 427 minutes
133
- (
134
- constraint_verifier.verify_that(service_finished_after_max_end_time)
135
- .given(vehicleA, visit1)
136
- .penalizes_by(427)
137
- )
138
-
139
-
140
- def test_total_driving_time():
141
- vehicleA = Vehicle(
142
- id="1", name="Alpha", capacity=100, home_location=LOCATION_1, departure_time=DEPARTURE_TIME
143
- )
144
- visit1 = Visit(
145
- id="2",
146
- name="John",
147
- location=LOCATION_2,
148
- demand=80,
149
- min_start_time=MIN_START_TIME,
150
- max_end_time=MAX_END_TIME,
151
- service_duration=SERVICE_DURATION,
152
- )
153
- visit2 = Visit(
154
- id="3",
155
- name="Paul",
156
- location=LOCATION_3,
157
- demand=40,
158
- min_start_time=MIN_START_TIME,
159
- max_end_time=MAX_END_TIME,
160
- service_duration=SERVICE_DURATION,
161
- )
162
-
163
- connect(vehicleA, visit1, visit2)
164
-
165
- # With Haversine formula:
166
- # LOCATION_1 -> LOCATION_2: 40018 seconds
167
- # LOCATION_2 -> LOCATION_3: 40025 seconds
168
- # LOCATION_3 -> LOCATION_1: 11322 seconds
169
- # Total: 91365 seconds
170
- (
171
- constraint_verifier.verify_that(minimize_travel_time)
172
- .given(vehicleA, visit1, visit2)
173
- .penalizes_by(91365)
174
- )
175
-
176
-
177
- def connect(vehicle: Vehicle, *visits: Visit):
178
- vehicle.visits = list(visits)
179
- for i in range(len(visits)):
180
- visit = visits[i]
181
- visit.vehicle = vehicle
182
- if i > 0:
183
- visit.previous_visit = visits[i - 1]
184
-
185
- if i < len(visits) - 1:
186
- visit.next_visit = visits[i + 1]
187
- visit.update_arrival_time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_demo_data.py DELETED
@@ -1,280 +0,0 @@
1
- """
2
- Tests for demo data generation with customer-type based time windows.
3
-
4
- These tests verify that the demo data correctly generates realistic
5
- delivery scenarios with customer types driving time windows and demand.
6
- """
7
- import pytest
8
- from datetime import time
9
-
10
- from vehicle_routing.demo_data import (
11
- DemoData,
12
- generate_demo_data,
13
- CustomerType,
14
- random_customer_type,
15
- CUSTOMER_TYPE_WEIGHTS,
16
- )
17
- from random import Random
18
-
19
-
20
- class TestCustomerTypes:
21
- """Tests for customer type definitions and selection."""
22
-
23
- def test_customer_types_have_valid_time_windows(self):
24
- """Each customer type should have a valid time window."""
25
- for ctype in CustomerType:
26
- assert ctype.window_start < ctype.window_end, (
27
- f"{ctype.name} window_start should be before window_end"
28
- )
29
-
30
- def test_customer_types_have_valid_demand_ranges(self):
31
- """Each customer type should have valid demand ranges."""
32
- for ctype in CustomerType:
33
- assert ctype.min_demand >= 1, f"{ctype.name} min_demand should be >= 1"
34
- assert ctype.max_demand >= ctype.min_demand, (
35
- f"{ctype.name} max_demand should be >= min_demand"
36
- )
37
-
38
- def test_customer_types_have_valid_service_duration_ranges(self):
39
- """Each customer type should have valid service duration ranges."""
40
- for ctype in CustomerType:
41
- assert ctype.min_service_minutes >= 1, (
42
- f"{ctype.name} min_service_minutes should be >= 1"
43
- )
44
- assert ctype.max_service_minutes >= ctype.min_service_minutes, (
45
- f"{ctype.name} max_service_minutes should be >= min_service_minutes"
46
- )
47
-
48
- def test_residential_time_window(self):
49
- """Residential customers have evening windows."""
50
- res = CustomerType.RESIDENTIAL
51
- assert res.window_start == time(17, 0)
52
- assert res.window_end == time(20, 0)
53
-
54
- def test_business_time_window(self):
55
- """Business customers have standard business hours."""
56
- biz = CustomerType.BUSINESS
57
- assert biz.window_start == time(9, 0)
58
- assert biz.window_end == time(17, 0)
59
-
60
- def test_restaurant_time_window(self):
61
- """Restaurant customers have early morning windows."""
62
- rest = CustomerType.RESTAURANT
63
- assert rest.window_start == time(6, 0)
64
- assert rest.window_end == time(10, 0)
65
-
66
- def test_weighted_selection_distribution(self):
67
- """Weighted selection should roughly match configured weights."""
68
- random = Random(42)
69
- counts = {ctype: 0 for ctype in CustomerType}
70
-
71
- n_samples = 10000
72
- for _ in range(n_samples):
73
- ctype = random_customer_type(random)
74
- counts[ctype] += 1
75
-
76
- # Expected: 50% residential, 30% business, 20% restaurant
77
- total_weight = sum(w for _, w in CUSTOMER_TYPE_WEIGHTS)
78
- for ctype, weight in CUSTOMER_TYPE_WEIGHTS:
79
- expected_pct = weight / total_weight
80
- actual_pct = counts[ctype] / n_samples
81
- # Allow 5% tolerance
82
- assert abs(actual_pct - expected_pct) < 0.05, (
83
- f"{ctype.name}: expected {expected_pct:.2%}, got {actual_pct:.2%}"
84
- )
85
-
86
-
87
- class TestDemoDataGeneration:
88
- """Tests for the demo data generation."""
89
-
90
- @pytest.mark.parametrize("demo", list(DemoData))
91
- def test_generates_correct_number_of_vehicles(self, demo):
92
- """Should generate the configured number of vehicles."""
93
- plan = generate_demo_data(demo)
94
- assert len(plan.vehicles) == demo.value.vehicle_count
95
-
96
- @pytest.mark.parametrize("demo", list(DemoData))
97
- def test_generates_correct_number_of_visits(self, demo):
98
- """Should generate the configured number of visits."""
99
- plan = generate_demo_data(demo)
100
- assert len(plan.visits) == demo.value.visit_count
101
-
102
- @pytest.mark.parametrize("demo", list(DemoData))
103
- def test_visits_have_valid_time_windows(self, demo):
104
- """All visits should have time windows matching customer types."""
105
- plan = generate_demo_data(demo)
106
- valid_windows = {
107
- (ctype.window_start, ctype.window_end) for ctype in CustomerType
108
- }
109
-
110
- for visit in plan.visits:
111
- window = (visit.min_start_time.time(), visit.max_end_time.time())
112
- assert window in valid_windows, (
113
- f"Visit {visit.id} has invalid window {window}"
114
- )
115
-
116
- @pytest.mark.parametrize("demo", list(DemoData))
117
- def test_visits_have_varied_time_windows(self, demo):
118
- """Visits should have a mix of different time windows."""
119
- plan = generate_demo_data(demo)
120
-
121
- windows = {
122
- (v.min_start_time.time(), v.max_end_time.time())
123
- for v in plan.visits
124
- }
125
-
126
- # Should have at least 2 different window types (likely all 3)
127
- assert len(windows) >= 2, "Should have varied time windows"
128
-
129
- @pytest.mark.parametrize("demo", list(DemoData))
130
- def test_vehicles_depart_at_6am(self, demo):
131
- """Vehicles should depart at 06:00 to serve restaurant customers."""
132
- plan = generate_demo_data(demo)
133
-
134
- for vehicle in plan.vehicles:
135
- assert vehicle.departure_time.hour == 6
136
- assert vehicle.departure_time.minute == 0
137
-
138
- @pytest.mark.parametrize("demo", list(DemoData))
139
- def test_visits_within_geographic_bounds(self, demo):
140
- """All visits should be within the specified geographic bounds."""
141
- plan = generate_demo_data(demo)
142
- sw = plan.south_west_corner
143
- ne = plan.north_east_corner
144
-
145
- for visit in plan.visits:
146
- assert sw.latitude <= visit.location.latitude <= ne.latitude, (
147
- f"Visit {visit.id} latitude {visit.location.latitude} "
148
- f"outside bounds [{sw.latitude}, {ne.latitude}]"
149
- )
150
- assert sw.longitude <= visit.location.longitude <= ne.longitude, (
151
- f"Visit {visit.id} longitude {visit.location.longitude} "
152
- f"outside bounds [{sw.longitude}, {ne.longitude}]"
153
- )
154
-
155
- @pytest.mark.parametrize("demo", list(DemoData))
156
- def test_vehicles_within_geographic_bounds(self, demo):
157
- """All vehicle home locations should be within geographic bounds."""
158
- plan = generate_demo_data(demo)
159
- sw = plan.south_west_corner
160
- ne = plan.north_east_corner
161
-
162
- for vehicle in plan.vehicles:
163
- loc = vehicle.home_location
164
- assert sw.latitude <= loc.latitude <= ne.latitude
165
- assert sw.longitude <= loc.longitude <= ne.longitude
166
-
167
- @pytest.mark.parametrize("demo", list(DemoData))
168
- def test_service_durations_match_customer_types(self, demo):
169
- """Service durations should match their customer type's service duration range."""
170
- plan = generate_demo_data(demo)
171
-
172
- # Map time windows back to customer types
173
- window_to_type = {
174
- (ctype.window_start, ctype.window_end): ctype
175
- for ctype in CustomerType
176
- }
177
-
178
- for visit in plan.visits:
179
- window = (visit.min_start_time.time(), visit.max_end_time.time())
180
- ctype = window_to_type[window]
181
- duration_minutes = int(visit.service_duration.total_seconds() / 60)
182
- assert ctype.min_service_minutes <= duration_minutes <= ctype.max_service_minutes, (
183
- f"Visit {visit.id} ({ctype.name}) service duration {duration_minutes}min "
184
- f"outside [{ctype.min_service_minutes}, {ctype.max_service_minutes}]"
185
- )
186
-
187
- @pytest.mark.parametrize("demo", list(DemoData))
188
- def test_demands_match_customer_types(self, demo):
189
- """Visit demands should match their customer type's demand range."""
190
- plan = generate_demo_data(demo)
191
-
192
- # Map time windows back to customer types
193
- window_to_type = {
194
- (ctype.window_start, ctype.window_end): ctype
195
- for ctype in CustomerType
196
- }
197
-
198
- for visit in plan.visits:
199
- window = (visit.min_start_time.time(), visit.max_end_time.time())
200
- ctype = window_to_type[window]
201
- assert ctype.min_demand <= visit.demand <= ctype.max_demand, (
202
- f"Visit {visit.id} ({ctype.name}) demand {visit.demand} "
203
- f"outside [{ctype.min_demand}, {ctype.max_demand}]"
204
- )
205
-
206
- @pytest.mark.parametrize("demo", list(DemoData))
207
- def test_vehicle_capacities_within_bounds(self, demo):
208
- """Vehicle capacities should be within configured bounds."""
209
- plan = generate_demo_data(demo)
210
- props = demo.value
211
-
212
- for vehicle in plan.vehicles:
213
- assert props.min_vehicle_capacity <= vehicle.capacity <= props.max_vehicle_capacity, (
214
- f"Vehicle {vehicle.id} capacity {vehicle.capacity} "
215
- f"outside [{props.min_vehicle_capacity}, {props.max_vehicle_capacity}]"
216
- )
217
-
218
- @pytest.mark.parametrize("demo", list(DemoData))
219
- def test_deterministic_with_same_seed(self, demo):
220
- """Same demo data should produce identical results (deterministic)."""
221
- plan1 = generate_demo_data(demo)
222
- plan2 = generate_demo_data(demo)
223
-
224
- assert len(plan1.visits) == len(plan2.visits)
225
- assert len(plan1.vehicles) == len(plan2.vehicles)
226
-
227
- for v1, v2 in zip(plan1.visits, plan2.visits):
228
- assert v1.location.latitude == v2.location.latitude
229
- assert v1.location.longitude == v2.location.longitude
230
- assert v1.demand == v2.demand
231
- assert v1.service_duration == v2.service_duration
232
- assert v1.min_start_time == v2.min_start_time
233
- assert v1.max_end_time == v2.max_end_time
234
-
235
-
236
- class TestHaversineIntegration:
237
- """Tests verifying Haversine distance is used correctly in demo data."""
238
-
239
- def test_philadelphia_diagonal_realistic(self):
240
- """Philadelphia area diagonal should be ~15km with Haversine (tightened bbox)."""
241
- props = DemoData.PHILADELPHIA.value
242
- diagonal_seconds = props.south_west_corner.driving_time_to(
243
- props.north_east_corner
244
- )
245
- diagonal_km = (diagonal_seconds / 3600) * 50 # 50 km/h average
246
-
247
- # Philadelphia bbox is tightened to Center City area (~8km x 12km)
248
- # Diagonal should be around 10-20km
249
- assert 8 < diagonal_km < 25, f"Diagonal {diagonal_km}km seems wrong"
250
-
251
- def test_firenze_diagonal_realistic(self):
252
- """Firenze area diagonal should be ~10km with Haversine."""
253
- props = DemoData.FIRENZE.value
254
- diagonal_seconds = props.south_west_corner.driving_time_to(
255
- props.north_east_corner
256
- )
257
- diagonal_km = (diagonal_seconds / 3600) * 50 # 50 km/h average
258
-
259
- # Firenze area is small, roughly 6km x 12km
260
- assert 5 < diagonal_km < 20, f"Diagonal {diagonal_km}km seems wrong"
261
-
262
- def test_inter_visit_distances_use_haversine(self):
263
- """Distances between visits should use Haversine formula."""
264
- plan = generate_demo_data(DemoData.PHILADELPHIA)
265
-
266
- # Pick two visits
267
- v1, v2 = plan.visits[0], plan.visits[1]
268
-
269
- # Calculate distance using the Location method
270
- haversine_time = v1.location.driving_time_to(v2.location)
271
-
272
- # Verify it's not using simple Euclidean (which would be ~4000 * coord_diff)
273
- simple_euclidean = round(
274
- ((v1.location.latitude - v2.location.latitude) ** 2 +
275
- (v1.location.longitude - v2.location.longitude) ** 2) ** 0.5 * 4000
276
- )
277
-
278
- # Haversine should give different (usually larger) results
279
- # for geographic coordinates
280
- assert haversine_time != simple_euclidean or haversine_time == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_feasible.py DELETED
@@ -1,54 +0,0 @@
1
- """
2
- Integration test for vehicle routing solver feasibility.
3
-
4
- Tests that the solver can find a feasible solution using the Haversine
5
- driving time calculator for realistic geographic distances.
6
- """
7
- from vehicle_routing.rest_api import json_to_vehicle_route_plan, app
8
-
9
- from fastapi.testclient import TestClient
10
- from time import sleep
11
- from pytest import fail
12
- import pytest
13
-
14
- client = TestClient(app)
15
-
16
-
17
- @pytest.mark.timeout(180) # Allow 3 minutes for this integration test
18
- def test_feasible():
19
- """
20
- Test that the solver can find a feasible solution for FIRENZE demo data.
21
-
22
- FIRENZE is a small geographic area (~10km diagonal) where all customer
23
- time windows can be satisfied. Larger areas like PHILADELPHIA may be
24
- intentionally challenging with realistic time windows.
25
-
26
- Customer types:
27
- - Restaurant (20%): 06:00-10:00 window, high demand (5-10)
28
- - Business (30%): 09:00-17:00 window, medium demand (3-6)
29
- - Residential (50%): 17:00-20:00 window, low demand (1-2)
30
- """
31
- demo_data_response = client.get("/demo-data/FIRENZE")
32
- assert demo_data_response.status_code == 200
33
-
34
- job_id_response = client.post("/route-plans", json=demo_data_response.json())
35
- assert job_id_response.status_code == 200
36
- job_id = job_id_response.text[1:-1]
37
-
38
- # Allow up to 120 seconds for the solver to find a feasible solution
39
- ATTEMPTS = 1200 # 120 seconds at 0.1s intervals
40
- best_score = None
41
- for i in range(ATTEMPTS):
42
- sleep(0.1)
43
- route_plan_response = client.get(f"/route-plans/{job_id}")
44
- route_plan_json = route_plan_response.json()
45
- timetable = json_to_vehicle_route_plan(route_plan_json)
46
- if timetable.score is not None:
47
- best_score = timetable.score
48
- if timetable.score.is_feasible:
49
- stop_solving_response = client.delete(f"/route-plans/{job_id}")
50
- assert stop_solving_response.status_code == 200
51
- return
52
-
53
- client.delete(f"/route-plans/{job_id}")
54
- pytest.skip(f'Solution is not feasible after 120 seconds. Best score: {best_score}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_haversine.py DELETED
@@ -1,156 +0,0 @@
1
- """
2
- Unit tests for the Haversine driving time calculator in Location class.
3
-
4
- These tests verify that the driving time calculations correctly implement
5
- the Haversine formula for great-circle distance on Earth.
6
- """
7
- from vehicle_routing.domain import Location
8
-
9
-
10
- class TestHaversineDrivingTime:
11
- """Tests for Location.driving_time_to() using Haversine formula."""
12
-
13
- def test_same_location_returns_zero(self):
14
- """Same location should return 0 driving time."""
15
- loc = Location(latitude=40.0, longitude=-75.0)
16
- assert loc.driving_time_to(loc) == 0
17
-
18
- def test_same_coordinates_returns_zero(self):
19
- """Two locations with same coordinates should return 0."""
20
- loc1 = Location(latitude=40.0, longitude=-75.0)
21
- loc2 = Location(latitude=40.0, longitude=-75.0)
22
- assert loc1.driving_time_to(loc2) == 0
23
-
24
- def test_symmetric_distance(self):
25
- """Distance from A to B should equal distance from B to A."""
26
- loc1 = Location(latitude=0, longitude=0)
27
- loc2 = Location(latitude=3, longitude=4)
28
- assert loc1.driving_time_to(loc2) == loc2.driving_time_to(loc1)
29
-
30
- def test_equator_one_degree_longitude(self):
31
- """
32
- One degree of longitude at the equator is approximately 111.32 km.
33
- At 50 km/h, this should take about 2.2 hours = 7920 seconds.
34
- """
35
- loc1 = Location(latitude=0, longitude=0)
36
- loc2 = Location(latitude=0, longitude=1)
37
- driving_time = loc1.driving_time_to(loc2)
38
- # Allow 5% tolerance for rounding
39
- assert 7500 < driving_time < 8500, f"Expected ~8000, got {driving_time}"
40
-
41
- def test_equator_one_degree_latitude(self):
42
- """
43
- One degree of latitude is approximately 111.32 km everywhere.
44
- At 50 km/h, this should take about 2.2 hours = 7920 seconds.
45
- """
46
- loc1 = Location(latitude=0, longitude=0)
47
- loc2 = Location(latitude=1, longitude=0)
48
- driving_time = loc1.driving_time_to(loc2)
49
- # Allow 5% tolerance for rounding
50
- assert 7500 < driving_time < 8500, f"Expected ~8000, got {driving_time}"
51
-
52
- def test_realistic_us_cities(self):
53
- """
54
- Test driving time between realistic US city coordinates.
55
- Philadelphia (39.95, -75.17) to New York (40.71, -74.01)
56
- Distance is approximately 130 km, should take ~2.6 hours at 50 km/h.
57
- """
58
- philadelphia = Location(latitude=39.95, longitude=-75.17)
59
- new_york = Location(latitude=40.71, longitude=-74.01)
60
- driving_time = philadelphia.driving_time_to(new_york)
61
- # Expected: ~130 km / 50 km/h * 3600 = ~9360 seconds
62
- # Allow reasonable tolerance
63
- assert 8500 < driving_time < 10500, f"Expected ~9400, got {driving_time}"
64
-
65
- def test_longer_distance(self):
66
- """
67
- Test longer distance: Philadelphia to Hartford.
68
- Distance is approximately 290 km.
69
- """
70
- philadelphia = Location(latitude=39.95, longitude=-75.17)
71
- hartford = Location(latitude=41.76, longitude=-72.68)
72
- driving_time = philadelphia.driving_time_to(hartford)
73
- # Expected: ~290 km / 50 km/h * 3600 = ~20880 seconds
74
- # Allow reasonable tolerance
75
- assert 19000 < driving_time < 23000, f"Expected ~21000, got {driving_time}"
76
-
77
- def test_known_values_from_test_data(self):
78
- """
79
- Verify the exact values used in constraint tests.
80
- These values are calculated using the Haversine formula.
81
- """
82
- LOCATION_1 = Location(latitude=0, longitude=0)
83
- LOCATION_2 = Location(latitude=3, longitude=4)
84
- LOCATION_3 = Location(latitude=-1, longitude=1)
85
-
86
- # These exact values are used in test_constraints.py
87
- assert LOCATION_1.driving_time_to(LOCATION_2) == 40018
88
- assert LOCATION_2.driving_time_to(LOCATION_3) == 40025
89
- assert LOCATION_1.driving_time_to(LOCATION_3) == 11322
90
-
91
- def test_negative_coordinates(self):
92
- """Test with negative latitude and longitude (Southern/Western hemisphere)."""
93
- loc1 = Location(latitude=-33.87, longitude=151.21) # Sydney
94
- loc2 = Location(latitude=-37.81, longitude=144.96) # Melbourne
95
- driving_time = loc1.driving_time_to(loc2)
96
- # Distance is approximately 714 km
97
- # Expected: ~714 km / 50 km/h * 3600 = ~51408 seconds
98
- assert 48000 < driving_time < 55000, f"Expected ~51400, got {driving_time}"
99
-
100
- def test_cross_hemisphere(self):
101
- """Test crossing equator."""
102
- loc1 = Location(latitude=10, longitude=0)
103
- loc2 = Location(latitude=-10, longitude=0)
104
- driving_time = loc1.driving_time_to(loc2)
105
- # 20 degrees of latitude = ~2226 km
106
- # Expected: ~2226 km / 50 km/h * 3600 = ~160272 seconds
107
- assert 155000 < driving_time < 165000, f"Expected ~160000, got {driving_time}"
108
-
109
- def test_cross_antimeridian(self):
110
- """Test crossing the antimeridian (date line)."""
111
- loc1 = Location(latitude=0, longitude=179)
112
- loc2 = Location(latitude=0, longitude=-179)
113
- driving_time = loc1.driving_time_to(loc2)
114
- # 2 degrees at equator = ~222 km
115
- # Expected: ~222 km / 50 km/h * 3600 = ~15984 seconds
116
- assert 15000 < driving_time < 17000, f"Expected ~16000, got {driving_time}"
117
-
118
-
119
- class TestHaversineInternalMethods:
120
- """Tests for internal Haversine calculation methods."""
121
-
122
- def test_to_cartesian_equator_prime_meridian(self):
123
- """Test Cartesian conversion at equator/prime meridian intersection."""
124
- loc = Location(latitude=0, longitude=0)
125
- x, y, z = loc._to_cartesian()
126
- # At (0, 0): x=0, y=0.5, z=0
127
- assert abs(x - 0) < 0.001
128
- assert abs(y - 0.5) < 0.001
129
- assert abs(z - 0) < 0.001
130
-
131
- def test_to_cartesian_north_pole(self):
132
- """Test Cartesian conversion at North Pole."""
133
- loc = Location(latitude=90, longitude=0)
134
- x, y, z = loc._to_cartesian()
135
- # At North Pole: x=0, y=0, z=0.5
136
- assert abs(x - 0) < 0.001
137
- assert abs(y - 0) < 0.001
138
- assert abs(z - 0.5) < 0.001
139
-
140
- def test_meters_to_driving_seconds(self):
141
- """Test conversion from meters to driving seconds."""
142
- # 50 km = 50000 m should take 1 hour = 3600 seconds at 50 km/h
143
- seconds = Location._meters_to_driving_seconds(50000)
144
- assert seconds == 3600
145
-
146
- def test_meters_to_driving_seconds_zero(self):
147
- """Zero meters should return zero seconds."""
148
- assert Location._meters_to_driving_seconds(0) == 0
149
-
150
- def test_meters_to_driving_seconds_small(self):
151
- """Test small distances."""
152
- # 1 km = 1000 m should take 72 seconds at 50 km/h
153
- seconds = Location._meters_to_driving_seconds(1000)
154
- assert seconds == 72
155
-
156
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_routing.py DELETED
@@ -1,431 +0,0 @@
1
- """
2
- Unit tests for the routing module.
3
-
4
- Tests cover:
5
- - RouteResult dataclass
6
- - DistanceMatrix operations
7
- - Haversine fallback calculations
8
- - Polyline encoding/decoding roundtrip
9
- - Location class integration with distance matrix
10
- """
11
- import pytest
12
- import polyline
13
-
14
- from vehicle_routing.domain import Location
15
- from vehicle_routing.routing import (
16
- RouteResult,
17
- DistanceMatrix,
18
- _haversine_driving_time,
19
- _haversine_distance_meters,
20
- _straight_line_geometry,
21
- compute_distance_matrix_with_progress,
22
- )
23
-
24
-
25
- class TestRouteResult:
26
- """Tests for the RouteResult dataclass."""
27
-
28
- def test_create_route_result(self):
29
- """Test creating a basic RouteResult."""
30
- result = RouteResult(
31
- duration_seconds=3600,
32
- distance_meters=50000,
33
- geometry="encodedPolyline"
34
- )
35
- assert result.duration_seconds == 3600
36
- assert result.distance_meters == 50000
37
- assert result.geometry == "encodedPolyline"
38
-
39
- def test_route_result_optional_geometry(self):
40
- """Test RouteResult with no geometry."""
41
- result = RouteResult(duration_seconds=100, distance_meters=1000)
42
- assert result.geometry is None
43
-
44
-
45
- class TestDistanceMatrix:
46
- """Tests for the DistanceMatrix class."""
47
-
48
- def test_empty_matrix(self):
49
- """Test empty distance matrix returns None."""
50
- matrix = DistanceMatrix()
51
- loc1 = Location(latitude=40.0, longitude=-75.0)
52
- loc2 = Location(latitude=41.0, longitude=-74.0)
53
- assert matrix.get_route(loc1, loc2) is None
54
-
55
- def test_set_and_get_route(self):
56
- """Test setting and retrieving a route."""
57
- matrix = DistanceMatrix()
58
- loc1 = Location(latitude=40.0, longitude=-75.0)
59
- loc2 = Location(latitude=41.0, longitude=-74.0)
60
-
61
- result = RouteResult(
62
- duration_seconds=3600,
63
- distance_meters=100000,
64
- geometry="test_geometry"
65
- )
66
- matrix.set_route(loc1, loc2, result)
67
-
68
- retrieved = matrix.get_route(loc1, loc2)
69
- assert retrieved is not None
70
- assert retrieved.duration_seconds == 3600
71
- assert retrieved.distance_meters == 100000
72
- assert retrieved.geometry == "test_geometry"
73
-
74
- def test_get_route_different_direction(self):
75
- """Test that routes are directional (A->B != B->A by default)."""
76
- matrix = DistanceMatrix()
77
- loc1 = Location(latitude=40.0, longitude=-75.0)
78
- loc2 = Location(latitude=41.0, longitude=-74.0)
79
-
80
- result = RouteResult(duration_seconds=3600, distance_meters=100000)
81
- matrix.set_route(loc1, loc2, result)
82
-
83
- # Should find loc1 -> loc2
84
- assert matrix.get_route(loc1, loc2) is not None
85
- # Should NOT find loc2 -> loc1 (wasn't set)
86
- assert matrix.get_route(loc2, loc1) is None
87
-
88
- def test_get_driving_time_from_matrix(self):
89
- """Test getting driving time from matrix."""
90
- matrix = DistanceMatrix()
91
- loc1 = Location(latitude=40.0, longitude=-75.0)
92
- loc2 = Location(latitude=41.0, longitude=-74.0)
93
-
94
- result = RouteResult(duration_seconds=3600, distance_meters=100000)
95
- matrix.set_route(loc1, loc2, result)
96
-
97
- assert matrix.get_driving_time(loc1, loc2) == 3600
98
-
99
- def test_get_driving_time_falls_back_to_haversine(self):
100
- """Test that missing routes fall back to haversine."""
101
- matrix = DistanceMatrix()
102
- loc1 = Location(latitude=40.0, longitude=-75.0)
103
- loc2 = Location(latitude=41.0, longitude=-74.0)
104
-
105
- # Don't set any route - should use haversine fallback
106
- time = matrix.get_driving_time(loc1, loc2)
107
- assert time > 0 # Should return some positive value from haversine
108
-
109
- def test_get_geometry(self):
110
- """Test getting geometry from matrix."""
111
- matrix = DistanceMatrix()
112
- loc1 = Location(latitude=40.0, longitude=-75.0)
113
- loc2 = Location(latitude=41.0, longitude=-74.0)
114
-
115
- result = RouteResult(
116
- duration_seconds=3600,
117
- distance_meters=100000,
118
- geometry="test_encoded_polyline"
119
- )
120
- matrix.set_route(loc1, loc2, result)
121
-
122
- assert matrix.get_geometry(loc1, loc2) == "test_encoded_polyline"
123
-
124
- def test_get_geometry_missing_returns_none(self):
125
- """Test that missing routes return None for geometry."""
126
- matrix = DistanceMatrix()
127
- loc1 = Location(latitude=40.0, longitude=-75.0)
128
- loc2 = Location(latitude=41.0, longitude=-74.0)
129
-
130
- assert matrix.get_geometry(loc1, loc2) is None
131
-
132
-
133
- class TestHaversineFunctions:
134
- """Tests for standalone haversine functions."""
135
-
136
- def test_haversine_driving_time_same_location(self):
137
- """Same location should return 0 driving time."""
138
- loc = Location(latitude=40.0, longitude=-75.0)
139
- assert _haversine_driving_time(loc, loc) == 0
140
-
141
- def test_haversine_driving_time_realistic(self):
142
- """Test haversine driving time with realistic coordinates."""
143
- philadelphia = Location(latitude=39.95, longitude=-75.17)
144
- new_york = Location(latitude=40.71, longitude=-74.01)
145
- time = _haversine_driving_time(philadelphia, new_york)
146
- # ~130 km at 50 km/h = ~9400 seconds
147
- assert 8500 < time < 10500
148
-
149
- def test_haversine_distance_meters_same_location(self):
150
- """Same location should return 0 distance."""
151
- loc = Location(latitude=40.0, longitude=-75.0)
152
- assert _haversine_distance_meters(loc, loc) == 0
153
-
154
- def test_haversine_distance_meters_one_degree(self):
155
- """Test one degree of latitude is approximately 111 km."""
156
- loc1 = Location(latitude=0, longitude=0)
157
- loc2 = Location(latitude=1, longitude=0)
158
- distance = _haversine_distance_meters(loc1, loc2)
159
- # 1 degree latitude = ~111.32 km
160
- assert 110000 < distance < 113000
161
-
162
- def test_straight_line_geometry(self):
163
- """Test straight line geometry encoding."""
164
- loc1 = Location(latitude=40.0, longitude=-75.0)
165
- loc2 = Location(latitude=41.0, longitude=-74.0)
166
- encoded = _straight_line_geometry(loc1, loc2)
167
-
168
- # Decode and verify
169
- points = polyline.decode(encoded)
170
- assert len(points) == 2
171
- assert abs(points[0][0] - 40.0) < 0.0001
172
- assert abs(points[0][1] - (-75.0)) < 0.0001
173
- assert abs(points[1][0] - 41.0) < 0.0001
174
- assert abs(points[1][1] - (-74.0)) < 0.0001
175
-
176
-
177
- class TestPolylineRoundtrip:
178
- """Tests for polyline encoding/decoding."""
179
-
180
- def test_encode_decode_roundtrip(self):
181
- """Test that encoding and decoding preserves coordinates."""
182
- coordinates = [(39.9526, -75.1652), (39.9535, -75.1589)]
183
- encoded = polyline.encode(coordinates, precision=5)
184
- decoded = polyline.decode(encoded, precision=5)
185
-
186
- assert len(decoded) == 2
187
- for orig, dec in zip(coordinates, decoded):
188
- assert abs(orig[0] - dec[0]) < 0.00001
189
- assert abs(orig[1] - dec[1]) < 0.00001
190
-
191
- def test_encode_single_point(self):
192
- """Test encoding a single point."""
193
- coordinates = [(40.0, -75.0)]
194
- encoded = polyline.encode(coordinates, precision=5)
195
- decoded = polyline.decode(encoded, precision=5)
196
-
197
- assert len(decoded) == 1
198
- assert abs(decoded[0][0] - 40.0) < 0.00001
199
- assert abs(decoded[0][1] - (-75.0)) < 0.00001
200
-
201
- def test_encode_many_points(self):
202
- """Test encoding many points (like a real route)."""
203
- coordinates = [
204
- (39.9526, -75.1652),
205
- (39.9535, -75.1589),
206
- (39.9543, -75.1690),
207
- (39.9520, -75.1685),
208
- (39.9505, -75.1660),
209
- ]
210
- encoded = polyline.encode(coordinates, precision=5)
211
- decoded = polyline.decode(encoded, precision=5)
212
-
213
- assert len(decoded) == len(coordinates)
214
- for orig, dec in zip(coordinates, decoded):
215
- assert abs(orig[0] - dec[0]) < 0.00001
216
- assert abs(orig[1] - dec[1]) < 0.00001
217
-
218
-
219
- class TestLocationDistanceMatrixIntegration:
220
- """Tests for Location class integration with DistanceMatrix."""
221
-
222
- def setup_method(self):
223
- """Clear any existing distance matrix before each test."""
224
- Location.clear_distance_matrix()
225
-
226
- def teardown_method(self):
227
- """Clear distance matrix after each test."""
228
- Location.clear_distance_matrix()
229
-
230
- def test_location_uses_haversine_without_matrix(self):
231
- """Without matrix, Location should use haversine."""
232
- loc1 = Location(latitude=40.0, longitude=-75.0)
233
- loc2 = Location(latitude=41.0, longitude=-74.0)
234
-
235
- # Should use haversine (no matrix set)
236
- time = loc1.driving_time_to(loc2)
237
- assert time > 0
238
-
239
- def test_location_uses_matrix_when_set(self):
240
- """With matrix set, Location should use matrix values."""
241
- matrix = DistanceMatrix()
242
- loc1 = Location(latitude=40.0, longitude=-75.0)
243
- loc2 = Location(latitude=41.0, longitude=-74.0)
244
-
245
- # Set a specific value in matrix
246
- result = RouteResult(duration_seconds=12345, distance_meters=100000)
247
- matrix.set_route(loc1, loc2, result)
248
-
249
- # Set the matrix on Location class
250
- Location.set_distance_matrix(matrix)
251
-
252
- # Should return the matrix value, not haversine
253
- time = loc1.driving_time_to(loc2)
254
- assert time == 12345
255
-
256
- def test_location_falls_back_when_route_not_in_matrix(self):
257
- """If route not in matrix, Location should fall back to haversine."""
258
- matrix = DistanceMatrix()
259
- loc1 = Location(latitude=40.0, longitude=-75.0)
260
- loc2 = Location(latitude=41.0, longitude=-74.0)
261
- loc3 = Location(latitude=42.0, longitude=-73.0)
262
-
263
- # Only set loc1 -> loc2
264
- result = RouteResult(duration_seconds=12345, distance_meters=100000)
265
- matrix.set_route(loc1, loc2, result)
266
-
267
- Location.set_distance_matrix(matrix)
268
-
269
- # loc1 -> loc2 should use matrix
270
- assert loc1.driving_time_to(loc2) == 12345
271
-
272
- # loc1 -> loc3 should fall back to haversine (not in matrix)
273
- time = loc1.driving_time_to(loc3)
274
- assert time != 12345 # Should be haversine calculated value
275
- assert time > 0
276
-
277
- def test_get_distance_matrix(self):
278
- """Test getting the current distance matrix."""
279
- assert Location.get_distance_matrix() is None
280
-
281
- matrix = DistanceMatrix()
282
- Location.set_distance_matrix(matrix)
283
- assert Location.get_distance_matrix() is matrix
284
-
285
- def test_clear_distance_matrix(self):
286
- """Test clearing the distance matrix."""
287
- matrix = DistanceMatrix()
288
- Location.set_distance_matrix(matrix)
289
- assert Location.get_distance_matrix() is not None
290
-
291
- Location.clear_distance_matrix()
292
- assert Location.get_distance_matrix() is None
293
-
294
-
295
- class TestDistanceMatrixSameLocation:
296
- """Tests for handling same-location routes."""
297
-
298
- def test_same_location_zero_time(self):
299
- """Same location should have zero driving time."""
300
- loc = Location(latitude=40.0, longitude=-75.0)
301
-
302
- matrix = DistanceMatrix()
303
- result = RouteResult(
304
- duration_seconds=0,
305
- distance_meters=0,
306
- geometry=polyline.encode([(40.0, -75.0)], precision=5)
307
- )
308
- matrix.set_route(loc, loc, result)
309
-
310
- assert matrix.get_driving_time(loc, loc) == 0
311
-
312
-
313
- class TestComputeDistanceMatrixWithProgress:
314
- """Tests for the compute_distance_matrix_with_progress function."""
315
-
316
- def test_empty_locations_returns_empty_matrix(self):
317
- """Empty location list should return empty matrix."""
318
- matrix = compute_distance_matrix_with_progress([], use_osm=False)
319
- assert matrix is not None
320
- # Empty matrix - no routes to check
321
-
322
- def test_haversine_mode_computes_all_pairs(self):
323
- """Haversine mode should compute all location pairs."""
324
- locations = [
325
- Location(latitude=40.0, longitude=-75.0),
326
- Location(latitude=41.0, longitude=-74.0),
327
- Location(latitude=42.0, longitude=-73.0),
328
- ]
329
- matrix = compute_distance_matrix_with_progress(
330
- locations, use_osm=False
331
- )
332
-
333
- # Should have all 9 pairs (3x3)
334
- for origin in locations:
335
- for dest in locations:
336
- result = matrix.get_route(origin, dest)
337
- assert result is not None
338
- if origin is dest:
339
- assert result.duration_seconds == 0
340
- assert result.distance_meters == 0
341
- else:
342
- assert result.duration_seconds > 0
343
- assert result.distance_meters > 0
344
- assert result.geometry is not None
345
-
346
- def test_progress_callback_is_called(self):
347
- """Progress callback should be called during computation."""
348
- locations = [
349
- Location(latitude=40.0, longitude=-75.0),
350
- Location(latitude=41.0, longitude=-74.0),
351
- ]
352
-
353
- progress_calls = []
354
-
355
- def callback(phase, message, percent, detail=""):
356
- progress_calls.append({
357
- "phase": phase,
358
- "message": message,
359
- "percent": percent,
360
- "detail": detail
361
- })
362
-
363
- compute_distance_matrix_with_progress(
364
- locations, use_osm=False, progress_callback=callback
365
- )
366
-
367
- # Should have received progress callbacks
368
- assert len(progress_calls) > 0
369
-
370
- # Should have a "complete" phase at the end
371
- assert any(p["phase"] == "complete" for p in progress_calls)
372
-
373
- # All percentages should be between 0 and 100
374
- for call in progress_calls:
375
- assert 0 <= call["percent"] <= 100
376
-
377
- def test_haversine_mode_skips_network_phase(self):
378
- """In haversine mode, should not have network download messages."""
379
- locations = [
380
- Location(latitude=40.0, longitude=-75.0),
381
- Location(latitude=41.0, longitude=-74.0),
382
- ]
383
-
384
- progress_calls = []
385
-
386
- def callback(phase, message, percent, detail=""):
387
- progress_calls.append({
388
- "phase": phase,
389
- "message": message
390
- })
391
-
392
- compute_distance_matrix_with_progress(
393
- locations, use_osm=False, progress_callback=callback
394
- )
395
-
396
- # Should have a "network" phase but with haversine message
397
- network_messages = [p for p in progress_calls if p["phase"] == "network"]
398
- assert len(network_messages) > 0
399
- assert "haversine" in network_messages[0]["message"].lower()
400
-
401
- def test_bbox_is_used_when_provided(self):
402
- """Provided bounding box should be used."""
403
- locations = [
404
- Location(latitude=40.0, longitude=-75.0),
405
- Location(latitude=41.0, longitude=-74.0),
406
- ]
407
-
408
- bbox = (42.0, 39.0, -73.0, -76.0) # north, south, east, west
409
-
410
- # Should complete without error with provided bbox
411
- matrix = compute_distance_matrix_with_progress(
412
- locations, bbox=bbox, use_osm=False
413
- )
414
- assert matrix is not None
415
-
416
- def test_geometries_are_straight_lines_in_haversine_mode(self):
417
- """In haversine mode, geometries should be straight lines."""
418
- loc1 = Location(latitude=40.0, longitude=-75.0)
419
- loc2 = Location(latitude=41.0, longitude=-74.0)
420
-
421
- matrix = compute_distance_matrix_with_progress(
422
- [loc1, loc2], use_osm=False
423
- )
424
-
425
- result = matrix.get_route(loc1, loc2)
426
- assert result is not None
427
- assert result.geometry is not None
428
-
429
- # Decode and verify it's a straight line (2 points)
430
- points = polyline.decode(result.geometry)
431
- assert len(points) == 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_timeline_fields.py DELETED
@@ -1,215 +0,0 @@
1
- """
2
- Tests for timeline visualization fields in API serialization.
3
-
4
- These tests verify that all fields required by the frontend timeline
5
- visualizations (By vehicle, By visit tabs) are correctly serialized.
6
- """
7
- from datetime import datetime, timedelta
8
- from vehicle_routing.domain import (
9
- Location,
10
- Visit,
11
- Vehicle,
12
- VehicleRoutePlan,
13
- )
14
- from vehicle_routing.converters import (
15
- visit_to_model,
16
- vehicle_to_model,
17
- plan_to_model,
18
- )
19
-
20
-
21
- def create_test_location(lat: float = 43.77, lng: float = 11.25) -> Location:
22
- """Create a test location."""
23
- return Location(latitude=lat, longitude=lng)
24
-
25
-
26
- def create_test_vehicle(
27
- departure_time: datetime = None,
28
- visits: list = None,
29
- ) -> Vehicle:
30
- """Create a test vehicle with optional visits."""
31
- if departure_time is None:
32
- departure_time = datetime(2024, 1, 1, 6, 0, 0)
33
- return Vehicle(
34
- id="1",
35
- name="Alpha",
36
- capacity=25,
37
- home_location=create_test_location(),
38
- departure_time=departure_time,
39
- visits=visits or [],
40
- )
41
-
42
-
43
- def create_test_visit(
44
- vehicle: Vehicle = None,
45
- previous_visit: "Visit" = None,
46
- arrival_time: datetime = None,
47
- ) -> Visit:
48
- """Create a test visit."""
49
- visit = Visit(
50
- id="101",
51
- name="Test Customer",
52
- location=create_test_location(43.78, 11.26),
53
- demand=5,
54
- min_start_time=datetime(2024, 1, 1, 9, 0, 0),
55
- max_end_time=datetime(2024, 1, 1, 17, 0, 0),
56
- service_duration=timedelta(minutes=15),
57
- vehicle=vehicle,
58
- previous_visit=previous_visit,
59
- arrival_time=arrival_time,
60
- )
61
- return visit
62
-
63
-
64
- def create_test_plan(vehicles: list = None, visits: list = None) -> VehicleRoutePlan:
65
- """Create a test route plan."""
66
- if vehicles is None:
67
- vehicles = [create_test_vehicle()]
68
- if visits is None:
69
- visits = []
70
- return VehicleRoutePlan(
71
- name="Test Plan",
72
- south_west_corner=create_test_location(43.75, 11.20),
73
- north_east_corner=create_test_location(43.80, 11.30),
74
- vehicles=vehicles,
75
- visits=visits,
76
- )
77
-
78
-
79
- class TestVisitTimelineFields:
80
- """Tests for visit timeline serialization fields."""
81
-
82
- def test_unassigned_visit_has_null_timeline_fields(self):
83
- """Unassigned visits should have null timeline fields."""
84
- visit = create_test_visit(vehicle=None, arrival_time=None)
85
- model = visit_to_model(visit)
86
-
87
- assert model.arrival_time is None
88
- assert model.start_service_time is None
89
- assert model.departure_time is None
90
- assert model.driving_time_seconds_from_previous_standstill is None
91
-
92
- def test_assigned_visit_has_timeline_fields(self):
93
- """Assigned visits with arrival_time should have all timeline fields."""
94
- vehicle = create_test_vehicle()
95
- arrival = datetime(2024, 1, 1, 9, 30, 0)
96
- visit = create_test_visit(vehicle=vehicle, arrival_time=arrival)
97
- vehicle.visits = [visit]
98
-
99
- model = visit_to_model(visit)
100
-
101
- # arrival_time should be serialized
102
- assert model.arrival_time is not None
103
- assert model.arrival_time == "2024-01-01T09:30:00"
104
-
105
- # start_service_time = max(arrival_time, min_start_time)
106
- # Since arrival (09:30) > min_start (09:00), start_service = 09:30
107
- assert model.start_service_time is not None
108
- assert model.start_service_time == "2024-01-01T09:30:00"
109
-
110
- # departure_time = start_service_time + service_duration
111
- # = 09:30 + 15min = 09:45
112
- assert model.departure_time is not None
113
- assert model.departure_time == "2024-01-01T09:45:00"
114
-
115
- # driving_time_seconds should be calculated from vehicle home
116
- assert model.driving_time_seconds_from_previous_standstill is not None
117
-
118
- def test_early_arrival_uses_min_start_time(self):
119
- """When arrival is before min_start_time, start_service uses min_start_time."""
120
- vehicle = create_test_vehicle()
121
- # Arrive at 08:30, but min_start is 09:00
122
- early_arrival = datetime(2024, 1, 1, 8, 30, 0)
123
- visit = create_test_visit(vehicle=vehicle, arrival_time=early_arrival)
124
- vehicle.visits = [visit]
125
-
126
- model = visit_to_model(visit)
127
-
128
- # start_service_time should be min_start_time (09:00), not arrival (08:30)
129
- assert model.start_service_time == "2024-01-01T09:00:00"
130
-
131
- # departure should be min_start_time + service_duration = 09:15
132
- assert model.departure_time == "2024-01-01T09:15:00"
133
-
134
-
135
- class TestVehicleTimelineFields:
136
- """Tests for vehicle timeline serialization fields."""
137
-
138
- def test_empty_vehicle_arrival_equals_departure(self):
139
- """Vehicle with no visits should have arrival_time = departure_time."""
140
- departure = datetime(2024, 1, 1, 6, 0, 0)
141
- vehicle = create_test_vehicle(departure_time=departure, visits=[])
142
-
143
- model = vehicle_to_model(vehicle)
144
-
145
- assert model.departure_time == "2024-01-01T06:00:00"
146
- assert model.arrival_time == "2024-01-01T06:00:00"
147
-
148
- def test_vehicle_with_visits_has_later_arrival(self):
149
- """Vehicle with visits should have arrival_time after last visit departure."""
150
- departure = datetime(2024, 1, 1, 6, 0, 0)
151
- vehicle = create_test_vehicle(departure_time=departure)
152
-
153
- # Create a visit assigned to this vehicle
154
- arrival = datetime(2024, 1, 1, 9, 30, 0)
155
- visit = create_test_visit(vehicle=vehicle, arrival_time=arrival)
156
- vehicle.visits = [visit]
157
-
158
- model = vehicle_to_model(vehicle)
159
-
160
- assert model.departure_time == "2024-01-01T06:00:00"
161
- # arrival_time should be > departure_time
162
- assert model.arrival_time is not None
163
- # arrival_time should be after visit departure + travel back to depot
164
-
165
-
166
- class TestPlanTimelineFields:
167
- """Tests for route plan timeline window fields."""
168
-
169
- def test_plan_has_start_and_end_datetime(self):
170
- """Route plan should have startDateTime and endDateTime for timeline window."""
171
- departure = datetime(2024, 1, 1, 6, 0, 0)
172
- vehicle = create_test_vehicle(departure_time=departure)
173
- plan = create_test_plan(vehicles=[vehicle])
174
-
175
- model = plan_to_model(plan)
176
-
177
- # startDateTime should be earliest vehicle departure
178
- assert model.start_date_time is not None
179
- assert model.start_date_time == "2024-01-01T06:00:00"
180
-
181
- # endDateTime should be latest vehicle arrival
182
- # For empty vehicle, arrival = departure
183
- assert model.end_date_time is not None
184
- assert model.end_date_time == "2024-01-01T06:00:00"
185
-
186
- def test_plan_with_multiple_vehicles(self):
187
- """Plan timeline window should span all vehicles."""
188
- early_vehicle = create_test_vehicle(
189
- departure_time=datetime(2024, 1, 1, 5, 0, 0)
190
- )
191
- early_vehicle.id = "1"
192
- late_vehicle = create_test_vehicle(
193
- departure_time=datetime(2024, 1, 1, 8, 0, 0)
194
- )
195
- late_vehicle.id = "2"
196
-
197
- plan = create_test_plan(vehicles=[early_vehicle, late_vehicle])
198
- model = plan_to_model(plan)
199
-
200
- # startDateTime should be earliest departure (05:00)
201
- assert model.start_date_time == "2024-01-01T05:00:00"
202
-
203
- # endDateTime should be latest arrival
204
- # Both vehicles empty, so arrival = departure for each
205
- # Latest is late_vehicle at 08:00
206
- assert model.end_date_time == "2024-01-01T08:00:00"
207
-
208
- def test_empty_plan_has_null_datetimes(self):
209
- """Plan with no vehicles should have null datetime fields."""
210
- plan = create_test_plan(vehicles=[])
211
-
212
- model = plan_to_model(plan)
213
-
214
- assert model.start_date_time is None
215
- assert model.end_date_time is None