- 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.
133 lines
4.3 KiB
Elixir
133 lines
4.3 KiB
Elixir
defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
|
use Microwaveprop.DataCase
|
|
|
|
alias Microwaveprop.Beacons.Beacon
|
|
alias Microwaveprop.Beacons.RangeEstimate
|
|
|
|
defp beacon(overrides \\ %{}) do
|
|
base = %Beacon{
|
|
id: "00000000-0000-0000-0000-000000000000",
|
|
frequency_mhz: 10_368.1,
|
|
callsign: "W5HN",
|
|
grid: "EM12",
|
|
lat: 32.897,
|
|
lon: -97.038,
|
|
power_mw: 10_000.0,
|
|
height_ft: 100,
|
|
on_the_air: true
|
|
}
|
|
|
|
struct!(base, overrides)
|
|
end
|
|
|
|
describe "mw_to_dbm/1" do
|
|
test "1 mW is 0 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(1.0), 0.0, 0.001
|
|
end
|
|
|
|
test "1 W (1000 mW) is 30 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(1000.0), 30.0, 0.001
|
|
end
|
|
|
|
test "10 W (10_000 mW) is 40 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(10_000.0), 40.0, 0.001
|
|
end
|
|
end
|
|
|
|
describe "nearest_band_mhz/1" do
|
|
test "10368.1 MHz maps to 10000 (10 GHz band)" do
|
|
assert RangeEstimate.nearest_band_mhz(10_368.1) == 10_000
|
|
end
|
|
|
|
test "24192.1 MHz maps to 24000" do
|
|
assert RangeEstimate.nearest_band_mhz(24_192.1) == 24_000
|
|
end
|
|
|
|
test "47088 MHz maps to 47000" do
|
|
assert RangeEstimate.nearest_band_mhz(47_088.0) == 47_000
|
|
end
|
|
end
|
|
|
|
describe "estimate/1" do
|
|
test "returns a map with band info, center score, and per-grid cells" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
assert result.band_mhz == 10_000
|
|
assert result.center_score == 50
|
|
assert is_float(result.eirp_dbm)
|
|
assert result.cells != []
|
|
assert result.grid_step == 0.125
|
|
assert result.tiers != []
|
|
end
|
|
|
|
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
for cell <- result.cells do
|
|
assert is_float(cell.lat)
|
|
assert is_float(cell.lon)
|
|
assert is_float(cell.distance_km)
|
|
assert is_integer(cell.rx_dbm)
|
|
assert byte_size(cell.label) > 0
|
|
assert byte_size(cell.color) > 0
|
|
# Detection floor enforced
|
|
assert cell.rx_dbm >= -145
|
|
end
|
|
end
|
|
|
|
test "cells closer to the beacon have stronger rx_dbm" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
[closest | _] = Enum.sort_by(result.cells, & &1.distance_km)
|
|
[farthest | _] = Enum.sort_by(result.cells, &(-&1.distance_km))
|
|
|
|
assert closest.rx_dbm > farthest.rx_dbm
|
|
end
|
|
|
|
test "a more powerful beacon produces more cells" do
|
|
weak = RangeEstimate.estimate(beacon(power_mw: 10.0))
|
|
strong = RangeEstimate.estimate(beacon(power_mw: 10_000.0))
|
|
assert length(strong.cells) > length(weak.cells)
|
|
end
|
|
|
|
test "uses a default score of 50 when no propagation data exists" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
# No scores in test DB -> center defaults, cells all use 50.
|
|
assert result.center_score == 50
|
|
assert result.valid_time == nil
|
|
|
|
# All cells should have score 50 when there's no HRRR data.
|
|
assert Enum.all?(result.cells, fn c -> c.score == 50 end)
|
|
end
|
|
|
|
test "cells align with the 0.125 deg HRRR grid" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
for cell <- result.cells do
|
|
# lat and lon should both be multiples of 0.125
|
|
lat_mod = abs(cell.lat / 0.125 - round(cell.lat / 0.125))
|
|
lon_mod = abs(cell.lon / 0.125 - round(cell.lon / 0.125))
|
|
assert lat_mod < 0.01
|
|
assert lon_mod < 0.01
|
|
end
|
|
end
|
|
|
|
test "short-circuits with empty cells for beacons below 5.76 GHz" do
|
|
# 432 MHz with 1 W at unrestricted range would generate >100k cells
|
|
# because atmospheric loss is negligible -- encoding that payload
|
|
# into the map's data attribute was locking up browsers in prod.
|
|
result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0))
|
|
assert result.cells == []
|
|
assert result.max_range_km == 0.0
|
|
# Still reports the nearest configured band for display purposes.
|
|
assert is_integer(result.band_mhz)
|
|
end
|
|
|
|
test "short-circuits at exactly 5760 MHz threshold" do
|
|
above = RangeEstimate.estimate(beacon(frequency_mhz: 5760.0, power_mw: 1000.0))
|
|
below = RangeEstimate.estimate(beacon(frequency_mhz: 5759.9, power_mw: 1000.0))
|
|
assert above.cells != []
|
|
assert below.cells == []
|
|
end
|
|
end
|
|
end
|