185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
"""Tests for activity evaluators."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from weather_or_not.activities import DataStore, Verdict
|
|
from weather_or_not.motorcycle import MotorcycleActivity
|
|
from weather_or_not.lawn import LawnActivity
|
|
from weather_or_not.weather import WeatherForecast
|
|
|
|
|
|
def _make_weather(
|
|
temp: float = 20,
|
|
wind: float = 10,
|
|
humidity: float = 50,
|
|
weather_code: int = 0,
|
|
precip_today: float = 0,
|
|
precip_24h: float = 0,
|
|
precip_2h: float = 0,
|
|
) -> WeatherForecast:
|
|
"""Create a minimal WeatherForecast for testing."""
|
|
return WeatherForecast(
|
|
latitude=40.7128,
|
|
longitude=-74.0060,
|
|
generationtime_ms=10.0,
|
|
utc_offset_seconds=-14400,
|
|
timezone="America/New_York",
|
|
timezone_abbreviation="EDT",
|
|
elevation=10.0,
|
|
current={
|
|
"temperature_4m": temp,
|
|
"wind_speed_10m": wind,
|
|
"relative_humidity_2m": humidity,
|
|
"weather_code": weather_code,
|
|
"precipitation": 0,
|
|
"is_day": 1,
|
|
"pressure_msl": 1013,
|
|
},
|
|
hourly={
|
|
"time": [
|
|
"2026-04-29T10:00:00",
|
|
"2026-04-29T11:00:00",
|
|
"2026-04-29T12:00:00",
|
|
],
|
|
"temperature_2m": [temp, temp + 1, temp + 2],
|
|
"relative_humidity_2m": [humidity, humidity - 5, humidity - 10],
|
|
"precipitation": [0, 0, 0],
|
|
"wind_speed_10m": [wind, wind - 1, wind - 2],
|
|
"weather_code": [0, 0, 0],
|
|
"apparent_temperature": [temp, temp + 1, temp + 2],
|
|
},
|
|
daily={
|
|
"time": ["2026-04-29"],
|
|
"precipitation_sum": [precip_today],
|
|
"temperature_2m_max": [temp + 5],
|
|
"temperature_2m_min": [temp - 5],
|
|
},
|
|
)
|
|
|
|
|
|
class TestDataStore:
|
|
def test_never_done(self, tmp_path):
|
|
store = DataStore(tmp_path)
|
|
assert store.days_since_activity("mow_lawn") == 999
|
|
|
|
def test_record_and_check(self, tmp_path):
|
|
store = DataStore(tmp_path)
|
|
store.record_activity_performed("mow_lawn", date(2026, 4, 25))
|
|
assert store.days_since_activity("mow_lawn") == 4 # Apr 29 - Apr 25
|
|
|
|
def test_stats(self, tmp_path):
|
|
store = DataStore(tmp_path)
|
|
store.record_activity_performed("mow_lawn", date(2026, 4, 25))
|
|
stats = store.get_activity_stats("mow_lawn")
|
|
assert stats["times_performed"] == 1
|
|
assert stats["last_performed"] == "2026-04-25"
|
|
|
|
def test_persistence(self, tmp_path):
|
|
"""Data persists across DataStore instances."""
|
|
store1 = DataStore(tmp_path)
|
|
store1.record_activity_performed("mow_lawn", date(2026, 4, 20))
|
|
del store1
|
|
|
|
store2 = DataStore(tmp_path)
|
|
assert store2.last_activity_date("mow_lawn") == date(2026, 4, 20)
|
|
|
|
|
|
class TestMotorcycleActivity:
|
|
def setup_method(self):
|
|
self.config = {
|
|
"min_temp_c": 10,
|
|
"max_temp_c": 35,
|
|
"max_precipitation_mm": 0,
|
|
"max_wind_speed_kmh": 30,
|
|
"max_humidity_pct": 90,
|
|
"no_ice": True,
|
|
"no_wet_roads": True,
|
|
}
|
|
self.data_store = DataStore(Path("/tmp/test-won-data"))
|
|
self.activity = MotorcycleActivity(self.config, self.data_store)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_good_conditions(self):
|
|
weather = _make_weather(temp=22, wind=10, humidity=50, weather_code=0)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.GO
|
|
assert all(r.passed for r in result.rules)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rain(self):
|
|
weather = _make_weather(precip_today=5)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
assert any(not r.passed and "precip" in r.rule_name.lower()
|
|
for r in result.rules)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wind_too_strong(self):
|
|
weather = _make_weather(wind=40)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
assert any(not r.passed and "wind" in r.rule_name.lower()
|
|
for r in result.rules)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_temp_too_cold(self):
|
|
weather = _make_weather(temp=5)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_thunderstorm(self):
|
|
weather = _make_weather(weather_code=95)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
|
|
|
|
class TestLawnActivity:
|
|
def setup_method(self):
|
|
self.config = {
|
|
"cooldown_days": 3,
|
|
"max_precipitation_today_mm": 0,
|
|
"max_precipitation_past_24h_mm": 5,
|
|
"max_wind_speed_kmh": 25,
|
|
"min_temp_c": 15,
|
|
}
|
|
self.data_store = DataStore(Path("/tmp/test-lawn-data"))
|
|
self.activity = LawnActivity(self.config, self.data_store)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_good_conditions(self):
|
|
# Ensure cooldown is met
|
|
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
|
|
weather = _make_weather(temp=22, wind=10)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.GO
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cooldown_not_met(self):
|
|
# Just mowed yesterday
|
|
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 28))
|
|
weather = _make_weather(temp=22, wind=10)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
assert any(not r.passed and "cooldown" in r.rule_name.lower()
|
|
for r in result.rules)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ground_wet(self):
|
|
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
|
|
weather = _make_weather(precip_24h=10)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rain_today(self):
|
|
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
|
|
weather = _make_weather(precip_today=3)
|
|
result = await self.activity.evaluate(weather)
|
|
assert result.verdict == Verdict.NO_GO
|