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/.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 "list_valid_times/0" do test "returns all persisted valid_times sorted ascending" do assert ProfilesFile.list_valid_times() == [] for vt <- [~U[2026-04-14 18:00:00Z], ~U[2026-04-14 10: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.list_valid_times() == [ ~U[2026-04-14 10:00:00Z], ~U[2026-04-14 14:00:00Z], ~U[2026-04-14 18:00:00Z] ] 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