Extend the f00 profile persistence to cover every forecast hour (f00-f18) so /weather keeps working across pod restarts and shows forecast-hour atmospheric data without a fresh database. Route the Weather cold path (latest_grid_valid_time, load_weather_grid, weather_point_detail) through ProfilesFile first, with the legacy hrrr_profiles table as a last-resort historical fallback. Warm GridCache from the latest profile on app start so /weather is hot the moment a pod boots.
135 lines
4.6 KiB
Elixir
135 lines
4.6 KiB
Elixir
defmodule Microwaveprop.Propagation.ProfilesFileTest do
|
|
# async: false — tests mutate the global Application env to redirect
|
|
# the scores dir to a private tmp tree.
|
|
use ExUnit.Case, async: false
|
|
|
|
alias Microwaveprop.Propagation.ProfilesFile
|
|
|
|
setup do
|
|
dir =
|
|
Path.join(
|
|
System.tmp_dir!(),
|
|
"profiles_file_test_#{System.unique_integer([:positive])}"
|
|
)
|
|
|
|
File.mkdir_p!(dir)
|
|
|
|
prev = Application.get_env(:microwaveprop, :propagation_scores_dir)
|
|
Application.put_env(:microwaveprop, :propagation_scores_dir, dir)
|
|
|
|
on_exit(fn ->
|
|
File.rm_rf!(dir)
|
|
|
|
if prev do
|
|
Application.put_env(:microwaveprop, :propagation_scores_dir, prev)
|
|
else
|
|
Application.delete_env(:microwaveprop, :propagation_scores_dir)
|
|
end
|
|
end)
|
|
|
|
%{dir: dir}
|
|
end
|
|
|
|
describe "path_for/1" do
|
|
test "nests under base_dir/profiles/<iso>.etf.gz", %{dir: dir} do
|
|
assert ProfilesFile.path_for(~U[2026-04-14 18:00:00Z]) ==
|
|
Path.join([dir, "profiles", "2026-04-14T18:00:00Z.etf.gz"])
|
|
end
|
|
end
|
|
|
|
describe "read/1" do
|
|
test "returns the full grid_data map round-tripped through ETF" do
|
|
valid_time = ~U[2026-04-14 18:00:00Z]
|
|
|
|
grid_data = %{
|
|
{32.75, -97.125} => %{surface_temp_c: 25.0, surface_dewpoint_c: 15.0},
|
|
{33.0, -97.0} => %{surface_temp_c: 26.0}
|
|
}
|
|
|
|
ProfilesFile.write!(valid_time, grid_data)
|
|
|
|
assert {:ok, round_tripped} = ProfilesFile.read(valid_time)
|
|
assert round_tripped[{32.75, -97.125}][:surface_temp_c] == 25.0
|
|
assert round_tripped[{33.0, -97.0}][:surface_temp_c] == 26.0
|
|
end
|
|
|
|
test "returns :enoent when no file exists" do
|
|
assert ProfilesFile.read(~U[2026-04-14 18:00:00Z]) == {:error, :enoent}
|
|
end
|
|
end
|
|
|
|
describe "latest_valid_time/0" do
|
|
test "returns the most recent persisted valid_time" do
|
|
assert ProfilesFile.latest_valid_time() == nil
|
|
|
|
for vt <- [~U[2026-04-14 10:00:00Z], ~U[2026-04-14 18:00:00Z], ~U[2026-04-14 14:00:00Z]] do
|
|
ProfilesFile.write!(vt, %{{25.0, -125.0} => %{surface_temp_c: 1.0}})
|
|
end
|
|
|
|
assert ProfilesFile.latest_valid_time() == ~U[2026-04-14 18:00:00Z]
|
|
end
|
|
end
|
|
|
|
describe "write!/2 + read_point/3" do
|
|
test "round-trips a point and snaps lat/lon to the 0.125° grid" do
|
|
valid_time = ~U[2026-04-14 18:00:00Z]
|
|
|
|
grid_data = %{
|
|
{32.75, -97.125} => %{surface_temp_c: 25.0, surface_dewpoint_c: 15.0, wind_u: 3.0, wind_v: -1.0},
|
|
{25.0, -125.0} => %{surface_temp_c: 10.0}
|
|
}
|
|
|
|
ProfilesFile.write!(valid_time, grid_data)
|
|
|
|
# Exact match
|
|
assert %{surface_temp_c: 25.0, wind_u: 3.0} =
|
|
ProfilesFile.read_point(valid_time, 32.75, -97.125)
|
|
|
|
# Nudged within snap tolerance still resolves to the same cell
|
|
assert %{surface_temp_c: 25.0} =
|
|
ProfilesFile.read_point(valid_time, 32.7, -97.1)
|
|
|
|
# A point with no stored profile returns nil
|
|
assert ProfilesFile.read_point(valid_time, 40.0, -80.0) == nil
|
|
end
|
|
|
|
test "returns nil for a missing valid_time" do
|
|
assert ProfilesFile.read_point(~U[2026-04-14 18:00:00Z], 32.75, -97.125) == nil
|
|
end
|
|
end
|
|
|
|
describe "prune_older_than/1" do
|
|
test "deletes files with valid_time strictly before the cutoff" do
|
|
old = ~U[2026-04-14 10:00:00Z]
|
|
keep = ~U[2026-04-14 18:00:00Z]
|
|
|
|
ProfilesFile.write!(old, %{{25.0, -125.0} => %{surface_temp_c: 1.0}})
|
|
ProfilesFile.write!(keep, %{{25.0, -125.0} => %{surface_temp_c: 2.0}})
|
|
|
|
assert 1 == ProfilesFile.prune_older_than(~U[2026-04-14 12:00:00Z])
|
|
assert ProfilesFile.read_point(old, 25.0, -125.0) == nil
|
|
assert %{surface_temp_c: 2.0} = ProfilesFile.read_point(keep, 25.0, -125.0)
|
|
end
|
|
end
|
|
|
|
describe "retain_window/2" do
|
|
test "keeps only files whose valid_time is inside [run_time, run_time + max_fh * 3600]" do
|
|
run_time = ~U[2026-04-14 18:00:00Z]
|
|
|
|
inside_f00 = ~U[2026-04-14 18:00:00Z]
|
|
inside_f10 = ~U[2026-04-15 04:00:00Z]
|
|
after_window = ~U[2026-04-15 13:00:00Z]
|
|
before_window = ~U[2026-04-14 10:00:00Z]
|
|
|
|
for vt <- [inside_f00, inside_f10, after_window, before_window] do
|
|
ProfilesFile.write!(vt, %{{25.0, -125.0} => %{surface_temp_c: 1.0}})
|
|
end
|
|
|
|
assert 2 == ProfilesFile.retain_window(run_time, 18)
|
|
assert %{surface_temp_c: 1.0} = ProfilesFile.read_point(inside_f00, 25.0, -125.0)
|
|
assert %{surface_temp_c: 1.0} = ProfilesFile.read_point(inside_f10, 25.0, -125.0)
|
|
assert ProfilesFile.read_point(after_window, 25.0, -125.0) == nil
|
|
assert ProfilesFile.read_point(before_window, 25.0, -125.0) == nil
|
|
end
|
|
end
|
|
end
|