116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
|
|
from unittest.mock import MagicMock
|
|
import sys
|
|
|
|
# Mock fitdecode before imports since it might not be installed in local env (running in docker)
|
|
sys.modules['fitdecode'] = MagicMock()
|
|
|
|
import pytest
|
|
from src.utils.geo import ramer_douglas_peucker, haversine_distance, calculate_bounds
|
|
from src.services.segment_matcher import SegmentMatcher
|
|
from src.models.activity import Activity
|
|
from src.models.segment import Segment
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
def test_haversine():
|
|
# Dist between (0,0) and (0,1) deg is ~111km
|
|
d = haversine_distance(0, 0, 0, 1)
|
|
# 1 deg lat ~ 111.32 km
|
|
assert 110000 < d < 112000
|
|
|
|
def test_rdp_simple():
|
|
# Points on a line
|
|
points = [[0,0], [1,1], [2,2]]
|
|
# Should simplify to [0,0], [2,2]
|
|
simplified = ramer_douglas_peucker(points, epsilon=0.1)
|
|
assert len(simplified) == 2
|
|
assert simplified[0] == [0,0]
|
|
assert simplified[1] == [2,2]
|
|
|
|
def test_rdp_peak():
|
|
# Triangle
|
|
points = [[0,0], [1,10], [2,0]] # [lon, lat] note: RDP expects [lon, lat] usually?
|
|
# My RDP implementation uses x,y so order doesn't matter for geometric shape
|
|
simplified = ramer_douglas_peucker(points, epsilon=1.0)
|
|
assert len(simplified) == 3
|
|
|
|
def test_bounds():
|
|
points = [[0,0], [10, 10], [-5, 5]]
|
|
bounds = calculate_bounds(points)
|
|
assert bounds['min_lat'] == 0 # wait, index 1 is lat? check utils
|
|
# If points are [lon, lat]
|
|
# 0,0: lat=0
|
|
# 10,10: lat=10
|
|
# -5,5: lat=5
|
|
# bounds are min_lat=0, max_lat=10. min_lon=-5, max_lon=10
|
|
|
|
# My calculate_bounds implementation assumes [lon, lat]
|
|
assert bounds['min_lat'] == 0
|
|
assert bounds['max_lat'] == 10
|
|
assert bounds['min_lon'] == -5
|
|
assert bounds['max_lon'] == 10
|
|
|
|
def test_matcher_logic():
|
|
# Mock DB session
|
|
mock_session = MagicMock()
|
|
|
|
# Create segment [0,0] -> [0, 0.01] (approx 1.1km north)
|
|
segment_points = [[0,0], [0, 0.01]]
|
|
segment = Segment(
|
|
id=1,
|
|
name="Test Seg",
|
|
points=json.dumps(segment_points),
|
|
bounds=json.dumps(calculate_bounds(segment_points)),
|
|
distance=1110.0,
|
|
activity_type='cycling'
|
|
)
|
|
|
|
mock_session.query.return_value.filter.return_value.all.return_value = [segment]
|
|
|
|
matcher = SegmentMatcher(mock_session)
|
|
|
|
# Create activity trace that covers this
|
|
# 0,0 at T=0
|
|
# 0,0.01 at T=100s
|
|
act_points = [[0,0], [0, 0.005], [0, 0.01]]
|
|
|
|
# Mock activity
|
|
activity = Activity(id=100, activity_start_time=datetime.now())
|
|
# Matcher needs to use parsers internally? Or uses slice of points?
|
|
# Matcher logic (_match_segment) uses points list passed to match_activity
|
|
|
|
# Wait, _match_segment needs timestamps to calc elapsed time.
|
|
# We need to mock extract_timestamps_from_file or patch it
|
|
from unittest.mock import patch
|
|
|
|
with patch('src.services.segment_matcher.extract_timestamps_from_file') as mock_extract:
|
|
# 0,0@T0, 0,0.005@T50, 0,0.01@T100
|
|
start_time = datetime.now()
|
|
timestamps = [start_time, start_time + timedelta(seconds=50), start_time + timedelta(seconds=100)]
|
|
mock_extract.return_value = timestamps
|
|
|
|
# Add dummy content
|
|
activity.file_content = b'dummy'
|
|
activity.file_type = 'fit'
|
|
|
|
# Run match
|
|
efforts = matcher.match_activity(activity, act_points)
|
|
|
|
assert len(efforts) == 1
|
|
effort = efforts[0]
|
|
assert effort.segment_id == 1
|
|
assert effort.elapsed_time == 100.0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# verification
|
|
try:
|
|
test_haversine()
|
|
test_rdp_simple()
|
|
test_bounds()
|
|
print("Geo Utils Passed")
|
|
except Exception as e:
|
|
print(f"Failed: {e}")
|