prop/test/microwaveprop/rover/compute_test.exs
Graham McIntire cb8445f329
fix: resolve 158/183 credo --strict warnings
- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg)
- Move 12+ nested alias/import/require to module top level
- Add phx-change/id attributes to 11 raw HTML <form> tags
- Remove 4 unused LiveView assigns (:bounds, :data_provider)
- Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts)
- Break 2 long lines (path_compute.ex:382)
- Strengthen weak test assertions (is_binary→byte_size, is_list→!=[])
- Replace Module.concat with Module.safe_concat (2 occurrences)
- Replace length/1 > 0 with list != [] (9 occurrences)
- Remove no-op assert true, fix no-assertion tests

Remaining: 24 socket.assigns introspection warnings (deliberate test
pattern for observable behavior testing), 1 formatter-resistant long
line, 3 app-code usage warnings.
2026-06-12 15:47:15 -05:00

522 lines
17 KiB
Elixir

defmodule Microwaveprop.Rover.ComputeTest do
use ExUnit.Case, async: true
alias Microwaveprop.Rover.Compute
defp build_grid(home_lat, home_lon) do
# 3x3 grid of cells around home spaced 0.1 deg apart
for dlat <- [-0.1, 0.0, 0.1], dlon <- [-0.1, 0.0, 0.1] do
%{
lat: Float.round(home_lat + dlat, 4),
lon: Float.round(home_lon + dlon, 4),
score: 80
}
end
end
test "returns top_candidates ordered by score desc with required fields" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _band, _t, _bbox -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _cells, _stations -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
args = %{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
}
result =
Compute.run(args,
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert map_size(result) > 0
assert Map.has_key?(result, :cells)
assert Map.has_key?(result, :top_candidates)
assert result.cells != []
assert length(result.top_candidates) <= 5
refute result.top_candidates == []
# Sorted by score desc.
scores = Enum.map(result.top_candidates, & &1.score)
assert scores == Enum.sort(scores, :desc)
# Required candidate fields.
cand = hd(result.top_candidates)
for key <- [
:grid,
:lat,
:lon,
:elev_m,
:drive_min,
:score,
:tier_color,
:distance_km,
:bearing_compass,
:name
] do
assert Map.has_key?(cand, key), "expected candidate to have key #{inspect(key)}"
end
assert byte_size(cand.grid) > 0
assert byte_size(cand.tier_color) > 0
assert byte_size(cand.bearing_compass) > 0
assert byte_size(cand.name) > 0
end
test "penalizes cells with tall nearby buildings (clutter)" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}
scores_at = fn _band, _t, _bbox -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _cells, _stations -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
buildings_clutter_lookup = fn cells ->
Map.new(cells, fn c ->
height = if {c.lat, c.lon} == tall_cell, do: 30.0, else: 0.0
{{c.lat, c.lon}, height}
end)
end
args = %{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
}
result =
Compute.run(args,
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup,
buildings_clutter_lookup: buildings_clutter_lookup
)
cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
tall_score = Map.fetch!(cells_by_pos, tall_cell)
ref_score = Map.fetch!(cells_by_pos, ref_cell)
# 30m nearby building -> penalty = 30/5 = 6.0 dB
assert ref_score - tall_score >= 5.5
end
test "calls progress callback with each pipeline step label" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
test_pid = self()
progress = fn label, step, total -> send(test_pid, {:progress, label, step, total}) end
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: fn _, _, _ -> grid end,
elev_lookup: fn points -> Map.new(points, fn p -> {p, 250} end) end,
clearance_lookup: fn _, _ -> %{} end,
prominence_lookup: fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end,
road_lookup: fn _, _ -> {:error, :stubbed} end,
progress: progress
)
assert_received {:progress, "Loading propagation grid", 1, total}
assert is_integer(total) and total >= 7
assert_received {:progress, "Looking up elevation", 2, ^total}
assert_received {:progress, "Computing path clearance", _, ^total}
assert_received {:progress, "Scoring cells", ^total, ^total}
end
test "filters cells failing min_elev_gain" do
home = %{lat: 33.0, lon: -96.0, elev_m: 500}
stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]
grid = build_grid(home.lat, home.lon)
# All elevations below home -- nothing should pass min_elev_gain=200.
scores_at = fn _band, _t, _bbox -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 100} end) end
clearance_lookup = fn _cells, _stations -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 200
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert result.cells == []
assert result.top_candidates == []
end
test "road proximity penalty reduces score" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
cell_with_penalty = {33.1, -96.0}
road_lookup = fn cells, _bbox ->
{:ok,
Map.new(cells, fn c ->
dist = if {c.lat, c.lon} == cell_with_penalty, do: 10.0, else: 0.0
{{c.lat, c.lon}, dist}
end)}
end
Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)
try do
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
cell_scores = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
penalized_score = cell_scores[cell_with_penalty]
other_scores = cell_scores |> Map.delete(cell_with_penalty) |> Map.values()
assert Enum.all?(other_scores, fn s -> s > penalized_score end)
after
Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)
end
end
test "returns warnings when grid is empty" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
scores_at = fn _, _, _ -> [] end
elev_lookup = fn _ -> %{} end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn _ -> %{} end
road_lookup = fn _, _ -> {:error, :stubbed} end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert result.cells == []
assert result.top_candidates == []
assert Enum.any?(result.warnings, &String.contains?(&1, "No HRRR"))
end
test "returns all-nil elevation warning" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, nil} end) end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _, _ -> {:error, :stubbed} end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert Enum.any?(result.warnings, &String.contains?(&1, "Elevation tiles"))
end
test "canopy clutter penalty reduces score" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _, _ -> {:error, :stubbed} end
canopy_clutter_lookup = fn cells ->
Map.new(cells, fn c ->
height = if {c.lat, c.lon} == tall_cell, do: 20.0, else: 0.0
{{c.lat, c.lon}, height}
end)
end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup,
canopy_clutter_lookup: canopy_clutter_lookup
)
cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
tall_score = Map.fetch!(cells_by_pos, tall_cell)
ref_score = Map.fetch!(cells_by_pos, ref_cell)
assert tall_score < ref_score
end
test "positive prominence gives a score boost" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
high_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
low_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _, _ -> %{} end
road_lookup = fn _, _ -> {:error, :stubbed} end
prominence_lookup = fn cells ->
Map.new(cells, fn c ->
value = if {c.lat, c.lon} == high_cell, do: 90, else: 0
{{c.lat, c.lon}, value}
end)
end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
high_score = Map.fetch!(cells_by_pos, high_cell)
low_score = Map.fetch!(cells_by_pos, low_cell)
assert high_score > low_score
end
test "terrain clearance gives score boost" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _, _ -> {:error, :stubbed} end
# Cell near home gets positive clearance to station
near_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
far_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}
clearance_lookup = fn cells, stations ->
Map.new(cells, fn c ->
s = hd(stations)
key = {{c.lat, c.lon}, {s.lat, s.lon}}
value = if {c.lat, c.lon} == near_cell, do: 150.0, else: 0.0
{key, value}
end)
end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
near_score = Map.fetch!(cells_by_pos, near_cell)
far_score = Map.fetch!(cells_by_pos, far_cell)
assert near_score > far_score
end
test "road_enabled? false skips road proximity check" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _, _ -> {:error, :not_called} end
Application.put_env(:microwaveprop, :rover_road_proximity_enabled, false)
try do
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert result.cells != []
assert result.top_candidates != []
after
Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)
end
end
test "good_locations snaps cells to exact locations" do
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
grid = build_grid(home.lat, home.lon)
scores_at = fn _, _, _ -> grid end
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
clearance_lookup = fn _, _ -> %{} end
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
road_lookup = fn _, _ -> {:error, :stubbed} end
result =
Compute.run(
%{
home: home,
stations: stations,
band_mhz: 10_000,
valid_time: ~U[2026-04-25 12:00:00Z],
mode: :ssb,
max_distance_km: 65.0,
min_elev_gain: 0,
good_locations: [%{lat: 33.05, lon: -96.05}]
},
scores_at: scores_at,
elev_lookup: elev_lookup,
clearance_lookup: clearance_lookup,
prominence_lookup: prominence_lookup,
road_lookup: road_lookup
)
assert result.cells != []
end
end