mirror of
https://github.com/acamarata/pray-calc-ml.git
synced 2026-06-30 19:04:26 +00:00
173 lines
5.9 KiB
Python
173 lines
5.9 KiB
Python
"""
|
|
Tests for src/collect/analysis/sightings_stats.py
|
|
|
|
Verifies angle_summary, geographic_coverage, and print_dataset_report
|
|
return correct types and values. Plot functions are tested for return type only
|
|
(no display).
|
|
"""
|
|
|
|
import sys
|
|
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
import pandas as pd
|
|
from datetime import datetime, timezone
|
|
|
|
from src.collect.analysis.sightings_stats import (
|
|
angle_summary,
|
|
geographic_coverage,
|
|
plot_angle_distribution,
|
|
plot_angle_vs_latitude,
|
|
plot_angle_vs_day_of_year,
|
|
print_dataset_report,
|
|
)
|
|
|
|
|
|
def _make_fajr_df(n=10):
|
|
"""Build a minimal Fajr DataFrame with day_of_year and fajr_angle."""
|
|
rows = []
|
|
for i in range(n):
|
|
day = (i * 36) + 1 # spread across the year
|
|
dt = datetime(2023, 1, 1, 4, 0, tzinfo=timezone.utc)
|
|
rows.append({
|
|
"date": f"2023-01-{i+1:02d}",
|
|
"utc_dt": dt,
|
|
"lat": 30.0 + i * 0.5,
|
|
"lng": 29.0,
|
|
"elevation_m": 100.0,
|
|
"prayer": "fajr",
|
|
"fajr_angle": 12.0 + i * 0.3,
|
|
"day_of_year": day,
|
|
"source": f"Source {i}",
|
|
"notes": "test",
|
|
})
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def _make_isha_df(n=5):
|
|
"""Build a minimal Isha DataFrame."""
|
|
rows = []
|
|
for i in range(n):
|
|
rows.append({
|
|
"date": f"2023-03-{i+1:02d}",
|
|
"utc_dt": datetime(2023, 3, i+1, 19, 0, tzinfo=timezone.utc),
|
|
"lat": 25.0 + i,
|
|
"lng": 46.0,
|
|
"elevation_m": 620.0,
|
|
"prayer": "isha",
|
|
"isha_angle": 17.0 + i * 0.2,
|
|
"day_of_year": 60 + i,
|
|
"source": f"Isha Source {i}",
|
|
"notes": "test",
|
|
})
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
class TestAngleSummary:
|
|
def test_returns_series(self):
|
|
"""angle_summary must return a pandas Series."""
|
|
df = _make_fajr_df()
|
|
result = angle_summary(df, "fajr_angle")
|
|
assert isinstance(result, pd.Series)
|
|
|
|
def test_count_is_correct(self):
|
|
"""count in summary must equal number of non-NaN angle rows."""
|
|
df = _make_fajr_df(10)
|
|
result = angle_summary(df, "fajr_angle")
|
|
assert result["count"] == 10.0
|
|
|
|
def test_mean_is_plausible(self):
|
|
"""Mean angle must be within the data range."""
|
|
df = _make_fajr_df(10)
|
|
result = angle_summary(df, "fajr_angle")
|
|
assert df["fajr_angle"].min() <= result["mean"] <= df["fajr_angle"].max()
|
|
|
|
def test_isha_angle_col(self):
|
|
"""Works correctly with isha_angle column."""
|
|
df = _make_isha_df()
|
|
result = angle_summary(df, "isha_angle")
|
|
assert result["count"] == 5.0
|
|
|
|
|
|
class TestGeographicCoverage:
|
|
def test_returns_dict(self):
|
|
"""geographic_coverage must return a dict."""
|
|
df = _make_fajr_df()
|
|
result = geographic_coverage(df)
|
|
assert isinstance(result, dict)
|
|
|
|
def test_required_keys_present(self):
|
|
"""All expected keys must be in the result dict."""
|
|
df = _make_fajr_df()
|
|
result = geographic_coverage(df)
|
|
for key in ("lat_min", "lat_max", "unique_locs", "date_min", "date_max", "total_records"):
|
|
assert key in result, f"Missing key: {key}"
|
|
|
|
def test_total_records_correct(self):
|
|
"""total_records must equal len(df)."""
|
|
df = _make_fajr_df(7)
|
|
result = geographic_coverage(df)
|
|
assert result["total_records"] == 7
|
|
|
|
def test_lat_range_correct(self):
|
|
"""lat_min and lat_max must match DataFrame lat column bounds."""
|
|
df = _make_fajr_df(10)
|
|
result = geographic_coverage(df)
|
|
assert abs(result["lat_min"] - df["lat"].min()) < 1e-6
|
|
assert abs(result["lat_max"] - df["lat"].max()) < 1e-6
|
|
|
|
def test_unique_locs_counts_groups(self):
|
|
"""unique_locs must count distinct (lat, lng) pairs."""
|
|
df = _make_fajr_df(10)
|
|
expected = len(df.groupby(["lat", "lng"]))
|
|
result = geographic_coverage(df)
|
|
assert result["unique_locs"] == expected
|
|
|
|
|
|
class TestPrintDatasetReport:
|
|
def test_runs_without_error(self, capsys):
|
|
"""print_dataset_report must run to completion without exceptions."""
|
|
fajr = _make_fajr_df(5)
|
|
isha = _make_isha_df(3)
|
|
print_dataset_report(fajr, isha)
|
|
out = capsys.readouterr().out
|
|
assert "Fajr dataset: 5" in out
|
|
assert "Isha dataset: 3" in out
|
|
|
|
def test_empty_datasets(self, capsys):
|
|
"""print_dataset_report handles empty DataFrames without crashing."""
|
|
fajr = pd.DataFrame(columns=["fajr_angle"])
|
|
isha = pd.DataFrame(columns=["isha_angle"])
|
|
print_dataset_report(fajr, isha)
|
|
out = capsys.readouterr().out
|
|
assert "Fajr dataset: 0" in out
|
|
assert "Isha dataset: 0" in out
|
|
|
|
|
|
class TestPlotFunctions:
|
|
def test_plot_angle_distribution_returns_figure(self):
|
|
"""plot_angle_distribution must return a matplotlib Figure."""
|
|
import matplotlib
|
|
matplotlib.use("Agg") # non-interactive backend for tests
|
|
df = _make_fajr_df(10)
|
|
fig = plot_angle_distribution(df, "fajr_angle")
|
|
import matplotlib.figure
|
|
assert isinstance(fig, matplotlib.figure.Figure)
|
|
|
|
def test_plot_angle_vs_latitude_returns_figure(self):
|
|
"""plot_angle_vs_latitude must return a matplotlib Figure."""
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
df = _make_fajr_df(10)
|
|
fig = plot_angle_vs_latitude(df, "fajr_angle")
|
|
import matplotlib.figure
|
|
assert isinstance(fig, matplotlib.figure.Figure)
|
|
|
|
def test_plot_angle_vs_day_of_year_returns_figure(self):
|
|
"""plot_angle_vs_day_of_year must return a matplotlib Figure."""
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
df = _make_fajr_df(10)
|
|
fig = plot_angle_vs_day_of_year(df, "fajr_angle")
|
|
import matplotlib.figure
|
|
assert isinstance(fig, matplotlib.figure.Figure)
|