prop/test/microwaveprop/rover/coverage_test.exs
Graham McIntire 6aec3eeea4 Extract Coverage module, fix computation, add tests
- Extract scoring logic to Microwaveprop.Rover.Coverage for testability
- Two-phase computation: fast pass (distance + propagation) for all grids,
  then SRTM terrain analysis for top 20 candidates only
- Cap search radius to 300 km to keep candidate count reasonable
- Run terrain analysis in Task with rescue/fallback for resilience
- Background Task doesn't block LiveView process
- 5 tests covering: ranked results, empty inputs, station details,
  band range differences, field presence
2026-04-07 16:08:03 -05:00

95 lines
3.1 KiB
Elixir

defmodule Microwaveprop.Rover.CoverageTest do
use Microwaveprop.DataCase, async: false
alias Microwaveprop.Rover.Coverage
describe "compute/2" do
test "returns ranked grids for stations within range" do
stations = [
%{label: "STA1", lat: 33.0, lon: -97.0},
%{label: "STA2", lat: 33.5, lon: -97.5}
]
result = Coverage.compute(stations, 10_000)
assert is_list(result)
assert length(result) > 0
scores = Enum.map(result, & &1.coverage_score)
assert scores == Enum.sort(scores, :desc)
first = hd(result)
assert is_binary(first.grid)
assert byte_size(first.grid) == 4
assert is_number(first.lat)
assert is_number(first.lon)
assert is_integer(first.coverage_score)
assert first.coverage_score >= 0 and first.coverage_score <= 100
assert is_integer(first.stations_in_range)
assert first.stations_in_range > 0
assert is_integer(first.workable_count)
assert is_integer(first.prop_score)
assert is_list(first.station_details)
assert is_list(first.forecast)
end
test "returns empty list with fewer than 2 stations" do
assert Coverage.compute([%{label: "STA1", lat: 33.0, lon: -97.0}], 10_000) == []
assert Coverage.compute([], 10_000) == []
end
test "station_details includes distance and in_range" do
stations = [
%{label: "STA1", lat: 33.0, lon: -97.0},
%{label: "STA2", lat: 33.5, lon: -97.0}
]
[first | _] = Coverage.compute(stations, 10_000)
assert length(first.station_details) == 2
for detail <- first.station_details do
assert Map.has_key?(detail, :label)
assert Map.has_key?(detail, :dist_km)
assert Map.has_key?(detail, :in_range)
assert is_number(detail.dist_km)
assert is_boolean(detail.in_range)
end
end
test "higher frequency bands have shorter range so fewer grids cover both stations" do
far_stations = [
%{label: "STA1", lat: 33.0, lon: -97.0},
%{label: "STA2", lat: 35.0, lon: -97.0}
]
result_10g = Coverage.compute(far_stations, 10_000)
result_241g = Coverage.compute(far_stations, 241_000)
# 10 GHz has 500 km range, 241 GHz has 50 km — far fewer grids reach both at 241
grids_reaching_both_10g = Enum.count(result_10g, &(&1.stations_in_range == 2))
grids_reaching_both_241g = Enum.count(result_241g, &(&1.stations_in_range == 2))
assert grids_reaching_both_10g >= grids_reaching_both_241g
end
test "top results have station details with expected fields" do
stations = [
%{label: "STA1", lat: 33.0, lon: -97.0},
%{label: "STA2", lat: 33.2, lon: -97.2}
]
[first | _] = Coverage.compute(stations, 10_000)
# Each station detail should have core fields
for detail <- first.station_details do
assert Map.has_key?(detail, :label)
assert Map.has_key?(detail, :dist_km)
assert Map.has_key?(detail, :in_range)
end
# At least one station should be in range
assert Enum.any?(first.station_details, & &1.in_range)
end
end
end