- test_app_trip + test_trip_sql: mock was_car_ever_connected() so trip building runs (BUG-10 guard suppresses trips until car ECU reached) - test_app_settings: include odometer_baseline in valid payload + assert save - test_yaris_uplink: Hotspot -> Hotspot-local; restore_fallback no longer activates AP (reports no_connection); wait timeout writes fallback_restored - test_app_network: local_hotspot mode removed by WiFi swap -> wifi/no-internet
165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
import unittest
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
from modules import app_trip
|
|
|
|
|
|
class TripBuilderTest(unittest.TestCase):
|
|
def setUp(self):
|
|
# BUG-10 guard suppresses trips until the car ECU has been reached
|
|
# once. Trip-building tests simulate a connected car.
|
|
self._car_connected = patch(
|
|
"modules.app_obd.was_car_ever_connected", return_value=True
|
|
)
|
|
self._car_connected.start()
|
|
|
|
def tearDown(self):
|
|
self._car_connected.stop()
|
|
|
|
def rows_for_route(self, coordinates, speeds=None, start_id=1):
|
|
started = datetime(2026, 1, 1, 8)
|
|
speeds = speeds or [0] * len(coordinates)
|
|
return [
|
|
(
|
|
start_id + index,
|
|
started + timedelta(seconds=index * 5),
|
|
latitude,
|
|
longitude,
|
|
speeds[index],
|
|
)
|
|
for index, (latitude, longitude) in enumerate(coordinates)
|
|
]
|
|
|
|
def test_builds_gps_trip_after_sustained_motion(self):
|
|
rows = self.rows_for_route([
|
|
(45.5000, -73.5000),
|
|
(45.5001, -73.5000),
|
|
(45.5002, -73.5000),
|
|
(45.5003, -73.5000),
|
|
])
|
|
|
|
trips = app_trip.build_trip_summaries(rows)
|
|
|
|
self.assertEqual(len(trips), 1)
|
|
self.assertEqual(trips[0]["start_tracking_id"], 1)
|
|
self.assertEqual(trips[0]["end_tracking_id"], 4)
|
|
self.assertGreater(trips[0]["distance_km"], 0.03)
|
|
self.assertGreater(trips[0]["average_speed_kmh"], 0)
|
|
self.assertGreater(trips[0]["max_speed_kmh"], 0)
|
|
|
|
def test_ignores_stationary_jitter_and_one_bad_coordinate(self):
|
|
rows = self.rows_for_route([
|
|
(45.500000, -73.500000),
|
|
(45.500010, -73.500000),
|
|
("\x0045.500020", -73.500000),
|
|
("not-a-coordinate", -73.500000),
|
|
(45.500010, -73.500000),
|
|
])
|
|
|
|
self.assertEqual(app_trip.build_trip_summaries(rows), [])
|
|
|
|
def test_inactivity_gap_splits_trips(self):
|
|
first = self.rows_for_route([
|
|
(45.5000, -73.5000),
|
|
(45.5001, -73.5000),
|
|
(45.5002, -73.5000),
|
|
(45.5003, -73.5000),
|
|
])
|
|
second = self.rows_for_route([
|
|
(45.5100, -73.5100),
|
|
(45.5101, -73.5100),
|
|
(45.5102, -73.5100),
|
|
(45.5103, -73.5100),
|
|
], start_id=5)
|
|
second = [
|
|
(row[0], row[1] + timedelta(minutes=10), row[2], row[3], row[4])
|
|
for row in second
|
|
]
|
|
|
|
trips = app_trip.build_trip_summaries(first + second)
|
|
|
|
self.assertEqual(len(trips), 2)
|
|
|
|
def test_obd_speed_detects_slow_trip(self):
|
|
rows = self.rows_for_route(
|
|
[(45.5, -73.5), (45.5, -73.5)],
|
|
speeds=[0, 5],
|
|
)
|
|
|
|
trips = app_trip.build_trip_summaries(rows)
|
|
|
|
self.assertEqual(len(trips), 1)
|
|
self.assertEqual(trips[0]["max_speed_kmh"], 5)
|
|
|
|
def test_obd_trip_counts_sub_threshold_gps_distance(self):
|
|
rows = self.rows_for_route(
|
|
[(45.5 + index * 0.00006, -73.5) for index in range(10)],
|
|
speeds=[5] * 10,
|
|
)
|
|
|
|
trips = app_trip.build_trip_summaries(rows)
|
|
|
|
self.assertGreater(trips[0]["distance_km"], 0.05)
|
|
self.assertGreater(trips[0]["average_speed_kmh"], 0)
|
|
|
|
def test_combined_obd_and_gps_motion_creates_one_trip(self):
|
|
rows = self.rows_for_route(
|
|
[(45.5 + index * 0.0001, -73.5) for index in range(5)],
|
|
speeds=[20] * 5,
|
|
)
|
|
|
|
trips = app_trip.build_trip_summaries(rows)
|
|
|
|
self.assertEqual(len(trips), 1)
|
|
self.assertEqual(trips[0]["start_tracking_id"], 1)
|
|
self.assertEqual(trips[0]["end_tracking_id"], 5)
|
|
|
|
def test_clock_rollback_does_not_merge_motion_episodes(self):
|
|
first = self.rows_for_route(
|
|
[(45.5, -73.5), (45.5, -73.5)],
|
|
speeds=[0, 5],
|
|
)
|
|
second = self.rows_for_route(
|
|
[(45.51, -73.51), (45.51, -73.51)],
|
|
speeds=[0, 5],
|
|
start_id=3,
|
|
)
|
|
second = [
|
|
(row[0], row[1] - timedelta(hours=1), row[2], row[3], row[4])
|
|
for row in second
|
|
]
|
|
|
|
trips = app_trip.build_trip_summaries(first + second)
|
|
|
|
self.assertEqual(len(trips), 2)
|
|
|
|
def test_route_downsampling_keeps_endpoints(self):
|
|
rows = self.rows_for_route(
|
|
[(45.5 + index / 10000, -73.5) for index in range(20)]
|
|
)
|
|
|
|
points = app_trip.route_points(rows, max_points=5)
|
|
|
|
self.assertEqual(len(points), 5)
|
|
self.assertEqual(points[0]["id"], 1)
|
|
self.assertEqual(points[-1]["id"], 20)
|
|
self.assertEqual(points[0]["speed"], 0)
|
|
self.assertGreater(points[-1]["speed"], 0)
|
|
self.assertEqual(points[-1]["speed_source"], "gps")
|
|
|
|
def test_route_filters_implausible_gps_outlier(self):
|
|
rows = self.rows_for_route([
|
|
(45.5000, -73.5000),
|
|
(0, 0),
|
|
(45.5001, -73.5000),
|
|
])
|
|
|
|
points = app_trip.route_points(rows)
|
|
|
|
self.assertEqual([point["id"] for point in points], [1, 3])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|