Initial commit (clean, ignores in place)
This commit is contained in:
8
tests/__init__.py
Normal file
8
tests/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Test suite for the mileage logging tool.
|
||||
|
||||
The tests in this package exercise the core components of the mileage
|
||||
logger. They use simple JSON fixtures to simulate a user's Google
|
||||
Semantic Location History exports and assert that the itinerary
|
||||
detection, distance resolution and Excel export modules behave as
|
||||
expected.
|
||||
"""
|
73
tests/data/routes_golden.csv
Normal file
73
tests/data/routes_golden.csv
Normal file
@@ -0,0 +1,73 @@
|
||||
# origin,destination,miles
|
||||
Home,Lingwood Primary Academy,12.9
|
||||
Home,Henderson Green Primary Academy,2.9
|
||||
Home,Valley Primary Academy,2.1
|
||||
Home,Heartsease Primary Academy,3.4
|
||||
Home,Colman Junior School,3.1
|
||||
Home,Colman Infant School,3.1
|
||||
Home,Robert Kett Primary School,11.2
|
||||
Home,Unity HQ,64.0
|
||||
Lingwood Primary Academy,Home,12.9
|
||||
Lingwood Primary Academy,Henderson Green Primary Academy,12.8
|
||||
Lingwood Primary Academy,Valley Primary Academy,13.4
|
||||
Lingwood Primary Academy,Heartsease Primary Academy,9.5999999999999996
|
||||
Lingwood Primary Academy,Colman Junior School,11.300000000000001
|
||||
Lingwood Primary Academy,Colman Infant School,11.1
|
||||
Lingwood Primary Academy,Robert Kett Primary School,18.699999999999999
|
||||
Lingwood Primary Academy,Unity HQ,63.100000000000001
|
||||
Henderson Green Primary Academy,Home,2.8999999999999999
|
||||
Henderson Green Primary Academy,Lingwood Primary Academy,12.800000000000001
|
||||
Henderson Green Primary Academy,Valley Primary Academy,1.1000000000000001
|
||||
Henderson Green Primary Academy,Heartsease Primary Academy,4.5
|
||||
Henderson Green Primary Academy,Colman Junior School,1.8
|
||||
Henderson Green Primary Academy,Colman Infant School,1.8
|
||||
Henderson Green Primary Academy,Robert Kett Primary School,8
|
||||
Henderson Green Primary Academy,Unity HQ,63.100000000000001
|
||||
Valley Primary Academy,Home,2.1000000000000001
|
||||
Valley Primary Academy,Lingwood Primary Academy,13.4
|
||||
Valley Primary Academy,Henderson Green Primary Academy,1.1000000000000001
|
||||
Valley Primary Academy,Heartsease Primary Academy,4.7000000000000002
|
||||
Valley Primary Academy,Colman Junior School,2.2999999999999998
|
||||
Valley Primary Academy,Colman Infant School,2.2999999999999998
|
||||
Valley Primary Academy,Robert Kett Primary School,8
|
||||
Valley Primary Academy,Unity HQ,64.700000000000003
|
||||
Heartsease Primary Academy,Home,3.3999999999999999
|
||||
Heartsease Primary Academy,Lingwood Primary Academy,9.5999999999999996
|
||||
Heartsease Primary Academy,Henderson Green Primary Academy,4.5
|
||||
Heartsease Primary Academy,Valley Primary Academy,4.7000000000000002
|
||||
Heartsease Primary Academy,Colman Junior School,4.0999999999999996
|
||||
Heartsease Primary Academy,Colman Infant School,3.8999999999999999
|
||||
Heartsease Primary Academy,Robert Kett Primary School,14
|
||||
Heartsease Primary Academy,Unity HQ,68.299999999999997
|
||||
Colman Junior School,Home,3.1000000000000001
|
||||
Colman Junior School,Lingwood Primary Academy,11.300000000000001
|
||||
Colman Junior School,Henderson Green Primary Academy,1.8
|
||||
Colman Junior School,Valley Primary Academy,2.2999999999999998
|
||||
Colman Junior School,Heartsease Primary Academy,4.0999999999999996
|
||||
Colman Junior School,Colman Infant School,0.20000000000000001
|
||||
Colman Junior School,Robert Kett Primary School,8.4000000000000004
|
||||
Colman Junior School,Unity HQ,62.600000000000001
|
||||
Colman Infant School,Home,3.1000000000000001
|
||||
Colman Infant School,Lingwood Primary Academy,11.1
|
||||
Colman Infant School,Henderson Green Primary Academy,1.8
|
||||
Colman Infant School,Valley Primary Academy,2.2999999999999998
|
||||
Colman Infant School,Heartsease Primary Academy,3.8999999999999999
|
||||
Colman Infant School,Colman Junior School,0.20000000000000001
|
||||
Colman Infant School,Robert Kett Primary School,8.0999999999999996
|
||||
Colman Infant School,Unity HQ,62.399999999999999
|
||||
Robert Kett Primary School,Home,11.199999999999999
|
||||
Robert Kett Primary School,Lingwood Primary Academy,18.699999999999999
|
||||
Robert Kett Primary School,Henderson Green Primary Academy,8
|
||||
Robert Kett Primary School,Valley Primary Academy,8
|
||||
Robert Kett Primary School,Heartsease Primary Academy,14
|
||||
Robert Kett Primary School,Colman Junior School,8.4000000000000004
|
||||
Robert Kett Primary School,Colman Infant School,8.0999999999999996
|
||||
Robert Kett Primary School,Unity HQ,57.299999999999997
|
||||
Unity HQ,Home,64
|
||||
Unity HQ,Lingwood Primary Academy,63.100000000000001
|
||||
Unity HQ,Henderson Green Primary Academy,72.400000000000006
|
||||
Unity HQ,Valley Primary Academy,64.700000000000003
|
||||
Unity HQ,Heartsease Primary Academy,68.299999999999997
|
||||
Unity HQ,Colman Junior School,62.600000000000001
|
||||
Unity HQ,Colman Infant School,62.399999999999999
|
||||
Unity HQ,Robert Kett Primary School,66.299999999999997
|
|
107
tests/fixtures/semantic/2025-08-08.one_day_simple.json
vendored
Normal file
107
tests/fixtures/semantic/2025-08-08.one_day_simple.json
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"timelineObjects": [
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723090800000",
|
||||
"endTimestampMs": "1723092600000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723092600000",
|
||||
"endTimestampMs": "1723093200000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000,
|
||||
"name": "Lingwood Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723093200000",
|
||||
"endTimestampMs": "1723111200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723111200000",
|
||||
"endTimestampMs": "1723112400000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000,
|
||||
"name": "Heartsease Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723112400000",
|
||||
"endTimestampMs": "1723123200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723123200000",
|
||||
"endTimestampMs": "1723125600000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1723125600000",
|
||||
"endTimestampMs": "1723168800000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
137
tests/fixtures/semantic/2025-08-12.looping.json
vendored
Normal file
137
tests/fixtures/semantic/2025-08-12.looping.json
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"timelineObjects": [
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754980200000",
|
||||
"endTimestampMs": "1754982000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754982000000",
|
||||
"endTimestampMs": "1754982900000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000,
|
||||
"name": "Lingwood Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754982900000",
|
||||
"endTimestampMs": "1754989200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524710000,
|
||||
"longitudeE7": 13590000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754989200000",
|
||||
"endTimestampMs": "1754990100000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524710000,
|
||||
"longitudeE7": 13590000,
|
||||
"name": "Valley Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754990100000",
|
||||
"endTimestampMs": "1754996400000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524710000,
|
||||
"longitudeE7": 13590000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754996400000",
|
||||
"endTimestampMs": "1754997300000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000,
|
||||
"name": "Lingwood Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754997300000",
|
||||
"endTimestampMs": "1755003600000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755003600000",
|
||||
"endTimestampMs": "1755004500000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755004500000",
|
||||
"endTimestampMs": "1755014400000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
77
tests/fixtures/semantic/cross_midnight.json
vendored
Normal file
77
tests/fixtures/semantic/cross_midnight.json
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"timelineObjects": [
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754683200000",
|
||||
"endTimestampMs": "1754692200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754692200000",
|
||||
"endTimestampMs": "1754695800000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000,
|
||||
"name": "Heartsease Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754695800000",
|
||||
"endTimestampMs": "1754701200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754701200000",
|
||||
"endTimestampMs": "1754703000000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1754703000000",
|
||||
"endTimestampMs": "1754712000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
137
tests/fixtures/semantic/day_with_detours.json
vendored
Normal file
137
tests/fixtures/semantic/day_with_detours.json
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"timelineObjects": [
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755154800000",
|
||||
"endTimestampMs": "1755156600000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755156600000",
|
||||
"endTimestampMs": "1755157500000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000,
|
||||
"name": "Lingwood Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755157500000",
|
||||
"endTimestampMs": "1755162000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524706000,
|
||||
"longitudeE7": 13538000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524670000,
|
||||
"longitudeE7": 13550000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755162000000",
|
||||
"endTimestampMs": "1755162900000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524670000,
|
||||
"longitudeE7": 13550000,
|
||||
"name": "Nice Cafe"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755162900000",
|
||||
"endTimestampMs": "1755165600000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524670000,
|
||||
"longitudeE7": 13550000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755165600000",
|
||||
"endTimestampMs": "1755166500000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000,
|
||||
"name": "Heartsease Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755166500000",
|
||||
"endTimestampMs": "1755172800000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755172800000",
|
||||
"endTimestampMs": "1755173700000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755173700000",
|
||||
"endTimestampMs": "1755190800000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
77
tests/fixtures/semantic/no_home_start.json
vendored
Normal file
77
tests/fixtures/semantic/no_home_start.json
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"timelineObjects": [
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524610000,
|
||||
"longitudeE7": 13500000,
|
||||
"name": "Unity SP"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755244800000",
|
||||
"endTimestampMs": "1755248400000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524610000,
|
||||
"longitudeE7": 13500000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755248400000",
|
||||
"endTimestampMs": "1755250200000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000,
|
||||
"name": "Heartsease Primary Academy"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755250200000",
|
||||
"endTimestampMs": "1755259200000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
"latitudeE7": 524634000,
|
||||
"longitudeE7": 13627000
|
||||
},
|
||||
"endLocation": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755259200000",
|
||||
"endTimestampMs": "1755261000000"
|
||||
},
|
||||
"activityType": "IN_PASSENGER_VEHICLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"placeVisit": {
|
||||
"location": {
|
||||
"latitudeE7": 524649000,
|
||||
"longitudeE7": 13460000,
|
||||
"name": "Home"
|
||||
},
|
||||
"duration": {
|
||||
"startTimestampMs": "1755261000000",
|
||||
"endTimestampMs": "1755277200000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
42
tests/test_distance_resolver.py
Normal file
42
tests/test_distance_resolver.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from mileage_logger.logic.detect_itinerary import SiteConfig, haversine_distance
|
||||
from mileage_logger.distance.resolve import DistanceResolver
|
||||
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
|
||||
ROUTES_PATH = os.path.join(os.path.dirname(__file__), "data", "routes_golden.csv")
|
||||
|
||||
|
||||
class TestDistanceResolver(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
|
||||
cls.resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
|
||||
|
||||
def test_route_lookup(self):
|
||||
origin = "Home"
|
||||
dest = "Lingwood Primary Academy"
|
||||
origin_site = self.site_config.by_canonical[origin]
|
||||
dest_site = self.site_config.by_canonical[dest]
|
||||
dist = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
|
||||
self.assertAlmostEqual(dist, 13.0, places=1)
|
||||
# Second call should hit cache and return same
|
||||
dist2 = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
|
||||
self.assertEqual(dist2, dist)
|
||||
|
||||
def test_fallback_haversine(self):
|
||||
# Choose a pair not in the route catalogue
|
||||
origin = "Lingwood Primary Academy"
|
||||
dest = "Unity SP"
|
||||
origin_site = self.site_config.by_canonical[origin]
|
||||
dest_site = self.site_config.by_canonical[dest]
|
||||
dist = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
|
||||
# Compute haversine expected
|
||||
expected = haversine_distance(origin_site.lat, origin_site.lon, dest_site.lat, dest_site.lon)
|
||||
self.assertAlmostEqual(dist, expected, places=1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
91
tests/test_excel_writer.py
Normal file
91
tests/test_excel_writer.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from mileage_logger.ingest.semantic_reader import load_place_visits
|
||||
from mileage_logger.logic.detect_itinerary import SiteConfig, detect_itinerary
|
||||
from mileage_logger.distance.resolve import DistanceResolver
|
||||
from mileage_logger.export.excel_writer import build_monthly_rows, write_monthly_workbook
|
||||
|
||||
|
||||
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "semantic")
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
|
||||
ROUTES_PATH = os.path.join(os.path.dirname(__file__), "data", "routes_golden.csv")
|
||||
|
||||
|
||||
class TestExcelWriter(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
|
||||
|
||||
def _build_workbook(self, fixture_file: str) -> str:
|
||||
visits = load_place_visits(os.path.join(FIXTURES_DIR, fixture_file))
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
|
||||
rows_by_month = build_monthly_rows(hops, self.site_config, resolver)
|
||||
# Write to a temporary file
|
||||
fd, path = tempfile.mkstemp(suffix=".xlsx")
|
||||
os.close(fd)
|
||||
write_monthly_workbook(rows_by_month, path)
|
||||
return path
|
||||
|
||||
def test_excel_layout_simple(self):
|
||||
# Build workbook for the simple fixture
|
||||
path = self._build_workbook("2025-08-08.one_day_simple.json")
|
||||
try:
|
||||
wb = load_workbook(path)
|
||||
# Determine expected month from the first hop date
|
||||
visits = load_place_visits(os.path.join(FIXTURES_DIR, "2025-08-08.one_day_simple.json"))
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
if not hops:
|
||||
self.fail("No hops detected for simple fixture")
|
||||
expected_month = hops[0].date.strftime("%Y-%m")
|
||||
self.assertIn(expected_month, wb.sheetnames)
|
||||
ws = wb[expected_month]
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
# number of hops + header
|
||||
self.assertEqual(len(rows), len(hops) + 1)
|
||||
header = rows[0]
|
||||
expected_header = ("Date", "Purpose", "Miles", "Vehicle", "Job Role", "From", "To", "Notes")
|
||||
self.assertEqual(header, expected_header)
|
||||
# Validate each hop row
|
||||
resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
|
||||
for i, hop in enumerate(hops, start=1):
|
||||
row = rows[i]
|
||||
origin_site = self.site_config.by_canonical[hop.origin]
|
||||
dest_site = self.site_config.by_canonical[hop.destination]
|
||||
dist = resolver.resolve(hop.origin, hop.destination, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
|
||||
self.assertEqual(row[0], hop.date.isoformat())
|
||||
expected_purpose = f"Travel from {origin_site.label} to {dest_site.label} {dist:.1f}mi"
|
||||
self.assertEqual(row[1], expected_purpose)
|
||||
self.assertAlmostEqual(float(row[2]), dist, places=1)
|
||||
self.assertEqual(row[3], resolver.vehicle_label)
|
||||
self.assertEqual(row[4], resolver.job_role)
|
||||
self.assertEqual(row[5], origin_site.label)
|
||||
self.assertEqual(row[6], dest_site.label)
|
||||
# Notes may be returned as None when reading from Excel
|
||||
self.assertIn(row[7] or "", ["", None])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_cross_midnight_sheet_rows(self):
|
||||
path = self._build_workbook("cross_midnight.json")
|
||||
try:
|
||||
wb = load_workbook(path)
|
||||
# Should still be month 2025-08
|
||||
self.assertIn("2025-08", wb.sheetnames)
|
||||
ws = wb["2025-08"]
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
# two hops -> 3 rows including header
|
||||
self.assertEqual(len(rows), 3)
|
||||
# Dates should span two days
|
||||
dates = [r[0] for r in rows[1:]]
|
||||
self.assertEqual(dates, ["2025-08-08", "2025-08-09"])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
79
tests/test_itinerary.py
Normal file
79
tests/test_itinerary.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
from mileage_logger.ingest.semantic_reader import load_place_visits
|
||||
from mileage_logger.logic.detect_itinerary import SiteConfig, detect_itinerary
|
||||
|
||||
|
||||
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "semantic")
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
|
||||
|
||||
|
||||
class TestItineraryDetection(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
|
||||
|
||||
def _load_visits(self, filename: str):
|
||||
path = os.path.join(FIXTURES_DIR, filename)
|
||||
return load_place_visits(path)
|
||||
|
||||
def test_one_day_simple(self):
|
||||
visits = self._load_visits("2025-08-08.one_day_simple.json")
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
canonical_pairs = [(h.origin, h.destination) for h in hops]
|
||||
expected = [
|
||||
("Home", "Lingwood Primary Academy"),
|
||||
("Lingwood Primary Academy", "Heartsease Primary Academy"),
|
||||
("Heartsease Primary Academy", "Home"),
|
||||
]
|
||||
self.assertEqual(canonical_pairs, expected)
|
||||
|
||||
def test_looping_multi_visit(self):
|
||||
visits = self._load_visits("2025-08-12.looping.json")
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
canonical_pairs = [(h.origin, h.destination) for h in hops]
|
||||
expected = [
|
||||
("Home", "Lingwood Primary Academy"),
|
||||
("Lingwood Primary Academy", "Valley Primary Academy"),
|
||||
("Valley Primary Academy", "Lingwood Primary Academy"),
|
||||
("Lingwood Primary Academy", "Home"),
|
||||
]
|
||||
self.assertEqual(canonical_pairs, expected)
|
||||
|
||||
def test_detours_ignored(self):
|
||||
visits = self._load_visits("day_with_detours.json")
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
canonical_pairs = [(h.origin, h.destination) for h in hops]
|
||||
expected = [
|
||||
("Home", "Lingwood Primary Academy"),
|
||||
("Lingwood Primary Academy", "Heartsease Primary Academy"),
|
||||
("Heartsease Primary Academy", "Home"),
|
||||
]
|
||||
self.assertEqual(canonical_pairs, expected)
|
||||
|
||||
def test_no_home_start(self):
|
||||
visits = self._load_visits("no_home_start.json")
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
canonical_pairs = [(h.origin, h.destination) for h in hops]
|
||||
expected = [
|
||||
("Unity SP", "Heartsease Primary Academy"),
|
||||
("Heartsease Primary Academy", "Home"),
|
||||
]
|
||||
self.assertEqual(canonical_pairs, expected)
|
||||
|
||||
def test_cross_midnight_dates(self):
|
||||
visits = self._load_visits("cross_midnight.json")
|
||||
hops = detect_itinerary(visits, self.site_config)
|
||||
self.assertEqual(len(hops), 2)
|
||||
# Ensure canonical names
|
||||
self.assertEqual((hops[0].origin, hops[0].destination), ("Home", "Heartsease Primary Academy"))
|
||||
self.assertEqual((hops[1].origin, hops[1].destination), ("Heartsease Primary Academy", "Home"))
|
||||
# Check dates (local) across midnight
|
||||
self.assertEqual(hops[0].date, date(2025, 8, 8))
|
||||
self.assertEqual(hops[1].date, date(2025, 8, 9))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in New Issue
Block a user