269 lines
9.1 KiB
Python
269 lines
9.1 KiB
Python
# test_gpx.py
|
|
#
|
|
# author: deng
|
|
# date: 20251129
|
|
|
|
import io
|
|
|
|
import pytest
|
|
|
|
|
|
class TestGPXProcessor:
|
|
"""Test cases for GPXProcessor class."""
|
|
|
|
@pytest.fixture
|
|
def simple_gpx_data(self):
|
|
"""Create a simple GPX file data for testing."""
|
|
gpx_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1" creator="test">
|
|
<wpt lat="25.0" lon="121.0">
|
|
<ele>100</ele>
|
|
<name>Test Waypoint</name>
|
|
</wpt>
|
|
<trk>
|
|
<name>Test Track</name>
|
|
<trkseg>
|
|
<trkpt lat="25.0" lon="121.0">
|
|
<ele>100</ele>
|
|
<time>2024-01-01T00:00:00Z</time>
|
|
</trkpt>
|
|
<trkpt lat="25.001" lon="121.001">
|
|
<ele>150</ele>
|
|
<time>2024-01-01T00:01:00Z</time>
|
|
</trkpt>
|
|
<trkpt lat="25.002" lon="121.002">
|
|
<ele>120</ele>
|
|
<time>2024-01-01T00:02:00Z</time>
|
|
</trkpt>
|
|
</trkseg>
|
|
</trk>
|
|
</gpx>"""
|
|
return io.BytesIO(gpx_xml.encode('utf-8'))
|
|
|
|
@pytest.fixture
|
|
def gpx_processor(self, simple_gpx_data):
|
|
"""Create a GPXProcessor instance with simple data."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(simple_gpx_data)
|
|
processor.validate_and_parse()
|
|
return processor
|
|
|
|
def test_init(self):
|
|
"""Test GPXProcessor initialization."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
gpx_file = io.BytesIO(b'test data')
|
|
processor = GPXProcessor(gpx_file)
|
|
|
|
assert processor.gpx_file == gpx_file
|
|
assert processor.gpx is None
|
|
assert processor.points == []
|
|
assert processor.elevations == []
|
|
assert processor.waypoints == []
|
|
|
|
def test_validate_and_parse_success(self, simple_gpx_data):
|
|
"""Test successful GPX parsing."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(simple_gpx_data)
|
|
result = processor.validate_and_parse()
|
|
|
|
assert result is True
|
|
assert len(processor.points) == 3
|
|
assert len(processor.elevations) == 3
|
|
assert len(processor.waypoints) == 1
|
|
|
|
def test_validate_and_parse_invalid_gpx_raises_exception(self):
|
|
"""Test that invalid GPX data raises exception."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
invalid_data = io.BytesIO(b'not a valid gpx file')
|
|
processor = GPXProcessor(invalid_data)
|
|
|
|
with pytest.raises(Exception, match='GPX 檔案解析失敗'):
|
|
processor.validate_and_parse()
|
|
|
|
def test_waypoint_extraction(self, gpx_processor):
|
|
"""Test waypoint extraction from GPX."""
|
|
assert len(gpx_processor.waypoints) == 1
|
|
waypoint = gpx_processor.waypoints[0]
|
|
assert waypoint['lat'] == 25.0
|
|
assert waypoint['lon'] == 121.0
|
|
assert waypoint['name'] == 'Test Waypoint'
|
|
assert waypoint['elevation'] == 100.0
|
|
|
|
def test_calculate_distance(self, gpx_processor):
|
|
"""Test distance calculation."""
|
|
distance = gpx_processor.calculate_distance()
|
|
|
|
assert distance > 0
|
|
assert isinstance(distance, float)
|
|
assert len(gpx_processor.distances) == 3
|
|
assert gpx_processor.distances[0] == 0.0
|
|
|
|
def test_calculate_distance_empty_points_returns_zero(self):
|
|
"""Test distance calculation with no points."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(io.BytesIO(b''))
|
|
distance = processor.calculate_distance()
|
|
|
|
assert distance == 0.0
|
|
|
|
def test_calculate_elevation_gain_loss(self, gpx_processor):
|
|
"""Test elevation gain/loss calculation."""
|
|
gain, loss = gpx_processor.calculate_elevation_gain_loss()
|
|
|
|
assert isinstance(gain, float)
|
|
assert isinstance(loss, float)
|
|
assert gain >= 0
|
|
assert loss >= 0
|
|
|
|
def test_get_min_max_elevation(self, gpx_processor):
|
|
"""Test min/max elevation retrieval."""
|
|
min_elev, max_elev = gpx_processor.get_min_max_elevation()
|
|
|
|
assert min_elev == 100.0
|
|
assert max_elev == 150.0
|
|
|
|
def test_get_min_max_elevation_empty_returns_zero(self):
|
|
"""Test min/max elevation with empty data."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(io.BytesIO(b''))
|
|
min_elev, max_elev = processor.get_min_max_elevation()
|
|
|
|
assert min_elev == 0.0
|
|
assert max_elev == 0.0
|
|
|
|
def test_get_start_end_points(self, gpx_processor):
|
|
"""Test start/end points retrieval."""
|
|
start, end = gpx_processor.get_start_end_points()
|
|
|
|
assert start == (25.0, 121.0, 100.0)
|
|
assert end == (25.002, 121.002, 120.0)
|
|
|
|
def test_get_start_end_points_insufficient_data_returns_none(self):
|
|
"""Test start/end points with insufficient data."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(io.BytesIO(b''))
|
|
start, end = processor.get_start_end_points()
|
|
|
|
assert start is None
|
|
assert end is None
|
|
|
|
def test_get_all_points(self, gpx_processor):
|
|
"""Test getting all route points."""
|
|
points = gpx_processor.get_all_points()
|
|
|
|
assert len(points) == 3
|
|
assert all(isinstance(p, tuple) and len(p) == 2 for p in points)
|
|
|
|
def test_get_elevation_profile_data(self, gpx_processor):
|
|
"""Test elevation profile data retrieval."""
|
|
# Need to calculate distances first
|
|
gpx_processor.calculate_distance()
|
|
distances, elevations = gpx_processor.get_elevation_profile_data()
|
|
|
|
assert len(distances) == len(elevations)
|
|
assert len(distances) == 3
|
|
|
|
def test_get_gradients(self, gpx_processor):
|
|
"""Test gradient calculation."""
|
|
gradients = gpx_processor.get_gradients()
|
|
|
|
assert len(gradients) == 2 # n-1 gradients for n points
|
|
assert all(isinstance(g, (int, float)) for g in gradients)
|
|
|
|
def test_get_gradients_empty_returns_empty_list(self):
|
|
"""Test gradient calculation with no points."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(io.BytesIO(b''))
|
|
gradients = processor.get_gradients()
|
|
|
|
assert gradients == []
|
|
|
|
def test_calculate_naismith_time(self, gpx_processor):
|
|
"""Test Naismith's Rule time calculation."""
|
|
time_minutes = gpx_processor.calculate_naismith_time()
|
|
|
|
assert isinstance(time_minutes, int)
|
|
assert time_minutes >= 0
|
|
|
|
def test_calculate_naismith_time_with_custom_speeds(self, gpx_processor):
|
|
"""Test Naismith calculation with custom speeds."""
|
|
time_default = gpx_processor.calculate_naismith_time()
|
|
time_fast = gpx_processor.calculate_naismith_time(horizontal_speed=10, vertical_speed=1200)
|
|
|
|
# Faster speeds should result in less time
|
|
assert time_fast < time_default
|
|
|
|
def test_calculate_naismith_time_no_points_returns_zero(self):
|
|
"""Test Naismith calculation with no points."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
processor = GPXProcessor(io.BytesIO(b''))
|
|
time_minutes = processor.calculate_naismith_time()
|
|
|
|
assert time_minutes == 0
|
|
|
|
|
|
class TestGPXProcessorEdgeCases:
|
|
"""Test edge cases for GPXProcessor."""
|
|
|
|
def test_gpx_with_bytes_data(self):
|
|
"""Test GPX parsing with bytes input."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
gpx_xml = b"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1">
|
|
<trk>
|
|
<trkseg>
|
|
<trkpt lat="25.0" lon="121.0"><ele>100</ele></trkpt>
|
|
</trkseg>
|
|
</trk>
|
|
</gpx>"""
|
|
processor = GPXProcessor(io.BytesIO(gpx_xml))
|
|
result = processor.validate_and_parse()
|
|
|
|
assert result is True
|
|
|
|
def test_gpx_with_string_data(self):
|
|
"""Test GPX parsing with string input."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
gpx_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1">
|
|
<trk>
|
|
<trkseg>
|
|
<trkpt lat="25.0" lon="121.0"><ele>100</ele></trkpt>
|
|
</trkseg>
|
|
</trk>
|
|
</gpx>"""
|
|
processor = GPXProcessor(io.StringIO(gpx_xml))
|
|
result = processor.validate_and_parse()
|
|
|
|
assert result is True
|
|
|
|
def test_gpx_with_no_elevation_data(self):
|
|
"""Test GPX parsing when elevation data is missing."""
|
|
from hiking_assistant.gpx import GPXProcessor
|
|
|
|
gpx_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<gpx version="1.1">
|
|
<trk>
|
|
<trkseg>
|
|
<trkpt lat="25.0" lon="121.0"></trkpt>
|
|
<trkpt lat="25.001" lon="121.001"></trkpt>
|
|
</trkseg>
|
|
</trk>
|
|
</gpx>"""
|
|
processor = GPXProcessor(io.BytesIO(gpx_xml.encode('utf-8')))
|
|
processor.validate_and_parse()
|
|
|
|
# Should default to 0 elevation
|
|
assert all(e == 0 for e in processor.elevations)
|