Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m38s
- F-level (228): replace length/1 with Enum.count_until/2 or pattern matching; convert Enum.flat_map+if to Enum.filter+Enum.map; fix identity case in narr_client - W-level (46): normalize dual atom/string key access in weather_layers, beacon_measurements, surface, skewt_live, contact_live/show; add :data_provider and weather-map assigns to ignored_assigns in credo config (consumed by child components credo can't trace); remove weak is_list assertion; remove explicit assert_receive timeout - R-level (6): replace 'This module provides...' moduledocs with meaningful descriptions - Also fix 4 compile-connected xref issues by deferring BandConfig.band_options() from module attribute to runtime Co-Authored-By: Claude <noreply@anthropic.com>
244 lines
5.7 KiB
Elixir
244 lines
5.7 KiB
Elixir
defmodule Microwaveprop.Aprs.PathParserTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Aprs.PathParser
|
|
|
|
@sender "N5XXX-9"
|
|
@sender_pos {32.897, -97.038}
|
|
@heard_at ~U[2026-04-30 12:00:00Z]
|
|
|
|
defp lookup_fn(map) do
|
|
fn callsign -> Map.get(map, callsign) end
|
|
end
|
|
|
|
describe "parse_hops/5" do
|
|
test "single used-flag callsign emits one hop from sender" do
|
|
lookup = lookup_fn(%{"K5GVL-10" => {32.95, -96.10}})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"K5GVL-10*,WIDE1-1,WIDE2-1",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert [hop] = hops
|
|
assert hop.src_callsign == @sender
|
|
assert hop.src_pos == @sender_pos
|
|
assert hop.dst_callsign == "K5GVL-10"
|
|
assert hop.dst_pos == {32.95, -96.10}
|
|
assert hop.heard_at == @heard_at
|
|
assert is_float(hop.distance_km)
|
|
end
|
|
|
|
test "two chained used-flag callsigns emit two hops, source advances" do
|
|
lookup =
|
|
lookup_fn(%{
|
|
"K5XYZ-3" => {33.0, -96.5},
|
|
"W5ABC-7" => {33.5, -95.5}
|
|
})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"K5XYZ-3*,W5ABC-7*,WIDE2-1",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert Enum.count_until(hops, 3) == 2
|
|
[first, second] = hops
|
|
|
|
assert first.src_callsign == @sender
|
|
assert first.src_pos == @sender_pos
|
|
assert first.dst_callsign == "K5XYZ-3"
|
|
assert first.dst_pos == {33.0, -96.5}
|
|
|
|
assert second.src_callsign == "K5XYZ-3"
|
|
assert second.src_pos == {33.0, -96.5}
|
|
assert second.dst_callsign == "W5ABC-7"
|
|
assert second.dst_pos == {33.5, -95.5}
|
|
end
|
|
|
|
test "anonymous WIDE1* used-flag yields no hops" do
|
|
lookup = lookup_fn(%{})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"WIDE1*,WIDE2-1",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "mixed path with no used real callsigns yields no hops" do
|
|
lookup = lookup_fn(%{"K5VOM-10" => {32.5, -96.5}})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"WA5VHU-8,WIDE1*,WIDE2-1,qAR,K5VOM-10",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "trailing unused real callsign does not emit a hop" do
|
|
lookup =
|
|
lookup_fn(%{
|
|
"K5GVL-10" => {32.95, -96.10},
|
|
"N5TXZ-10" => {33.1, -96.2}
|
|
})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"K5GVL-10*,N5TXZ-10",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert [hop] = hops
|
|
assert hop.dst_callsign == "K5GVL-10"
|
|
end
|
|
|
|
test "TCPIP and Q-construct yield no hops" do
|
|
lookup = lookup_fn(%{})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"TCPIP*,qAC,T2TEXAS",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "lookup returns nil for a used digi → drop that hop and do NOT advance source" do
|
|
lookup =
|
|
lookup_fn(%{
|
|
"K5XYZ-3" => {33.0, -96.5},
|
|
# W5ABC-7 deliberately missing
|
|
"K5DEF-1" => {33.5, -95.5}
|
|
})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"K5XYZ-3*,W5ABC-7*,K5DEF-1*",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert Enum.count_until(hops, 3) == 2
|
|
# First hop is sender → K5XYZ-3 as before.
|
|
assert Enum.at(hops, 0).src_callsign == @sender
|
|
assert Enum.at(hops, 0).dst_callsign == "K5XYZ-3"
|
|
# Critical: second emitted hop chains from K5XYZ-3 (NOT from W5ABC-7
|
|
# which was dropped). Proves source did not advance through the
|
|
# missing-position digi.
|
|
assert Enum.at(hops, 1).src_callsign == "K5XYZ-3"
|
|
assert Enum.at(hops, 1).dst_callsign == "K5DEF-1"
|
|
end
|
|
|
|
test "empty path string yields no hops" do
|
|
lookup = lookup_fn(%{})
|
|
|
|
hops = PathParser.parse_hops(@sender, @sender_pos, "", @heard_at, lookup)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "aliases-only path yields no hops" do
|
|
lookup = lookup_fn(%{})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"WIDE1-1,WIDE2-2",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "lowercase Q-construct qAo is recognized and yields no hops" do
|
|
lookup = lookup_fn(%{})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"qAo",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "Q-construct followed by IGate name yields no hops" do
|
|
lookup = lookup_fn(%{"IGATE-CALL" => {33.0, -96.0}})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"qAR,IGATE-CALL",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "self-loop suppressed when used digi callsign matches sender callsign" do
|
|
# Sender sees its own callsign reflected in the digi path (corrupted
|
|
# frame). Should be skipped.
|
|
lookup = lookup_fn(%{@sender => {99.0, 99.0}})
|
|
|
|
hops =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"#{@sender}*",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert hops == []
|
|
end
|
|
|
|
test "haversine distance for DFW → K5GVL-10 is approximately 88.4 km" do
|
|
lookup = lookup_fn(%{"K5GVL-10" => {32.95, -96.10}})
|
|
|
|
[hop] =
|
|
PathParser.parse_hops(
|
|
@sender,
|
|
@sender_pos,
|
|
"K5GVL-10*",
|
|
@heard_at,
|
|
lookup
|
|
)
|
|
|
|
assert abs(hop.distance_km - 88.4) < 1.0
|
|
end
|
|
end
|
|
end
|