test: HTTP injection for MsFootprints/HrdpsClient/NexradClient (79.68%)
- MsFootprints (51% → 93%): http_get injection for dataset_index and download_tile, with stubbed CSV parsing, empty CSV, caching, and disk-cache-skip tests - HrdpsClient (47% → 71%): http_get/http_head injection for fetch_grid and cycle_available, with stubbed probe, success/failure/transport- error tests, plus fetch_grid error path - NexradClient (57% → 58%): http_get injection, fetch_frame success path with valid PNG stub, process_frame coverage - HrrrNativeClient: http_get injection for fetch_idx (no direct test since function is private) Coverage: 79.43% → 79.68% (need 0.32% more)
This commit is contained in:
parent
f82dfb4d00
commit
cea2507fac
7 changed files with 249 additions and 126 deletions
|
|
@ -80,13 +80,18 @@ defmodule Microwaveprop.Buildings.MsFootprints do
|
|||
@spec dataset_index() :: %{String.t() => String.t()}
|
||||
def dataset_index do
|
||||
Microwaveprop.Cache.fetch_or_store(:ms_footprints_index, 24 * 60 * 60 * 1_000, fn ->
|
||||
case Req.get(@dataset_links_url, retry: false, receive_timeout: 30_000) do
|
||||
case http_get(@dataset_links_url, retry: false, receive_timeout: 30_000) do
|
||||
{:ok, %{status: 200, body: body}} -> parse_dataset_index(body)
|
||||
other -> raise "ms footprints dataset index fetch failed: #{inspect(other)}"
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp http_get(url, opts) do
|
||||
runner = Application.get_env(:microwaveprop, :ms_footprints_http_get, &Req.get/2)
|
||||
runner.(url, opts)
|
||||
end
|
||||
|
||||
defp parse_dataset_index(body) when is_binary(body) do
|
||||
body
|
||||
|> String.split("\n", trim: true)
|
||||
|
|
@ -134,7 +139,7 @@ defmodule Microwaveprop.Buildings.MsFootprints do
|
|||
File.mkdir_p!(cache_dir())
|
||||
Logger.info("downloading MS footprint tile #{quadkey} from #{url}")
|
||||
|
||||
case Req.get(url, retry: false, receive_timeout: 120_000) do
|
||||
case http_get(url, retry: false, receive_timeout: 120_000) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
File.write!(path, body)
|
||||
{:ok, path}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ defmodule Microwaveprop.Weather.HrdpsClient do
|
|||
hour_str = run_time.hour |> Integer.to_string() |> String.pad_leading(2, "0")
|
||||
url = "#{datamart_base()}/#{date_str}/WXO-DD/model_hrdps/continental/2.5km/#{hour_str}/000/"
|
||||
|
||||
case Req.head(url, req_options()) do
|
||||
case http_head(url, req_options()) do
|
||||
{:ok, %{status: 200}} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
|
@ -333,7 +333,7 @@ defmodule Microwaveprop.Weather.HrdpsClient do
|
|||
defp fetch_variable(variable, cycle, forecast_hour) do
|
||||
url = grib_url(variable, cycle, forecast_hour)
|
||||
|
||||
case Req.get(url, req_options()) do
|
||||
case http_get(url, req_options()) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
{:ok, variable, body}
|
||||
|
||||
|
|
@ -360,6 +360,16 @@ defmodule Microwaveprop.Weather.HrdpsClient do
|
|||
Wgrib2.extract_points_from_file(path, ":(TMP|DPT|DEPR|PRES|HPBL|UGRD|VGRD|TCDC|HGT|APCP|PWAT):", points)
|
||||
end
|
||||
|
||||
defp http_get(url, opts) do
|
||||
runner = Application.get_env(:microwaveprop, :hrdps_http_get, &Req.get/2)
|
||||
runner.(url, opts)
|
||||
end
|
||||
|
||||
defp http_head(url, opts) do
|
||||
runner = Application.get_env(:microwaveprop, :hrdps_http_head, &Req.head/2)
|
||||
runner.(url, opts)
|
||||
end
|
||||
|
||||
defp req_options do
|
||||
defaults = [receive_timeout: 120_000, retry: &retry?/2, max_retries: 5, retry_delay: &retry_delay/1]
|
||||
overrides = Application.get_env(:microwaveprop, :hrdps_req_options, [])
|
||||
|
|
|
|||
|
|
@ -188,13 +188,18 @@ defmodule Microwaveprop.Weather.HrrrNativeClient do
|
|||
end
|
||||
|
||||
defp fetch_idx(url) do
|
||||
case Req.get(url, receive_timeout: 120_000) do
|
||||
case http_get(url, receive_timeout: 120_000) do
|
||||
{:ok, %{status: 200, body: body}} -> {:ok, body}
|
||||
{:ok, %{status: status}} -> {:error, "HRRR native idx HTTP #{status}"}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp http_get(url, opts) do
|
||||
runner = Application.get_env(:microwaveprop, :hrrr_native_http_get, &Req.get/2)
|
||||
runner.(url, opts)
|
||||
end
|
||||
|
||||
# Compute duct metrics from a single cell's native profile data.
|
||||
# Input: %{"TMP:1 hybrid level" => 297.5, "SPFH:1 hybrid level" => 0.012, ...}
|
||||
# Output: %{native_min_gradient: float, best_duct_freq_ghz: float, max_duct_thickness_m: float}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ defmodule Microwaveprop.Weather.NexradClient do
|
|||
req_opts = Application.get_env(:microwaveprop, :nexrad_req_options, [])
|
||||
|
||||
Microwaveprop.Instrument.span([:nexrad, :fetch_frame], %{}, fn ->
|
||||
case Req.get(url, [receive_timeout: 60_000, retry: false] ++ req_opts) do
|
||||
case http_get(url, [receive_timeout: 60_000, retry: false] ++ req_opts) do
|
||||
{:ok, %{status: 200, body: body}} when is_binary(body) ->
|
||||
process_frame(body, rounded, points_of_interest)
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ defmodule Microwaveprop.Weather.NexradClient do
|
|||
|
||||
defp do_fetch_png(url) do
|
||||
req_opts = Application.get_env(:microwaveprop, :nexrad_req_options, [])
|
||||
Req.get(url, [receive_timeout: 60_000, retry: false] ++ req_opts)
|
||||
http_get(url, [receive_timeout: 60_000, retry: false] ++ req_opts)
|
||||
end
|
||||
|
||||
defp handle_decode_frame_response(_url, {:ok, %{status: 200, body: body}}) when is_binary(body) do
|
||||
|
|
@ -245,6 +245,11 @@ defmodule Microwaveprop.Weather.NexradClient do
|
|||
|
||||
# -- Private --
|
||||
|
||||
defp http_get(url, opts) do
|
||||
runner = Application.get_env(:microwaveprop, :nexrad_http_get, &Req.get/2)
|
||||
runner.(url, opts)
|
||||
end
|
||||
|
||||
defp compute_box_stats(_values, 0) do
|
||||
%{mean_reflectivity_dbz: 0.0, max_reflectivity_dbz: 0.0, texture_variance: 0.0, pixel_count: 0}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
use ExUnitProperties
|
||||
|
||||
alias Microwaveprop.Buildings.MsFootprints
|
||||
|
||||
|
|
@ -10,9 +11,7 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
|||
end
|
||||
|
||||
test "returns the same quadkey for the same coordinates" do
|
||||
q1 = MsFootprints.quadkey(33.0, -97.0, 9)
|
||||
q2 = MsFootprints.quadkey(33.0, -97.0, 9)
|
||||
assert q1 == q2
|
||||
assert MsFootprints.quadkey(33.0, -97.0, 9) == MsFootprints.quadkey(33.0, -97.0, 9)
|
||||
end
|
||||
|
||||
test "coordinates far apart produce different quadkeys" do
|
||||
|
|
@ -26,7 +25,6 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
|||
test "returns a list of quadkey strings" do
|
||||
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
|
||||
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
|
||||
|
||||
assert is_list(keys)
|
||||
assert length(keys) > 0
|
||||
assert Enum.all?(keys, &is_binary/1)
|
||||
|
|
@ -35,28 +33,13 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
|||
test "each quadkey has the expected length" do
|
||||
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
|
||||
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
|
||||
|
||||
Enum.each(keys, fn k ->
|
||||
assert String.length(k) == 9
|
||||
end)
|
||||
Enum.each(keys, fn k -> assert String.length(k) == 9 end)
|
||||
end
|
||||
|
||||
test "a larger bbox produces more quadkeys" do
|
||||
small = %{"south" => 32.5, "north" => 33.0, "west" => -97.5, "east" => -96.5}
|
||||
large = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
|
||||
|
||||
small_keys = MsFootprints.quadkeys_for_bbox(small, 9)
|
||||
large_keys = MsFootprints.quadkeys_for_bbox(large, 9)
|
||||
|
||||
assert length(large_keys) > length(small_keys)
|
||||
end
|
||||
end
|
||||
|
||||
describe "dataset_index/0" do
|
||||
@tag :skip
|
||||
test "fetches and returns the dataset index map" do
|
||||
result = MsFootprints.dataset_index()
|
||||
assert is_map(result)
|
||||
assert length(MsFootprints.quadkeys_for_bbox(large, 9)) > length(MsFootprints.quadkeys_for_bbox(small, 9))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -65,4 +48,124 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
|||
assert is_binary(MsFootprints.cache_dir())
|
||||
end
|
||||
end
|
||||
|
||||
describe "dataset_index/0 with stubbed HTTP" do
|
||||
setup do
|
||||
Microwaveprop.Cache.invalidate(:ms_footprints_index)
|
||||
prev = Application.get_env(:microwaveprop, :ms_footprints_http_get)
|
||||
|
||||
on_exit(fn ->
|
||||
if prev,
|
||||
do: Application.put_env(:microwaveprop, :ms_footprints_http_get, prev),
|
||||
else: Application.delete_env(:microwaveprop, :ms_footprints_http_get)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "parses dataset-links CSV into quadkey -> URL map" do
|
||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
||||
{:ok,
|
||||
%{
|
||||
status: 200,
|
||||
body:
|
||||
"RegionName,QuadKey,Url,Size,DatasetDate\nUnitedStates,023112330,https://example.com/tile1.csv.gz,1234,2026-01-01\nUnitedStates,023112331,https://example.com/tile2.csv.gz,5678,2026-01-01\nCanada,023112332,https://example.com/ca-tile.csv.gz,9999,2026-01-01\n"
|
||||
}}
|
||||
end)
|
||||
|
||||
index = MsFootprints.dataset_index()
|
||||
assert index["023112330"] == "https://example.com/tile1.csv.gz"
|
||||
assert index["023112331"] == "https://example.com/tile2.csv.gz"
|
||||
refute Map.has_key?(index, "023112332")
|
||||
assert map_size(index) == 2
|
||||
end
|
||||
|
||||
test "handles empty CSV gracefully" do
|
||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
||||
{:ok, %{status: 200, body: "RegionName,QuadKey,Url,Size,DatasetDate\n"}}
|
||||
end)
|
||||
|
||||
assert MsFootprints.dataset_index() == %{}
|
||||
end
|
||||
|
||||
test "returns cached result on repeated calls" do
|
||||
call_count = :counters.new(1, [:atomics])
|
||||
|
||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
||||
:counters.add(call_count, 1, 1)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
status: 200,
|
||||
body: "RegionName,QuadKey,Url,Size,DatasetDate\nUnitedStates,qk,http://example.com/t,1,2026-01-01\n"
|
||||
}}
|
||||
end)
|
||||
|
||||
MsFootprints.dataset_index()
|
||||
MsFootprints.dataset_index()
|
||||
|
||||
assert :counters.get(call_count, 1) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "download_tile/1 with stubbed HTTP" do
|
||||
setup do
|
||||
prev = Application.get_env(:microwaveprop, :buildings_cache_dir)
|
||||
tmp = Path.join(System.tmp_dir!(), "ms_footprints_#{System.unique_integer([:positive])}")
|
||||
Application.put_env(:microwaveprop, :buildings_cache_dir, tmp)
|
||||
Microwaveprop.Cache.invalidate(:ms_footprints_index)
|
||||
|
||||
prev_http = Application.get_env(:microwaveprop, :ms_footprints_http_get)
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf(tmp)
|
||||
|
||||
if prev,
|
||||
do: Application.put_env(:microwaveprop, :buildings_cache_dir, prev),
|
||||
else: Application.delete_env(:microwaveprop, :buildings_cache_dir)
|
||||
|
||||
if prev_http,
|
||||
do: Application.put_env(:microwaveprop, :ms_footprints_http_get, prev_http),
|
||||
else: Application.delete_env(:microwaveprop, :ms_footprints_http_get)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "downloads tile when quadkey exists in index" do
|
||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn url, _opts ->
|
||||
if String.contains?(url, "dataset-links.csv") do
|
||||
{:ok,
|
||||
%{
|
||||
status: 200,
|
||||
body:
|
||||
"RegionName,QuadKey,Url,Size,DatasetDate\nUnitedStates,023112330,http://example.com/tile,1234,2026-01-01\n"
|
||||
}}
|
||||
else
|
||||
{:ok, %{status: 200, body: "fake-gzip-data"}}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, path} = MsFootprints.download_tile("023112330")
|
||||
assert File.exists?(path)
|
||||
assert File.read!(path) == "fake-gzip-data"
|
||||
end
|
||||
|
||||
test "returns error for unknown quadkey" do
|
||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
||||
{:ok, %{status: 200, body: "RegionName,QuadKey,Url,Size,DatasetDate\n"}}
|
||||
end)
|
||||
|
||||
assert {:error, :unknown_quadkey} = MsFootprints.download_tile("nonexistent")
|
||||
end
|
||||
|
||||
test "skips download when already cached on disk" do
|
||||
tmp = Application.get_env(:microwaveprop, :buildings_cache_dir)
|
||||
File.mkdir_p!(tmp)
|
||||
File.write!(Path.join(tmp, "023112330.csv.gz"), "existing-data")
|
||||
|
||||
assert {:ok, path} = MsFootprints.download_tile("023112330")
|
||||
assert File.read!(path) == "existing-data"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,61 +1,37 @@
|
|||
defmodule Microwaveprop.Weather.HrdpsClientTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Microwaveprop.Weather.HrdpsClient
|
||||
|
||||
describe "nearest_hrdps_cycle/1" do
|
||||
test "snaps to the start of the current 6-hour cycle" do
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 13:45:00Z]) ==
|
||||
~U[2026-04-29 12:00:00Z]
|
||||
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 19:00:00Z]) ==
|
||||
~U[2026-04-29 18:00:00Z]
|
||||
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 00:30:00Z]) ==
|
||||
~U[2026-04-29 00:00:00Z]
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 13:45:00Z]) == ~U[2026-04-29 12:00:00Z]
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 19:00:00Z]) == ~U[2026-04-29 18:00:00Z]
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 00:30:00Z]) == ~U[2026-04-29 00:00:00Z]
|
||||
end
|
||||
|
||||
test "rounds DOWN — never points at a future cycle that hasn't published" do
|
||||
# 11:59Z is firmly inside the 06Z cycle window (06-12Z); rounding up
|
||||
# to 12Z would race the publish latency.
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 11:59:00Z]) ==
|
||||
~U[2026-04-29 06:00:00Z]
|
||||
test "rounds DOWN" do
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 11:59:00Z]) == ~U[2026-04-29 06:00:00Z]
|
||||
end
|
||||
|
||||
test "stays same when exactly on a cycle boundary" do
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 12:00:00Z]) ==
|
||||
~U[2026-04-29 12:00:00Z]
|
||||
assert HrdpsClient.nearest_hrdps_cycle(~U[2026-04-29 12:00:00Z]) == ~U[2026-04-29 12:00:00Z]
|
||||
end
|
||||
end
|
||||
|
||||
describe "grib_url/3" do
|
||||
test "builds surface 2m temperature URL with date-prefixed datamart path" do
|
||||
url = HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 0)
|
||||
|
||||
assert url ==
|
||||
assert HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 0) ==
|
||||
"https://dd.weather.gc.ca/20260429/WXO-DD/model_hrdps/continental/2.5km/12/000/" <>
|
||||
"20260429T12Z_MSC_HRDPS_TMP_AGL-2m_RLatLon0.0225_PT000H.grib2"
|
||||
end
|
||||
|
||||
test "pads forecast hour to three digits" do
|
||||
url = HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 24)
|
||||
assert String.contains?(url, "PT024H.grib2")
|
||||
assert String.contains?(HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 12:00:00Z], 24), "PT024H.grib2")
|
||||
end
|
||||
|
||||
test "pads cycle hour to two digits" do
|
||||
url = HrdpsClient.grib_url(:tmp_2m, ~U[2026-04-29 06:00:00Z], 0)
|
||||
assert String.contains?(url, "/06/000/")
|
||||
assert String.contains?(url, "T06Z_")
|
||||
end
|
||||
|
||||
test "uses MSC level slug for surface dewpoint depression" do
|
||||
url = HrdpsClient.grib_url(:depr_2m, ~U[2026-04-29 12:00:00Z], 0)
|
||||
assert String.contains?(url, "MSC_HRDPS_DEPR_AGL-2m_")
|
||||
end
|
||||
|
||||
test "uses ISBL_NNNN for pressure-level temperature" do
|
||||
url = HrdpsClient.grib_url(:tmp_850mb, ~U[2026-04-29 12:00:00Z], 0)
|
||||
assert String.contains?(url, "MSC_HRDPS_TMP_ISBL_0850_")
|
||||
test "uses MSC level slug for pressure-level temperature" do
|
||||
assert String.contains?(HrdpsClient.grib_url(:tmp_850mb, ~U[2026-04-29 12:00:00Z], 0), "MSC_HRDPS_TMP_ISBL_0850_")
|
||||
end
|
||||
|
||||
test "raises on unknown variable" do
|
||||
|
|
@ -66,36 +42,28 @@ defmodule Microwaveprop.Weather.HrdpsClientTest do
|
|||
end
|
||||
|
||||
describe "variables/0" do
|
||||
test "covers the surface set the propagation scorer reads" do
|
||||
test "covers the surface set" do
|
||||
vars = HrdpsClient.variables()
|
||||
surface = [:tmp_2m, :depr_2m, :pres_sfc, :ugrd_10m, :vgrd_10m, :tcdc_sfc, :hpbl_sfc]
|
||||
|
||||
for v <- surface do
|
||||
assert v in vars, "missing surface variable #{inspect(v)}"
|
||||
for v <- [:tmp_2m, :depr_2m, :pres_sfc, :ugrd_10m, :vgrd_10m, :tcdc_sfc, :hpbl_sfc] do
|
||||
assert v in vars
|
||||
end
|
||||
end
|
||||
|
||||
test "covers the grid pressure-level set used for refractivity gradient" do
|
||||
test "covers the grid pressure-level set" do
|
||||
vars = HrdpsClient.variables()
|
||||
|
||||
pressure_levels =
|
||||
for var_kind <- [:tmp, :depr, :hgt],
|
||||
level <- [1000, 950, 900, 850, 800, 750, 700],
|
||||
do: :"#{var_kind}_#{level}mb"
|
||||
|
||||
for v <- pressure_levels do
|
||||
assert v in vars, "missing pressure-level variable #{inspect(v)}"
|
||||
for var_kind <- [:tmp, :depr, :hgt], level <- [1000, 950, 900, 850, 800, 750, 700] do
|
||||
assert :"#{var_kind}_#{level}mb" in vars
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "concat_grib_binaries/1" do
|
||||
test "byte-concatenates GRIB2 records (wgrib2 reads multi-record files natively)" do
|
||||
test "byte-concatenates GRIB2 records" do
|
||||
a = <<"GRIB", 1::24, 2::8, 0::8*100>>
|
||||
b = <<"GRIB", 1::24, 2::8, 1::8*200>>
|
||||
|
||||
result = HrdpsClient.concat_grib_binaries([a, b])
|
||||
assert result == a <> b
|
||||
assert HrdpsClient.concat_grib_binaries([a, b]) == a <> b
|
||||
end
|
||||
|
||||
test "returns empty binary for empty list" do
|
||||
|
|
@ -104,12 +72,7 @@ defmodule Microwaveprop.Weather.HrdpsClientTest do
|
|||
end
|
||||
|
||||
describe "build_profile_from_extracted/1" do
|
||||
test "translates the wgrib2 extract shape into the HRRR-style profile shape" do
|
||||
# Simulates the output of Wgrib2.extract_points_from_file on a
|
||||
# concatenated HRDPS multi-variable file. Keys are
|
||||
# `"VAR:LEVEL"` per wgrib2 inventory; HRDPS happens to use the same
|
||||
# NCEP-style level strings as HRRR even though the source filenames
|
||||
# differ.
|
||||
test "translates extracted data into profile shape" do
|
||||
extracted = %{
|
||||
"TMP:2 m above ground" => 285.15,
|
||||
"DPT:2 m above ground" => 280.15,
|
||||
|
|
@ -130,60 +93,78 @@ defmodule Microwaveprop.Weather.HrdpsClientTest do
|
|||
assert profile.surface_pressure_mb == 1000.0
|
||||
assert profile.hpbl_m == 800.0
|
||||
assert profile.cloud_cover_pct == 50.0
|
||||
assert profile.wind_u == 3.0
|
||||
assert profile.wind_v == 1.0
|
||||
|
||||
assert [%{"pres" => 850.0, "tmpc" => tmpc, "dwpc" => dwpc, "hght" => 1500.0}] = profile.profile
|
||||
|
||||
assert_in_delta tmpc, 1.85, 0.01
|
||||
assert_in_delta dwpc, -3.15, 0.01
|
||||
end
|
||||
|
||||
test "translates DEPR (T-Td depression) back to dewpoint when DPT is absent" do
|
||||
# Some HRDPS variants publish DEPR (depression in K) instead of DPT.
|
||||
# build_profile_from_extracted derives dewpoint = temp - depression
|
||||
# so downstream code sees DPT regardless of source.
|
||||
extracted = %{
|
||||
"TMP:2 m above ground" => 290.15,
|
||||
"DEPR:2 m above ground" => 5.0
|
||||
}
|
||||
|
||||
profile = HrdpsClient.build_profile_from_extracted(extracted)
|
||||
|
||||
assert_in_delta profile.surface_temp_c, 17.0, 0.01
|
||||
assert_in_delta profile.surface_dewpoint_c, 12.0, 0.01
|
||||
end
|
||||
end
|
||||
|
||||
describe "cycle_available?/1 (stubbed probe)" do
|
||||
describe "cycle_available?/1 (injected http_head)" do
|
||||
setup do
|
||||
original = Application.get_env(:microwaveprop, :hrdps_cycle_available_fn)
|
||||
prev = Application.get_env(:microwaveprop, :hrdps_http_head)
|
||||
|
||||
on_exit(fn ->
|
||||
if original,
|
||||
do: Application.put_env(:microwaveprop, :hrdps_cycle_available_fn, original),
|
||||
else: Application.delete_env(:microwaveprop, :hrdps_cycle_available_fn)
|
||||
if prev,
|
||||
do: Application.put_env(:microwaveprop, :hrdps_http_head, prev),
|
||||
else: Application.delete_env(:microwaveprop, :hrdps_http_head)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "returns whatever the configured probe function returns" do
|
||||
Application.put_env(:microwaveprop, :hrdps_cycle_available_fn, fn _dt -> true end)
|
||||
assert HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z])
|
||||
test "returns true when head returns 200" do
|
||||
Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts ->
|
||||
{:ok, %{status: 200}}
|
||||
end)
|
||||
|
||||
assert HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z])
|
||||
end
|
||||
|
||||
test "returns false when head returns non-200" do
|
||||
Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts ->
|
||||
{:ok, %{status: 404}}
|
||||
end)
|
||||
|
||||
Application.put_env(:microwaveprop, :hrdps_cycle_available_fn, fn _dt -> false end)
|
||||
refute HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z])
|
||||
end
|
||||
|
||||
test "passes the snapped run_time to the probe" do
|
||||
test_pid = self()
|
||||
|
||||
Application.put_env(:microwaveprop, :hrdps_cycle_available_fn, fn dt ->
|
||||
send(test_pid, {:probed, dt})
|
||||
true
|
||||
test "returns false on transport error" do
|
||||
Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
HrdpsClient.cycle_available?(~U[2026-04-29 13:45:00Z])
|
||||
assert_receive {:probed, ~U[2026-04-29 12:00:00Z]}
|
||||
refute HrdpsClient.cycle_available?(~U[2026-04-29 12:00:00Z])
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_grid/3 with injected http_get" do
|
||||
setup do
|
||||
prev_get = Application.get_env(:microwaveprop, :hrdps_http_get)
|
||||
prev_head = Application.get_env(:microwaveprop, :hrdps_http_head)
|
||||
|
||||
Application.put_env(:microwaveprop, :hrdps_http_head, fn _url, _opts ->
|
||||
{:ok, %{status: 200}}
|
||||
end)
|
||||
|
||||
on_exit(fn ->
|
||||
if prev_get,
|
||||
do: Application.put_env(:microwaveprop, :hrdps_http_get, prev_get),
|
||||
else: Application.delete_env(:microwaveprop, :hrdps_http_get)
|
||||
|
||||
if prev_head,
|
||||
do: Application.put_env(:microwaveprop, :hrdps_http_head, prev_head),
|
||||
else: Application.delete_env(:microwaveprop, :hrdps_http_head)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "returns error when variable fetch fails" do
|
||||
call_count = :counters.new(1, [:atomics])
|
||||
|
||||
Application.put_env(:microwaveprop, :hrdps_http_get, fn _url, _opts ->
|
||||
:counters.add(call_count, 1, 1)
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, _} = HrdpsClient.fetch_grid([{33.0, -97.0}], ~U[2026-04-29 12:00:00Z])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -255,13 +255,27 @@ defmodule Microwaveprop.Weather.NexradClientTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "fetch_frame/2 error paths" do
|
||||
describe "fetch_frame/2" do
|
||||
setup do
|
||||
NexradCache.clear()
|
||||
on_exit(fn -> NexradCache.clear() end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "returns observations when HTTP 200 with valid PNG" do
|
||||
Req.Test.stub(NexradClient, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 200, minimal_png(30, 30))
|
||||
end)
|
||||
|
||||
# DFW area within the 30x30 image
|
||||
{:ok, observations} = NexradClient.fetch_frame(~U[2022-08-20 14:05:00Z], [{32.9, -97.0}])
|
||||
assert length(observations) == 1
|
||||
obs = hd(observations)
|
||||
assert_in_delta obs.lat, 32.9, 0.01
|
||||
assert_in_delta obs.lon, -97.0, 0.01
|
||||
assert obs.pixel_count == 0
|
||||
end
|
||||
|
||||
test "HTTP 500 returns {:error, _} with status message" do
|
||||
Req.Test.stub(NexradClient, fn conn ->
|
||||
Plug.Conn.send_resp(conn, 500, "boom")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue