Initial commit: weather-or-not scaffold
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
184
tests/test_activities.py
Normal file
184
tests/test_activities.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user