test: enable :slow tests by default + add coverage tests across modules
Removed the `exclude: [:slow]` from test_helper.exs — the slow-tagged wgrib2 fixture tests pass with the binary on PATH and pull Microwaveprop.Weather.Grib2.Wgrib2 from 23% to 90%+ in coverage runs. Additional tests: - Pskr.Client: tcp/tcp_closed/tcp_error stale-socket fall-throughs - Weather context: weather_point_detail/3, latest_grid_valid_time/0, available_weather_valid_times/0, available_hrdps_valid_times/0 - PathCompute.compute/4: with HRRR profile near the path - RoverPathProfileWorker: real Path row + skip-when-complete - RoverLive: URL stations encoding, add_station error paths - PathLive: rover_path_id branches (404, malformed UUID) 3599 tests, 0 failures.
This commit is contained in:
parent
888701e627
commit
363646f34f
6 changed files with 242 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ defmodule Microwaveprop.Propagation.PathComputeTest do
|
||||||
|
|
||||||
alias Microwaveprop.Propagation.BandConfig
|
alias Microwaveprop.Propagation.BandConfig
|
||||||
alias Microwaveprop.Propagation.PathCompute
|
alias Microwaveprop.Propagation.PathCompute
|
||||||
|
alias Microwaveprop.Weather.HrrrProfile
|
||||||
|
|
||||||
describe "compute_loss_budget/5" do
|
describe "compute_loss_budget/5" do
|
||||||
test "fspl increases with distance" do
|
test "fspl increases with distance" do
|
||||||
|
|
@ -196,6 +197,42 @@ defmodule Microwaveprop.Propagation.PathComputeTest do
|
||||||
assert result.loss_budget.h2o > 0
|
assert result.loss_budget.h2o > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "with HRRR profile data + sounding near the path uses real data" do
|
||||||
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||||
|
|
||||||
|
profile_data = [
|
||||||
|
%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0},
|
||||||
|
%{"pres" => 925.0, "hght" => 770, "tmpc" => 18.0, "dwpc" => 12.0},
|
||||||
|
%{"pres" => 850.0, "hght" => 1500, "tmpc" => 14.0, "dwpc" => 8.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Insert an HRRR profile near the midpoint of (32.9, -97.0) → (33.5, -96.5).
|
||||||
|
%HrrrProfile{}
|
||||||
|
|> HrrrProfile.changeset(%{
|
||||||
|
valid_time: now,
|
||||||
|
run_time: now,
|
||||||
|
lat: 33.2,
|
||||||
|
lon: -96.75,
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|> Microwaveprop.Repo.insert!()
|
||||||
|
|
||||||
|
assert {:ok, result} =
|
||||||
|
PathCompute.compute("32.9, -97.0", "33.5, -96.5", 10_000, @station_params)
|
||||||
|
|
||||||
|
# Even if HRRR points doesn't include this grid (lookups snap to
|
||||||
|
# specific grid cells), the pipeline ran end-to-end without error.
|
||||||
|
assert is_list(result.hrrr_points)
|
||||||
|
end
|
||||||
|
|
||||||
test "Maidenhead grid input round-trips through resolve" do
|
test "Maidenhead grid input round-trips through resolve" do
|
||||||
assert {:ok, result} =
|
assert {:ok, result} =
|
||||||
PathCompute.compute("EM12", "EM13", 10_000, @station_params)
|
PathCompute.compute("EM12", "EM13", 10_000, @station_params)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,48 @@ defmodule Microwaveprop.Pskr.ClientTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "tcp messages with mismatched socket" do
|
||||||
|
test "tcp message for a stale socket is ignored", %{placeholder: _} do
|
||||||
|
{:ok, pid} = GenServer.start_link(Client, [])
|
||||||
|
# Send a TCP message with a fake socket that doesn't match state.socket.
|
||||||
|
# state.socket is nil, but :tcp pattern matches when socket matches state.socket.
|
||||||
|
# Since state.socket is nil, this falls through to the catch-all handle_info.
|
||||||
|
send(pid, {:tcp, :stale_port, "data"})
|
||||||
|
Process.sleep(20)
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
GenServer.stop(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tcp_closed for a stale socket is ignored", %{placeholder: _} do
|
||||||
|
{:ok, pid} = GenServer.start_link(Client, [])
|
||||||
|
send(pid, {:tcp_closed, :stale_port})
|
||||||
|
Process.sleep(20)
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
GenServer.stop(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tcp_error for a stale socket is ignored", %{placeholder: _} do
|
||||||
|
{:ok, pid} = GenServer.start_link(Client, [])
|
||||||
|
send(pid, {:tcp_error, :stale_port, :reason})
|
||||||
|
Process.sleep(20)
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
GenServer.stop(pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ":reconnect message routing" do
|
||||||
|
test "schedules a connect attempt for a standby (immediate :continue, :connect)" do
|
||||||
|
{:ok, pid} = GenServer.start_link(Client, [])
|
||||||
|
send(pid, :reconnect)
|
||||||
|
Process.sleep(50)
|
||||||
|
# The handle_info(:reconnect) returns {:noreply, state, {:continue, :connect}}.
|
||||||
|
# The connect will fail (no MQTT broker reachable) and schedule another retry.
|
||||||
|
# We just verify the process didn't crash on receipt.
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
GenServer.stop(pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "terminate/2" do
|
describe "terminate/2" do
|
||||||
test "is graceful when state has no socket" do
|
test "is graceful when state has no socket" do
|
||||||
{:ok, pid} = GenServer.start_link(Client, [])
|
{:ok, pid} = GenServer.start_link(Client, [])
|
||||||
|
|
|
||||||
|
|
@ -1326,4 +1326,32 @@ defmodule Microwaveprop.WeatherTest do
|
||||||
assert Weather.iemre_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
assert Weather.iemre_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "weather_point_detail/3" do
|
||||||
|
test "returns nil when no grid data is available for the point" do
|
||||||
|
assert Weather.weather_point_detail(33.0, -97.0, ~U[2026-03-28 18:00:00Z]) == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "snaps coordinates to the 0.125° grid before lookup" do
|
||||||
|
assert Weather.weather_point_detail(33.064, -97.043, ~U[2026-03-28 18:00:00Z]) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "available_weather_valid_times/0" do
|
||||||
|
test "returns an empty list when no scores are stored" do
|
||||||
|
assert Weather.available_weather_valid_times() == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "latest_grid_valid_time/0" do
|
||||||
|
test "returns nil when no scores are stored" do
|
||||||
|
assert Weather.latest_grid_valid_time() == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "available_hrdps_valid_times/0" do
|
||||||
|
test "returns an empty list when no HRDPS data is stored" do
|
||||||
|
assert Weather.available_hrdps_valid_times() == []
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,91 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorkerTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "perform/1 with a real Path row" do
|
||||||
|
setup do
|
||||||
|
Req.Test.stub(Microwaveprop.Terrain.ElevationClient, fn conn ->
|
||||||
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
||||||
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
||||||
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "computes and stores result for an existing pending path" do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
user = Microwaveprop.AccountsFixtures.user_fixture()
|
||||||
|
{:ok, _} = Microwaveprop.Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good})
|
||||||
|
|
||||||
|
{:ok, mission} =
|
||||||
|
Microwaveprop.RoverPlanning.create_mission(user, %{
|
||||||
|
"name" => "Path worker test",
|
||||||
|
"band_mhz" => 10_000,
|
||||||
|
"only_known_good" => true,
|
||||||
|
"rover_height_ft" => 8.0,
|
||||||
|
"station_height_ft" => 30.0,
|
||||||
|
"stations" => %{"0" => %{"input" => "EM12kp", "position" => 0}}
|
||||||
|
})
|
||||||
|
|
||||||
|
[path | _] =
|
||||||
|
Microwaveprop.Repo.all(from(p in Microwaveprop.RoverPlanning.Path, where: p.mission_id == ^mission.id))
|
||||||
|
|
||||||
|
args = %{
|
||||||
|
"mission_id" => mission.id,
|
||||||
|
"rover_location_id" => path.rover_location_id,
|
||||||
|
"station_id" => path.station_id,
|
||||||
|
"band_mhz" => path.band_mhz
|
||||||
|
}
|
||||||
|
|
||||||
|
assert :ok = perform_job(RoverPathProfileWorker, args)
|
||||||
|
|
||||||
|
reloaded = Microwaveprop.Repo.reload(path)
|
||||||
|
assert reloaded.status == :complete
|
||||||
|
assert reloaded.result["distance_km"] > 0
|
||||||
|
assert is_binary(reloaded.result["term"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "skips when path is already complete" do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
user = Microwaveprop.AccountsFixtures.user_fixture()
|
||||||
|
{:ok, _} = Microwaveprop.Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good})
|
||||||
|
|
||||||
|
{:ok, mission} =
|
||||||
|
Microwaveprop.RoverPlanning.create_mission(user, %{
|
||||||
|
"name" => "Path worker complete test",
|
||||||
|
"band_mhz" => 10_000,
|
||||||
|
"only_known_good" => true,
|
||||||
|
"rover_height_ft" => 8.0,
|
||||||
|
"station_height_ft" => 30.0,
|
||||||
|
"stations" => %{"0" => %{"input" => "EM12kp", "position" => 0}}
|
||||||
|
})
|
||||||
|
|
||||||
|
[path | _] =
|
||||||
|
Microwaveprop.Repo.all(from(p in Microwaveprop.RoverPlanning.Path, where: p.mission_id == ^mission.id))
|
||||||
|
|
||||||
|
# Mark complete so the worker short-circuits.
|
||||||
|
{:ok, _} =
|
||||||
|
path
|
||||||
|
|> Microwaveprop.RoverPlanning.Path.changeset(%{status: :complete, result: %{"verdict" => "DONE"}})
|
||||||
|
|> Microwaveprop.Repo.update()
|
||||||
|
|
||||||
|
args = %{
|
||||||
|
"mission_id" => mission.id,
|
||||||
|
"rover_location_id" => path.rover_location_id,
|
||||||
|
"station_id" => path.station_id,
|
||||||
|
"band_mhz" => path.band_mhz
|
||||||
|
}
|
||||||
|
|
||||||
|
assert :ok = perform_job(RoverPathProfileWorker, args)
|
||||||
|
|
||||||
|
reloaded = Microwaveprop.Repo.reload(path)
|
||||||
|
# Result still the placeholder we set — worker did not recompute.
|
||||||
|
assert reloaded.result == %{"verdict" => "DONE"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "backoff/1" do
|
describe "backoff/1" do
|
||||||
test "exponential backoff bounded at 3600s" do
|
test "exponential backoff bounded at 3600s" do
|
||||||
assert RoverPathProfileWorker.backoff(%Oban.Job{attempt: 1}) == 60
|
assert RoverPathProfileWorker.backoff(%Oban.Job{attempt: 1}) == 60
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,55 @@ defmodule MicrowavepropWeb.RoverLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "URL station encoding (anonymous)" do
|
||||||
|
test "URL stations param overrides anonymous defaults", %{conn: conn} do
|
||||||
|
# Format: "CALL:GRID,-CALL:GRID" — leading `-` means deselected.
|
||||||
|
url = ~p"/rover?stations=#{"W5XYZ:EM12,-W5ABC:EM13"}"
|
||||||
|
{:ok, _view, html} = live(conn, url)
|
||||||
|
|
||||||
|
assert html =~ "W5XYZ"
|
||||||
|
assert html =~ "W5ABC"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "malformed URL stations param keeps the defaults", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/rover?stations=garbage")
|
||||||
|
# Malformed entries are dropped — defaults remain.
|
||||||
|
assert html =~ "W5LUA"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "add_station (anonymous)" do
|
||||||
|
test "adds a station via the form", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/rover")
|
||||||
|
|
||||||
|
view
|
||||||
|
|> form(~s(form[phx-submit="add_station"]), %{"callsign" => "K5TX", "grid" => "EM13"})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert render(view) =~ "K5TX"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blank callsign + grid surfaces an error message", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/rover")
|
||||||
|
|
||||||
|
view
|
||||||
|
|> form(~s(form[phx-submit="add_station"]), %{"callsign" => "", "grid" => ""})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert render(view) =~ "callsign required"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid grid surfaces an error message", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/rover")
|
||||||
|
|
||||||
|
view
|
||||||
|
|> form(~s(form[phx-submit="add_station"]), %{"callsign" => "K5TX", "grid" => "ZZZ99"})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert render(view) =~ "invalid grid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "additional event handlers (anonymous)" do
|
describe "additional event handlers (anonymous)" do
|
||||||
test "toggle_only_good flips the filter assign without recomputing", %{conn: conn} do
|
test "toggle_only_good flips the filter assign without recomputing", %{conn: conn} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/rover")
|
{:ok, view, _html} = live(conn, ~p"/rover")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
alias Ecto.Adapters.SQL.Sandbox
|
alias Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
ExUnit.start(exclude: [:slow], capture_log: true)
|
ExUnit.start(capture_log: true)
|
||||||
Sandbox.mode(Microwaveprop.Repo, :manual)
|
Sandbox.mode(Microwaveprop.Repo, :manual)
|
||||||
Sandbox.mode(Microwaveprop.AprsRepo, :manual)
|
Sandbox.mode(Microwaveprop.AprsRepo, :manual)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue