- 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.
522 lines
17 KiB
Elixir
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
|