prop/test/microwaveprop/weather/untested_functions_test.exs
Graham McIntire e3d430f8c4
feat(weather): add duct cutoff band map layer
Show on /weather where the detected duct geometry traps each microwave
band. Overview mode bins cells by their lowest trapped frequency
(Bean & Dutton cutoff); band-picker mode masks cells whose duct doesn't
reach the selected band. Cutoff is pre-computed per cell in both
writers (Elixir derive + Rust derive_row reads best_duct_freq_ghz from
CellValues), persisted to ScalarFile, and shipped to the JS hook via
the existing binary cell pack. Also cleans up six pre-existing
length/1 credo warnings in unrelated test files.
2026-05-15 19:51:24 -05:00

530 lines
16 KiB
Elixir

defmodule Microwaveprop.Weather.UntestedFunctionsTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Repo
alias Microwaveprop.Weather
alias Microwaveprop.Weather.GefsProfile
alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.SolarIndex
alias Microwaveprop.Weather.Station
defp create_station(attrs \\ %{}) do
defaults = %{
station_code: "KDFW",
station_type: "asos",
name: "Dallas/Fort Worth Intl",
lat: 32.9,
lon: -97.0
}
{:ok, station} =
%Station{}
|> Station.changeset(Map.merge(defaults, attrs))
|> Repo.insert()
station
end
describe "upsert_solar_index/1" do
test "inserts a new solar index record" do
attrs = %{
date: ~D[2026-05-01],
sfi: 150.0,
sunspot_number: 80,
ap_index: 5
}
assert {:ok, %SolarIndex{} = idx} = Weather.upsert_solar_index(attrs)
assert idx.sfi == 150.0
assert idx.sunspot_number == 80
assert idx.date == ~D[2026-05-01]
end
test "updates existing record on conflict" do
Weather.upsert_solar_index(%{date: ~D[2026-05-02], sfi: 140.0, ap_index: 4})
assert {:ok, %SolarIndex{} = idx} = Weather.upsert_solar_index(%{date: ~D[2026-05-02], sfi: 145.0, ap_index: 4})
assert idx.sfi == 145.0
end
end
describe "station_day_pairs_covered/1" do
test "empty list returns empty MapSet" do
assert Weather.station_day_pairs_covered([]) == MapSet.new()
end
end
describe "station_ids_with_surface_observations/3" do
test "returns station ids that have observations in window" do
st = create_station()
{:ok, _obs} = Weather.upsert_surface_observation(st, %{observed_at: ~U[2026-05-01 12:00:00Z], temp_f: 75.0})
result = Weather.station_ids_with_surface_observations([st.id], ~U[2026-05-01 00:00:00Z], ~U[2026-05-02 00:00:00Z])
assert MapSet.member?(result, st.id)
end
test "excludes stations outside the time window" do
st = create_station(%{station_code: "KLAX"})
{:ok, _obs} = Weather.upsert_surface_observation(st, %{observed_at: ~U[2026-05-01 12:00:00Z], temp_f: 70.0})
result = Weather.station_ids_with_surface_observations([st.id], ~U[2026-06-01 00:00:00Z], ~U[2026-06-02 00:00:00Z])
assert MapSet.size(result) == 0
end
end
describe "upsert_hrrr_profile/1" do
test "inserts a new HRRR profile" do
now = DateTime.truncate(DateTime.utc_now(), :second)
profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}]
attrs = %{
valid_time: now,
run_time: now,
lat: 32.9,
lon: -97.0,
profile: profile_data,
hpbl_m: 1500.0,
pwat_mm: 25.0,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
surface_refractivity: 320.5,
min_refractivity_gradient: -45.0,
ducting_detected: false
}
assert {:ok, %HrrrProfile{} = profile} = Weather.upsert_hrrr_profile(attrs)
assert profile.lat == 32.9
assert profile.lon == -97.0
end
end
describe "upsert_gefs_profile/1" do
test "inserts a new GEFS profile" do
now = DateTime.truncate(DateTime.utc_now(), :second)
profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}]
attrs = %{
valid_time: now,
run_time: now,
forecast_hour: 0,
lat: 32.9,
lon: -97.0,
profile: profile_data,
pwat_mm: 25.0,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
surface_refractivity: 320.5,
min_refractivity_gradient: -45.0,
ducting_detected: false
}
assert {:ok, %GefsProfile{} = profile} = Weather.upsert_gefs_profile(attrs)
assert profile.forecast_hour == 0
assert profile.lat == 32.9
end
end
describe "upsert_gefs_profiles_batch/1" do
test "bulk inserts gefs profiles" do
now = DateTime.truncate(DateTime.utc_now(), :second)
profile_data = [%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0}]
profiles = [
%{
valid_time: now,
run_time: now,
forecast_hour: 0,
lat: 32.9,
lon: -97.0,
profile: profile_data,
pwat_mm: 25.0,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
surface_refractivity: 320.5,
min_refractivity_gradient: -45.0,
ducting_detected: false
}
]
assert {1, _} = Weather.upsert_gefs_profiles_batch(profiles)
end
end
describe "available_weather_valid_times/0" do
test "returns a list (empty when no data)" do
assert is_list(Weather.available_weather_valid_times())
end
end
describe "available_hrdps_valid_times/0" do
test "returns a list (empty when no data)" do
assert is_list(Weather.available_hrdps_valid_times())
end
end
describe "round_to_hrrr_grid/2" do
test "snaps coordinates to HRRR 3km grid" do
{lat, lon} = Weather.round_to_hrrr_grid(32.91, -97.04)
assert is_float(lat)
assert is_float(lon)
end
end
describe "round_to_iemre_grid/2" do
test "snaps coordinates to IEMRE 0.125 degree grid" do
{lat, lon} = Weather.round_to_iemre_grid(32.91, -97.04)
assert is_float(lat)
assert is_float(lon)
end
end
describe "nil guard functions" do
test "hrrr_data_fully_present?/1 returns false for nil pos1" do
refute Weather.hrrr_data_fully_present?(%{pos1: nil})
end
test "hrrr_data_fully_present?/1 returns false for nil qso_timestamp" do
refute Weather.hrrr_data_fully_present?(%{pos1: %{"lat" => 32.9, "lon" => -97.0}, qso_timestamp: nil})
end
test "hrrr_for_contact/1 returns nil for nil pos1" do
assert Weather.hrrr_for_contact(%{pos1: nil}) == nil
end
test "narr_for_contact/1 returns nil for nil pos1" do
assert Weather.narr_for_contact(%{pos1: nil}) == nil
end
test "iemre_for_contact/1 returns nil for nil pos1" do
assert Weather.iemre_for_contact(%{pos1: nil}) == nil
end
test "iemre_for_path/1 returns empty for nil pos1" do
assert Weather.iemre_for_path(%{pos1: nil}) == []
end
test "narr_profiles_for_path/1 returns empty for nil pos1" do
assert Weather.narr_profiles_for_path(%{pos1: nil}) == []
end
test "hrrr_profiles_for_path/1 returns empty for nil pos1" do
assert Weather.hrrr_profiles_for_path(%{pos1: nil}) == []
end
end
describe "has_hrrr_profile?/3" do
test "returns false when no profile exists" do
refute Weather.has_hrrr_profile?(32.9, -97.0, DateTime.utc_now())
end
end
describe "has_iemre_observation?/3" do
test "returns false when no observation exists" do
refute Weather.has_iemre_observation?(32.9, -97.0, ~D[2026-05-01])
end
end
describe "purge_grid_point_profiles/0" do
test "returns a count (0 when no data)" do
result = Weather.purge_grid_point_profiles()
assert is_integer(result)
assert result >= 0
end
end
describe "find_or_create_station/1" do
test "creates a new station when not present" do
assert {:ok, %Station{} = st} =
Weather.find_or_create_station(%{
station_code: "KAUS",
station_type: "asos",
name: "Austin",
lat: 30.2,
lon: -97.7
})
assert st.station_code == "KAUS"
end
test "returns existing station on duplicate" do
{:ok, st1} =
Weather.find_or_create_station(%{
station_code: "KORD",
station_type: "asos",
name: "Chicago",
lat: 41.9,
lon: -87.9
})
{:ok, st2} =
Weather.find_or_create_station(%{
station_code: "KORD",
station_type: "asos",
name: "Chicago",
lat: 41.9,
lon: -87.9
})
assert st1.id == st2.id
end
test "string-keyed attrs work too" do
assert {:ok, %Station{}} =
Weather.find_or_create_station(%{
"station_code" => "KSEA",
"station_type" => "asos",
"name" => "Seattle",
"lat" => 47.4,
"lon" => -122.3
})
end
end
describe "has_surface_observations?/3" do
test "returns false when no obs in window" do
st = create_station(%{station_code: "KSAN"})
refute Weather.has_surface_observations?(
st.id,
~U[2026-01-01 00:00:00Z],
~U[2026-01-02 00:00:00Z]
)
end
test "returns true when an obs exists in window" do
st = create_station(%{station_code: "KSFO"})
{:ok, _} =
Weather.upsert_surface_observation(st, %{
observed_at: ~U[2026-05-01 12:00:00Z],
temp_f: 70.0
})
assert Weather.has_surface_observations?(
st.id,
~U[2026-05-01 00:00:00Z],
~U[2026-05-02 00:00:00Z]
)
end
end
describe "station_day_covered?/2" do
test "false when no obs on that day" do
st = create_station(%{station_code: "KMIA"})
refute Weather.station_day_covered?(st.id, ~D[2026-05-01])
end
test "true after upserting an obs on that day" do
st = create_station(%{station_code: "KBOS"})
{:ok, _} =
Weather.upsert_surface_observation(st, %{
observed_at: ~U[2026-05-01 14:00:00Z],
temp_f: 60.0
})
assert Weather.station_day_covered?(st.id, ~D[2026-05-01])
end
end
describe "station_day_pairs_covered/1 with rows" do
test "returns the covered subset" do
st = create_station(%{station_code: "KPHX"})
{:ok, _} =
Weather.upsert_surface_observation(st, %{
observed_at: ~U[2026-05-01 06:00:00Z],
temp_f: 80.0
})
result = Weather.station_day_pairs_covered([{st.id, ~D[2026-05-01]}, {st.id, ~D[2026-05-02]}])
assert MapSet.member?(result, {st.id, ~D[2026-05-01]})
end
end
describe "has_sounding?/2" do
test "returns false when no sounding exists" do
st = create_station(%{station_code: "KFWD", station_type: "sounding"})
refute Weather.has_sounding?(st.id, ~U[2026-05-01 12:00:00Z])
end
end
describe "station_ids_with_soundings/2" do
test "empty result when none match" do
st = create_station(%{station_code: "KOUN", station_type: "sounding"})
result = Weather.station_ids_with_soundings([st.id], [~U[2026-05-01 12:00:00Z]])
assert MapSet.size(result) == 0
end
end
describe "get_solar_index/1" do
test "returns nil when none present" do
assert Weather.get_solar_index(~D[2026-05-01]) == nil
end
test "returns the row after upsert" do
{:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-03], sfi: 130.0})
idx = Weather.get_solar_index(~D[2026-05-03])
assert idx
assert idx.sfi == 130.0
end
end
describe "existing_solar_dates/0" do
test "empty MapSet when DB is empty" do
assert Weather.existing_solar_dates() == MapSet.new()
end
test "contains dates after upserts" do
{:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-04], sfi: 120.0})
{:ok, _} = Weather.upsert_solar_index(%{date: ~D[2026-05-05], sfi: 121.0})
dates = Weather.existing_solar_dates()
assert MapSet.member?(dates, ~D[2026-05-04])
assert MapSet.member?(dates, ~D[2026-05-05])
end
end
describe "upsert_solar_indices_batch/1" do
test "empty list returns 0" do
assert Weather.upsert_solar_indices_batch([]) == 0
end
test "inserts multiple rows" do
records = [
%{date: ~D[2026-06-01], sfi: 100.0, ap_index: 5},
%{date: ~D[2026-06-02], sfi: 105.0, ap_index: 6}
]
count = Weather.upsert_solar_indices_batch(records)
assert count == 2
end
end
describe "nearby_stations/4" do
test "returns empty when no stations in range" do
assert Weather.nearby_stations(0.0, 0.0, "asos", 10.0) == []
end
test "finds station within radius" do
_ = create_station(%{station_code: "KDFW2", lat: 32.9, lon: -97.0})
results = Weather.nearby_stations(32.9, -97.0, "asos", 50.0)
assert results != []
end
end
describe "sounding_times_around/1" do
test "returns a list of DateTime values" do
times = Weather.sounding_times_around(~U[2026-05-01 12:00:00Z])
assert is_list(times)
assert Enum.all?(times, fn t -> match?(%DateTime{}, t) end)
end
end
describe "latest_grid_valid_time/0" do
test "returns nil when no rows" do
assert Weather.latest_grid_valid_time() == nil
end
end
describe "find_nearest_hrrr/3" do
test "returns nil when no profile exists" do
assert Weather.find_nearest_hrrr(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil
end
end
describe "find_nearest_native_profile/3" do
test "returns nil when no profile exists" do
assert Weather.find_nearest_native_profile(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil
end
end
describe "find_nearest_iemre/3" do
test "returns nil when no observation exists" do
assert Weather.find_nearest_iemre(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil
end
end
describe "find_nearest_narr/3" do
test "returns nil when no profile exists" do
assert Weather.find_nearest_narr(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil
end
end
describe "nearest_native_duct_ghz/3" do
test "returns nil when no native profile" do
assert Weather.nearest_native_duct_ghz(32.9, -97.0, ~U[2026-05-01 12:00:00Z]) == nil
end
end
describe "nearest_native_duct_info/3" do
test "returns an error tuple when no native profile" do
assert match?({:error, _}, Weather.nearest_native_duct_info(32.9, -97.0, ~U[2026-05-01 12:00:00Z]))
end
end
describe "nearest_sounding_to/4" do
test "returns an error tuple when no soundings near point" do
assert match?({:error, _}, Weather.nearest_sounding_to(32.9, -97.0, ~U[2026-05-01 12:00:00Z]))
end
end
describe "best_profile_for_contact/1" do
test "returns nil for contact missing pos1" do
assert Weather.best_profile_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-05-01 12:00:00Z]}) == nil
end
end
describe "profiles_along_path/1" do
test "returns empty list for contact missing pos1" do
assert Weather.profiles_along_path(%{pos1: nil, pos2: %{"lat" => 33.0, "lon" => -97.0}}) == []
end
end
describe "upsert_iemre_observation/1" do
test "inserts a new IEMRE observation" do
attrs = %{
date: ~D[2026-05-01],
lat: 32.9,
lon: -97.0,
hourly: [
%{"hour" => 0, "air_temp_f" => 65.0}
]
}
assert {:ok, _obs} = Weather.upsert_iemre_observation(attrs)
end
end
describe "backfill_hrrr_scalars/1" do
test "runs on empty DB without raising" do
result = Weather.backfill_hrrr_scalars(batch_size: 5, max_batches: 1)
assert is_integer(result) or match?({:ok, _}, result) or result == :ok
end
end
describe "analyze_all/1" do
test "runs on empty DB without raising" do
_ = Weather.analyze_all(skip_recent_seconds: 0)
assert true
end
end
describe "reconcile_*_statuses (smoke)" do
test "reconcile_iemre_statuses returns count tuple on empty DB" do
assert {:ok, count} = Weather.reconcile_iemre_statuses()
assert is_integer(count)
end
test "reconcile_hrrr_statuses returns count tuple on empty DB" do
assert {:ok, count} = Weather.reconcile_hrrr_statuses()
assert is_integer(count)
end
test "reconcile_weather_statuses returns count tuple on empty DB" do
assert {:ok, count} = Weather.reconcile_weather_statuses()
assert is_integer(count)
end
end
end