test gpx, weather, utils
This commit is contained in:
@ -14,7 +14,14 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: ruff
|
name: ruff
|
||||||
entry: ruff check .
|
entry: uv run ruff check .
|
||||||
language: python
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- id: pytest
|
||||||
|
name: pytest
|
||||||
|
entry: uv run pytest tests/ -v
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
always_run: true
|
always_run: true
|
||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# __init__.py for tests package
|
||||||
268
tests/test_gpx.py
Normal file
268
tests/test_gpx.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
# 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)
|
||||||
62
tests/test_utils.py
Normal file
62
tests/test_utils.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# test_utils.py
|
||||||
|
#
|
||||||
|
# author: deng
|
||||||
|
# date: 20251129
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertImageToBase64:
|
||||||
|
"""Test cases for convert_image_to_base64 function."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_image_file(self):
|
||||||
|
"""Create a temporary image file for testing."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='wb', suffix='.jpg', delete=False) as f:
|
||||||
|
# Create a simple test image (1x1 red pixel)
|
||||||
|
f.write(b'\xff\xd8\xff\xe0\x00\x10JFIF') # JPEG header
|
||||||
|
temp_path = f.name
|
||||||
|
yield temp_path
|
||||||
|
# Cleanup
|
||||||
|
Path(temp_path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
def test_convert_image_to_base64_returns_string(self, temp_image_file):
|
||||||
|
"""Test that function returns a string."""
|
||||||
|
from hiking_assistant.utils import convert_image_to_base64
|
||||||
|
|
||||||
|
result = convert_image_to_base64(temp_image_file)
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
def test_convert_image_to_base64_returns_valid_base64(self, temp_image_file):
|
||||||
|
"""Test that returned string is valid base64."""
|
||||||
|
from hiking_assistant.utils import convert_image_to_base64
|
||||||
|
|
||||||
|
result = convert_image_to_base64(temp_image_file)
|
||||||
|
# Try to decode - should not raise exception
|
||||||
|
decoded = base64.b64decode(result)
|
||||||
|
assert isinstance(decoded, bytes)
|
||||||
|
|
||||||
|
def test_convert_image_to_base64_roundtrip(self, temp_image_file):
|
||||||
|
"""Test roundtrip conversion (file -> base64 -> binary)."""
|
||||||
|
from hiking_assistant.utils import convert_image_to_base64
|
||||||
|
|
||||||
|
# Read original file
|
||||||
|
with open(temp_image_file, 'rb') as f:
|
||||||
|
original_data = f.read()
|
||||||
|
|
||||||
|
# Convert to base64 and back
|
||||||
|
base64_str = convert_image_to_base64(temp_image_file)
|
||||||
|
decoded_data = base64.b64decode(base64_str)
|
||||||
|
|
||||||
|
assert decoded_data == original_data
|
||||||
|
|
||||||
|
def test_convert_image_to_base64_nonexistent_file_raises_error(self):
|
||||||
|
"""Test that function raises error for non-existent file."""
|
||||||
|
from hiking_assistant.utils import convert_image_to_base64
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
convert_image_to_base64('/nonexistent/path/to/image.jpg')
|
||||||
262
tests/test_weather.py
Normal file
262
tests/test_weather.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
# test_weather.py
|
||||||
|
#
|
||||||
|
# author: deng
|
||||||
|
# date: 20251129
|
||||||
|
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeatherFetcher:
|
||||||
|
"""Test cases for WeatherFetcher class."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def weather_fetcher(self):
|
||||||
|
"""Create a WeatherFetcher instance."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
return WeatherFetcher()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_weather_response(self):
|
||||||
|
"""Mock weather API response data."""
|
||||||
|
return {
|
||||||
|
'current': {
|
||||||
|
'relative_humidity_2m': 75,
|
||||||
|
'apparent_temperature': 18.5,
|
||||||
|
'precipitation_probability': 30,
|
||||||
|
'precipitation': 0.5,
|
||||||
|
'wind_speed_10m': 15.2,
|
||||||
|
'wind_direction_10m': 180,
|
||||||
|
},
|
||||||
|
'daily': {
|
||||||
|
'temperature_2m_max': [22.0, 23.5, 21.0],
|
||||||
|
'temperature_2m_min': [15.0, 16.0, 14.5],
|
||||||
|
'apparent_temperature_max': [20.0, 21.5, 19.0],
|
||||||
|
'apparent_temperature_min': [13.0, 14.0, 12.5],
|
||||||
|
'relative_humidity_2m_mean': [70, 75, 68],
|
||||||
|
'precipitation_sum': [0.0, 2.5, 0.0],
|
||||||
|
'precipitation_probability_max': [20, 60, 15],
|
||||||
|
'wind_speed_10m_max': [25.0, 35.0, 20.0],
|
||||||
|
'wind_direction_10m_dominant': [180, 90, 270],
|
||||||
|
'sunrise': ['2024-01-01T06:00:00', '2024-01-02T06:01:00', '2024-01-03T06:02:00'],
|
||||||
|
'sunset': ['2024-01-01T18:00:00', '2024-01-02T18:01:00', '2024-01-03T18:02:00'],
|
||||||
|
'uv_index_max': [5.0, 7.0, 3.0],
|
||||||
|
'time': ['2024-01-01', '2024-01-02', '2024-01-03'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_init_default_values(self):
|
||||||
|
"""Test WeatherFetcher initialization with defaults."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher()
|
||||||
|
|
||||||
|
assert fetcher.api_url == 'https://api.open-meteo.com/v1/forecast'
|
||||||
|
assert fetcher.request_timeout == 8
|
||||||
|
assert fetcher.forecast_days == 7
|
||||||
|
|
||||||
|
def test_init_custom_values(self):
|
||||||
|
"""Test WeatherFetcher initialization with custom values."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher(api_url='https://custom.api.com', request_timeout=10, forecast_days=3)
|
||||||
|
|
||||||
|
assert fetcher.api_url == 'https://custom.api.com'
|
||||||
|
assert fetcher.request_timeout == 10
|
||||||
|
assert fetcher.forecast_days == 3
|
||||||
|
|
||||||
|
def test_convert_wind_degrees_to_flow_direction_north(self, weather_fetcher):
|
||||||
|
"""Test wind direction conversion for north."""
|
||||||
|
result = weather_fetcher.convert_wind_degrees_to_flow_direction(0)
|
||||||
|
assert result in ['⬇️', '⬆️'] # 0 and 360 degrees
|
||||||
|
|
||||||
|
def test_convert_wind_degrees_to_flow_direction_east(self, weather_fetcher):
|
||||||
|
"""Test wind direction conversion for east."""
|
||||||
|
result = weather_fetcher.convert_wind_degrees_to_flow_direction(90)
|
||||||
|
assert result == '⬅️' # 90 degrees = east wind blowing west
|
||||||
|
|
||||||
|
def test_convert_wind_degrees_to_flow_direction_south(self, weather_fetcher):
|
||||||
|
"""Test wind direction conversion for south."""
|
||||||
|
result = weather_fetcher.convert_wind_degrees_to_flow_direction(180)
|
||||||
|
assert result == '⬆️'
|
||||||
|
|
||||||
|
def test_convert_wind_degrees_to_flow_direction_west(self, weather_fetcher):
|
||||||
|
"""Test wind direction conversion for west."""
|
||||||
|
result = weather_fetcher.convert_wind_degrees_to_flow_direction(270)
|
||||||
|
assert result == '➡️' # 270 degrees = west wind blowing east
|
||||||
|
|
||||||
|
def test_convert_wind_degrees_to_flow_direction_none(self, weather_fetcher):
|
||||||
|
"""Test wind direction conversion with None input."""
|
||||||
|
result = weather_fetcher.convert_wind_degrees_to_flow_direction(None)
|
||||||
|
assert result == 'N/A'
|
||||||
|
|
||||||
|
def test_get_wind_speed_indicator_safe(self, weather_fetcher):
|
||||||
|
"""Test wind speed indicator for safe level."""
|
||||||
|
result = weather_fetcher.get_wind_speed_indicator(15)
|
||||||
|
assert result == '🟢'
|
||||||
|
|
||||||
|
def test_get_wind_speed_indicator_caution(self, weather_fetcher):
|
||||||
|
"""Test wind speed indicator for caution level."""
|
||||||
|
result = weather_fetcher.get_wind_speed_indicator(30)
|
||||||
|
assert result == '🟡'
|
||||||
|
|
||||||
|
def test_get_wind_speed_indicator_alert(self, weather_fetcher):
|
||||||
|
"""Test wind speed indicator for alert level."""
|
||||||
|
result = weather_fetcher.get_wind_speed_indicator(50)
|
||||||
|
assert result == '🟠'
|
||||||
|
|
||||||
|
def test_get_wind_speed_indicator_dangerous(self, weather_fetcher):
|
||||||
|
"""Test wind speed indicator for dangerous level."""
|
||||||
|
result = weather_fetcher.get_wind_speed_indicator(65)
|
||||||
|
assert result == '🔴'
|
||||||
|
|
||||||
|
def test_get_wind_speed_indicator_none(self, weather_fetcher):
|
||||||
|
"""Test wind speed indicator with None input."""
|
||||||
|
result = weather_fetcher.get_wind_speed_indicator(None)
|
||||||
|
assert result == ''
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_low(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator for low level."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(2)
|
||||||
|
assert result == '🟢'
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_moderate(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator for moderate level."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(4)
|
||||||
|
assert result == '🟡'
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_high(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator for high level."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(6)
|
||||||
|
assert result == '🟠'
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_very_high(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator for very high level."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(9)
|
||||||
|
assert result == '🔴'
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_extreme(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator for extreme level."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(11)
|
||||||
|
assert result == '🟣'
|
||||||
|
|
||||||
|
def test_get_uv_index_indicator_none(self, weather_fetcher):
|
||||||
|
"""Test UV index indicator with None input."""
|
||||||
|
result = weather_fetcher.get_uv_index_indicator(None)
|
||||||
|
assert result == ''
|
||||||
|
|
||||||
|
@patch('hiking_assistant.weather.requests.get')
|
||||||
|
def test_get_weather_success(self, mock_get, weather_fetcher, mock_weather_response):
|
||||||
|
"""Test successful weather data retrieval."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = mock_weather_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = weather_fetcher.get_weather(25.0, 121.0)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert 'current_humidity' in result
|
||||||
|
assert 'daily_temp_max' in result
|
||||||
|
assert result['current_humidity'] == 75
|
||||||
|
assert len(result['daily_temp_max']) == 3
|
||||||
|
|
||||||
|
@patch('hiking_assistant.weather.requests.get')
|
||||||
|
def test_get_weather_request_exception(self, mock_get, weather_fetcher):
|
||||||
|
"""Test weather retrieval with request exception."""
|
||||||
|
mock_get.side_effect = requests.exceptions.RequestException('Connection error')
|
||||||
|
|
||||||
|
result = weather_fetcher.get_weather(25.0, 121.0)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
@patch('hiking_assistant.weather.requests.get')
|
||||||
|
def test_get_weather_json_parsing_error(self, mock_get, weather_fetcher):
|
||||||
|
"""Test weather retrieval with JSON parsing error."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.side_effect = ValueError('Invalid JSON')
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = weather_fetcher.get_weather(25.0, 121.0)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
@patch('hiking_assistant.weather.requests.get')
|
||||||
|
def test_get_weather_api_parameters(self, mock_get, weather_fetcher, mock_weather_response):
|
||||||
|
"""Test that correct API parameters are sent."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = mock_weather_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
weather_fetcher.get_weather(25.123, 121.456)
|
||||||
|
|
||||||
|
# Verify the call was made with correct parameters
|
||||||
|
mock_get.assert_called_once()
|
||||||
|
call_args = mock_get.call_args
|
||||||
|
params = call_args[1]['params']
|
||||||
|
|
||||||
|
assert params['latitude'] == 25.123
|
||||||
|
assert params['longitude'] == 121.456
|
||||||
|
assert params['timezone'] == 'auto'
|
||||||
|
assert params['forecast_days'] == 7
|
||||||
|
|
||||||
|
@patch('hiking_assistant.weather.requests.get')
|
||||||
|
def test_get_weather_timeout_parameter(self, mock_get, weather_fetcher, mock_weather_response):
|
||||||
|
"""Test that timeout parameter is used."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = mock_weather_response
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
weather_fetcher.get_weather(25.0, 121.0)
|
||||||
|
|
||||||
|
# Verify timeout was passed
|
||||||
|
call_args = mock_get.call_args
|
||||||
|
assert call_args[1]['timeout'] == 8
|
||||||
|
|
||||||
|
|
||||||
|
class TestWeatherFetcherEdgeCases:
|
||||||
|
"""Test edge cases for WeatherFetcher."""
|
||||||
|
|
||||||
|
def test_forecast_days_minimum_is_one(self):
|
||||||
|
"""Test that forecast_days is at least 1."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher(forecast_days=0)
|
||||||
|
assert fetcher.forecast_days == 1
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher(forecast_days=-5)
|
||||||
|
assert fetcher.forecast_days == 1
|
||||||
|
|
||||||
|
def test_wind_direction_boundary_values(self):
|
||||||
|
"""Test wind direction conversion at boundary values."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher()
|
||||||
|
|
||||||
|
# Test 360 degrees (same as 0)
|
||||||
|
result_0 = fetcher.convert_wind_degrees_to_flow_direction(0)
|
||||||
|
result_360 = fetcher.convert_wind_degrees_to_flow_direction(360)
|
||||||
|
assert result_0 == result_360
|
||||||
|
|
||||||
|
def test_wind_speed_boundary_values(self):
|
||||||
|
"""Test wind speed indicators at exact boundary values."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher()
|
||||||
|
|
||||||
|
assert fetcher.get_wind_speed_indicator(20) == '🟡' # Exactly 20
|
||||||
|
assert fetcher.get_wind_speed_indicator(40) == '🟠' # Exactly 40
|
||||||
|
assert fetcher.get_wind_speed_indicator(60) == '🔴' # Exactly 60
|
||||||
|
|
||||||
|
def test_uv_index_boundary_values(self):
|
||||||
|
"""Test UV index indicators at exact boundary values."""
|
||||||
|
from hiking_assistant.weather import WeatherFetcher
|
||||||
|
|
||||||
|
fetcher = WeatherFetcher()
|
||||||
|
|
||||||
|
assert fetcher.get_uv_index_indicator(2) == '🟢' # Exactly 2
|
||||||
|
assert fetcher.get_uv_index_indicator(5) == '🟡' # Exactly 5
|
||||||
|
assert fetcher.get_uv_index_indicator(7) == '🟠' # Exactly 7
|
||||||
|
assert fetcher.get_uv_index_indicator(10) == '🔴' # Exactly 10
|
||||||
Reference in New Issue
Block a user