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.PathCompute
|
||||
alias Microwaveprop.Weather.HrrrProfile
|
||||
|
||||
describe "compute_loss_budget/5" do
|
||||
test "fspl increases with distance" do
|
||||
|
|
@ -196,6 +197,42 @@ defmodule Microwaveprop.Propagation.PathComputeTest do
|
|||
assert result.loss_budget.h2o > 0
|
||||
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
|
||||
assert {:ok, result} =
|
||||
PathCompute.compute("EM12", "EM13", 10_000, @station_params)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,48 @@ defmodule Microwaveprop.Pskr.ClientTest do
|
|||
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
|
||||
test "is graceful when state has no socket" do
|
||||
{: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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -40,6 +40,91 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorkerTest do
|
|||
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
|
||||
test "exponential backoff bounded at 3600s" do
|
||||
assert RoverPathProfileWorker.backoff(%Oban.Job{attempt: 1}) == 60
|
||||
|
|
|
|||
|
|
@ -105,6 +105,55 @@ defmodule MicrowavepropWeb.RoverLiveTest do
|
|||
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
|
||||
test "toggle_only_good flips the filter assign without recomputing", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/rover")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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.AprsRepo, :manual)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue