Skip to content

Commit 1dd1cbb

Browse files
committed
Add PreFlightGoNoGo unit tests for Kp, sun, scoring, stations.
1 parent dd93fb2 commit 1dd1cbb

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

‎PreFlightGoNoGo/tests/test_kp.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
from preflight_go_no_go.kp import parse_kp_json
7+
8+
9+
def test_kp_quiet_latest(fixtures_dir: Path):
10+
data = json.loads((fixtures_dir / "kp_quiet.json").read_text())
11+
kp = parse_kp_json(data)
12+
assert kp is not None
13+
assert kp.kp_index == 1
14+
assert kp.estimated_kp == 1.33
15+
16+
17+
def test_kp_storm(fixtures_dir: Path):
18+
data = json.loads((fixtures_dir / "kp_storm.json").read_text())
19+
kp = parse_kp_json(data)
20+
assert kp is not None
21+
assert kp.kp_index == 7
22+
23+
24+
def test_kp_header_rows_style():
25+
payload = [
26+
["time_tag", "Kp", "a_running_average"],
27+
["2026-10-07 12:00:00", "3", "12"],
28+
["2026-10-07 15:00:00", "5", "22"],
29+
]
30+
kp = parse_kp_json(payload)
31+
assert kp is not None
32+
assert kp.kp_index == 5
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from datetime import datetime
5+
from pathlib import Path
6+
from zoneinfo import ZoneInfo
7+
8+
from preflight_go_no_go.kp import parse_kp_json
9+
from preflight_go_no_go.metar import parse_metar_list
10+
from preflight_go_no_go.models import Thresholds, Verdict
11+
from preflight_go_no_go.scoring import evaluate
12+
from preflight_go_no_go.sun import compute_sun_times
13+
14+
15+
def _day_now():
16+
return datetime(2026, 10, 7, 13, 0, tzinfo=ZoneInfo("America/New_York"))
17+
18+
19+
def test_vfr_quiet_is_fly(fixtures_dir: Path):
20+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_vfr.json").read_text()))
21+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_quiet.json").read_text()))
22+
primary = next(m for m in metars if m.icao == "KXMR")
23+
sun = compute_sun_times(28.39, -80.60, when=_day_now())
24+
report = evaluate(
25+
primary, [m for m in metars if m.icao != "KXMR"], kp, sun, Thresholds(),
26+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=_day_now(),
27+
)
28+
assert report.verdict == Verdict.FLY
29+
30+
31+
def test_mvfr_rain_is_marginal(fixtures_dir: Path):
32+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_mvfr.json").read_text()))
33+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_quiet.json").read_text()))
34+
primary = next(m for m in metars if m.icao == "KXMR")
35+
sun = compute_sun_times(28.39, -80.60, when=_day_now())
36+
report = evaluate(
37+
primary, [], kp, sun, Thresholds(),
38+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=_day_now(),
39+
)
40+
assert report.verdict == Verdict.MARGINAL
41+
weather = next(f for f in report.factors if f.name == "weather")
42+
assert weather.verdict == Verdict.MARGINAL
43+
44+
45+
def test_thunderstorm_is_dont_fly(fixtures_dir: Path):
46+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_thunderstorm.json").read_text()))
47+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_quiet.json").read_text()))
48+
sun = compute_sun_times(28.39, -80.60, when=_day_now())
49+
report = evaluate(
50+
metars[0], [], kp, sun, Thresholds(),
51+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=_day_now(),
52+
)
53+
assert report.verdict == Verdict.DONT_FLY
54+
55+
56+
def test_kp_storm_dont_fly_even_in_vfr(fixtures_dir: Path):
57+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_vfr.json").read_text()))
58+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_storm.json").read_text()))
59+
primary = next(m for m in metars if m.icao == "KXMR")
60+
sun = compute_sun_times(28.39, -80.60, when=_day_now())
61+
report = evaluate(
62+
primary, [], kp, sun, Thresholds(),
63+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=_day_now(),
64+
)
65+
assert report.verdict == Verdict.DONT_FLY
66+
kp_f = next(f for f in report.factors if f.name == "kp")
67+
assert kp_f.verdict == Verdict.DONT_FLY
68+
69+
70+
def test_kp_marginal(fixtures_dir: Path):
71+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_vfr.json").read_text()))
72+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_marginal.json").read_text()))
73+
primary = next(m for m in metars if m.icao == "KXMR")
74+
sun = compute_sun_times(28.39, -80.60, when=_day_now())
75+
report = evaluate(
76+
primary, [], kp, sun, Thresholds(),
77+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=_day_now(),
78+
)
79+
assert report.verdict == Verdict.MARGINAL
80+
81+
82+
def test_night_is_marginal_by_default(fixtures_dir: Path):
83+
metars = parse_metar_list(json.loads((fixtures_dir / "metar_vfr.json").read_text()))
84+
kp = parse_kp_json(json.loads((fixtures_dir / "kp_quiet.json").read_text()))
85+
primary = next(m for m in metars if m.icao == "KXMR")
86+
night = datetime(2026, 10, 7, 23, 0, tzinfo=ZoneInfo("America/New_York"))
87+
sun = compute_sun_times(28.39, -80.60, when=night)
88+
report = evaluate(
89+
primary, [], kp, sun, Thresholds(),
90+
location_label="KXMR", lat=28.39, lon=-80.60, generated_at=night,
91+
)
92+
assert report.verdict == Verdict.MARGINAL
93+
day_f = next(f for f in report.factors if f.name == "daylight")
94+
assert day_f.verdict == Verdict.MARGINAL
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from preflight_go_no_go.stations import icao_list_from_latlon, nearest_stations, pick_primary
4+
from preflight_go_no_go.metar import parse_metar_json
5+
6+
7+
def test_nearest_space_coast():
8+
near = nearest_stations(28.39, -80.60, n=3)
9+
codes = [s.icao for s in near]
10+
assert "KXMR" in codes
11+
assert near[0].distance_nm is not None
12+
assert near[0].distance_nm < near[-1].distance_nm
13+
14+
15+
def test_icao_list():
16+
codes = icao_list_from_latlon(28.39, -80.60, n=3)
17+
assert len(codes) == 3
18+
assert codes[0] == "KXMR"
19+
20+
21+
def test_pick_primary_preferred():
22+
a = parse_metar_json({"icaoId": "KCOF", "lat": 28.24, "lon": -80.61, "wspd": 5})
23+
b = parse_metar_json({"icaoId": "KXMR", "lat": 28.47, "lon": -80.56, "wspd": 5})
24+
primary, nearby = pick_primary([a, b], preferred_icao="KXMR")
25+
assert primary is not None and primary.icao == "KXMR"
26+
assert len(nearby) == 1

‎PreFlightGoNoGo/tests/test_sun.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from zoneinfo import ZoneInfo
5+
6+
from preflight_go_no_go.sun import compute_sun_times
7+
8+
9+
def test_space_coast_daylight_october():
10+
tz = ZoneInfo("America/New_York")
11+
# Midday EDT on 2026-10-07 near Cape Canaveral
12+
when = datetime(2026, 10, 7, 13, 0, tzinfo=tz)
13+
sun = compute_sun_times(28.39, -80.60, when=when, tz_name="America/New_York")
14+
assert sun.is_daylight
15+
assert sun.phase in {"day", "golden_morning", "golden_evening"}
16+
assert sun.sunrise.hour < 8 # ~7:xx EDT in early October FL
17+
assert sun.sunset.hour >= 18
18+
assert sun.civil_dawn < sun.sunrise < sun.golden_morning_end
19+
assert sun.golden_evening_start < sun.sunset < sun.civil_dusk
20+
21+
22+
def test_night_phase():
23+
tz = ZoneInfo("America/New_York")
24+
when = datetime(2026, 10, 7, 23, 30, tzinfo=tz)
25+
sun = compute_sun_times(28.39, -80.60, when=when)
26+
assert not sun.is_daylight
27+
assert sun.phase == "night"

0 commit comments

Comments
 (0)