Adds ~600 new test cases (2013 → 2613 tests; 170 properties; 0
failures) across 12 new test files plus expansions of eight existing
files. Big lifts per module:
ContactLive.Mechanism 33 → 100%
MetricsPlug 54 → ~100%
Microwaveprop.Release 15.8 → 70%+
Telemetry 38 → 76%
HrrrNativeGridWorker 27.7 → 76%
ContactLive.Show 34 → 48% (handler + render branches)
Admin.ContactEditLive 49.7 → 70%+
GefsFetchWorker 35.8 → ~55%
IonosphereFetchWorker 56.3 → 75%
PathLive 58.9 → 67%
WeatherMapLive 66.9 → 80.2%
RoverLive 0 → 70.3%
ContactMapLive 59.2 → 89.8%
ContactMapController 0 → 100%
Mix tasks (Rust.Golden, Notebook, Backtest, HrrrBackfill,
HrrrClimatology, HrrrNativeBackfill, NexradBackfill,
RadarBackfill, ImportContestLogs, PropagationGrid,
ResetEnrichment, Hrrr.PurgeGridPoints) 0 → ~60-100%
Two incidental fixes made while adding tests:
- ContactLive.Show.handle_event("toggle_flag", ...) was passing
socket.assigns.current_scope to admin?/1 instead of socket.assigns,
so admins never matched the pattern and every toggle ran the
"Admins only." flash branch. Flag now toggles for admins again.
- Commercial.PollWorker.fetch_weather/1 promoted from private to
@doc'd public so the IEM-fetch + ASOS-upsert path can be tested
directly without driving perform/1 through live SNMP (which was
timing out for ~50 s per test in the earlier attempt).
Stable property-test additions cover the dewpoint-from-RH monotonicity,
nearest_run/1 idempotence, and the ionosphere station envelope.
214 lines
6.2 KiB
Elixir
214 lines
6.2 KiB
Elixir
defmodule Microwaveprop.WeatherExtraTest do
|
|
@moduledoc """
|
|
Fills in coverage holes on lesser-used `Microwaveprop.Weather`
|
|
context helpers: has_surface_observations?/3, has_sounding?/2,
|
|
the two `station_ids_with_*` set-membership helpers,
|
|
`existing_solar_dates/0`, `sounding_times_around/1`, and
|
|
`hrrr_data_fully_present?/1`.
|
|
|
|
Each is driven with a small, deterministic fixture — we care about
|
|
the boolean / set return contracts rather than large real-world
|
|
data shapes.
|
|
"""
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather
|
|
alias Microwaveprop.Weather.Sounding
|
|
alias Microwaveprop.Weather.SurfaceObservation
|
|
|
|
defp station!(attrs \\ %{}) do
|
|
base = %{
|
|
station_code: "TST#{System.unique_integer([:positive])}",
|
|
station_type: "asos",
|
|
name: "Test Station",
|
|
lat: 33.0,
|
|
lon: -97.0
|
|
}
|
|
|
|
{:ok, s} = Weather.find_or_create_station(Map.merge(base, attrs))
|
|
s
|
|
end
|
|
|
|
describe "has_surface_observations?/3" do
|
|
test "true when any observation falls inside the window" do
|
|
station = station!()
|
|
|
|
{:ok, _} =
|
|
%SurfaceObservation{}
|
|
|> SurfaceObservation.changeset(%{
|
|
station_id: station.id,
|
|
observed_at: ~U[2026-04-20 12:00:00Z],
|
|
temp_f: 72.0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
assert Weather.has_surface_observations?(
|
|
station.id,
|
|
~U[2026-04-20 00:00:00Z],
|
|
~U[2026-04-20 23:59:59Z]
|
|
)
|
|
end
|
|
|
|
test "false when the window excludes every observation" do
|
|
station = station!()
|
|
|
|
{:ok, _} =
|
|
%SurfaceObservation{}
|
|
|> SurfaceObservation.changeset(%{
|
|
station_id: station.id,
|
|
observed_at: ~U[2026-04-20 12:00:00Z],
|
|
temp_f: 72.0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
refute Weather.has_surface_observations?(
|
|
station.id,
|
|
~U[2026-04-21 00:00:00Z],
|
|
~U[2026-04-21 23:59:59Z]
|
|
)
|
|
end
|
|
|
|
test "false for a station with no observations" do
|
|
station = station!()
|
|
refute Weather.has_surface_observations?(station.id, ~U[2026-04-20 00:00:00Z], ~U[2026-04-20 23:59:59Z])
|
|
end
|
|
end
|
|
|
|
describe "has_sounding?/2" do
|
|
test "true when a sounding exists at the exact observed_at" do
|
|
station = station!(%{station_type: "sounding"})
|
|
|
|
{:ok, _} =
|
|
%Sounding{}
|
|
|> Sounding.changeset(%{
|
|
station_id: station.id,
|
|
observed_at: ~U[2026-04-20 12:00:00Z],
|
|
profile: [],
|
|
level_count: 0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
assert Weather.has_sounding?(station.id, ~U[2026-04-20 12:00:00Z])
|
|
end
|
|
|
|
test "false when no sounding exists at the given time" do
|
|
station = station!(%{station_type: "sounding"})
|
|
refute Weather.has_sounding?(station.id, ~U[2026-04-20 12:00:00Z])
|
|
end
|
|
end
|
|
|
|
describe "station_ids_with_* set-membership helpers" do
|
|
test "station_ids_with_surface_observations returns only the stations that have rows in the window" do
|
|
a = station!()
|
|
b = station!()
|
|
c = station!()
|
|
|
|
{:ok, _} =
|
|
%SurfaceObservation{}
|
|
|> SurfaceObservation.changeset(%{
|
|
station_id: a.id,
|
|
observed_at: ~U[2026-04-20 12:00:00Z],
|
|
temp_f: 60.0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
{:ok, _} =
|
|
%SurfaceObservation{}
|
|
|> SurfaceObservation.changeset(%{
|
|
station_id: b.id,
|
|
observed_at: ~U[2026-04-20 13:00:00Z],
|
|
temp_f: 70.0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
present =
|
|
Weather.station_ids_with_surface_observations(
|
|
[a.id, b.id, c.id],
|
|
~U[2026-04-20 00:00:00Z],
|
|
~U[2026-04-20 23:59:59Z]
|
|
)
|
|
|
|
assert MapSet.size(present) == 2
|
|
assert MapSet.member?(present, a.id)
|
|
assert MapSet.member?(present, b.id)
|
|
refute MapSet.member?(present, c.id)
|
|
end
|
|
|
|
test "station_ids_with_soundings returns {station_id, observed_at} tuples" do
|
|
station = station!(%{station_type: "sounding"})
|
|
|
|
{:ok, _} =
|
|
%Sounding{}
|
|
|> Sounding.changeset(%{
|
|
station_id: station.id,
|
|
observed_at: ~U[2026-04-20 12:00:00Z],
|
|
profile: [],
|
|
level_count: 0
|
|
})
|
|
|> Repo.insert()
|
|
|
|
set =
|
|
Weather.station_ids_with_soundings(
|
|
[station.id],
|
|
[~U[2026-04-20 12:00:00Z], ~U[2026-04-20 00:00:00Z]]
|
|
)
|
|
|
|
assert MapSet.member?(set, {station.id, ~U[2026-04-20 12:00:00Z]})
|
|
refute MapSet.member?(set, {station.id, ~U[2026-04-20 00:00:00Z]})
|
|
end
|
|
end
|
|
|
|
describe "sounding_times_around/1" do
|
|
# Radiosondes fly twice a day (00Z, 12Z). The helper returns the
|
|
# two nearest bracketing times for a given wall-clock UTC.
|
|
test "early-morning times bracket prev-day 12Z and same-day 00Z" do
|
|
times = Weather.sounding_times_around(~U[2026-04-20 06:00:00Z])
|
|
|
|
assert length(times) >= 2
|
|
assert ~U[2026-04-19 12:00:00Z] in times
|
|
assert ~U[2026-04-20 00:00:00Z] in times
|
|
end
|
|
|
|
test "afternoon times bracket same-day 00Z/12Z" do
|
|
times = Weather.sounding_times_around(~U[2026-04-20 18:00:00Z])
|
|
assert ~U[2026-04-20 00:00:00Z] in times
|
|
assert ~U[2026-04-20 12:00:00Z] in times
|
|
end
|
|
|
|
test "noon UTC lands on the 12Z bracket" do
|
|
times = Weather.sounding_times_around(~U[2026-04-20 12:00:00Z])
|
|
assert ~U[2026-04-20 12:00:00Z] in times
|
|
end
|
|
end
|
|
|
|
describe "hrrr_data_fully_present?/1" do
|
|
test "false when pos1 is missing" do
|
|
refute Weather.hrrr_data_fully_present?(%{pos1: nil})
|
|
end
|
|
|
|
test "false when qso_timestamp is missing" do
|
|
refute Weather.hrrr_data_fully_present?(%{qso_timestamp: nil, pos1: %{"lat" => 32.9, "lon" => -97.0}})
|
|
end
|
|
end
|
|
|
|
describe "existing_solar_dates/0" do
|
|
test "returns a MapSet of Dates for every row in solar_indices" do
|
|
date = ~D[2026-04-20]
|
|
|
|
Weather.upsert_solar_indices_batch([
|
|
%{
|
|
date: date,
|
|
sfi: 120.0,
|
|
sfi_adjusted: 118.0,
|
|
sunspot_number: 55,
|
|
ap_index: 5,
|
|
kp_values: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
|
|
}
|
|
])
|
|
|
|
set = Weather.existing_solar_dates()
|
|
assert MapSet.member?(set, date)
|
|
end
|
|
end
|
|
end
|