fix(aprs): tighten PathParser self-loop check and Q-construct regex
Three spec-compliance fixes to PathParser: 1. Q-construct regex now accepts lowercase letters (qAo, qAr, etc.). The moduledoc lists qAo as a Q-construct to skip but the previous ~r/^qA[A-Z]$/ rejected it, leaving it to be misclassified. 2. self_loop?/2 now compares callsigns only; the spurious src_pos == dst_pos clause would falsely suppress two distinct stations that happen to share coarse coordinates (e.g. same building). 3. Strengthen the "lookup returned nil → drop hop, do NOT advance source" test: extend the path to three tokens so the assertion actually proves the third hop chains from the previous good source rather than from the dropped one. The two-token version passed regardless of behavior. Self-loop fixture updated to trigger via callsign equality (the only remaining mechanism) instead of position equality.
This commit is contained in:
parent
4a5864f090
commit
4759d1313e
2 changed files with 37 additions and 13 deletions
|
|
@ -38,7 +38,7 @@ defmodule Microwaveprop.Aprs.PathParser do
|
||||||
|
|
||||||
@alias_regex ~r/^(WIDE[12](-[0-9])?|TRACE[12](-[0-9])?|RELAY|ECHO|GATE|NOGATE|RFONLY)$/
|
@alias_regex ~r/^(WIDE[12](-[0-9])?|TRACE[12](-[0-9])?|RELAY|ECHO|GATE|NOGATE|RFONLY)$/
|
||||||
@callsign_regex ~r/^[A-Z0-9]{1,2}[0-9][A-Z]{1,3}(-[0-9]{1,2})?$/
|
@callsign_regex ~r/^[A-Z0-9]{1,2}[0-9][A-Z]{1,3}(-[0-9]{1,2})?$/
|
||||||
@q_construct_regex ~r/^qA[A-Z]$/
|
@q_construct_regex ~r/^qA[A-Za-z]$/
|
||||||
|
|
||||||
@spec parse_hops(
|
@spec parse_hops(
|
||||||
sender_callsign :: String.t(),
|
sender_callsign :: String.t(),
|
||||||
|
|
@ -89,7 +89,7 @@ defmodule Microwaveprop.Aprs.PathParser do
|
||||||
dst_pos ->
|
dst_pos ->
|
||||||
{src_callsign, src_pos} = source
|
{src_callsign, src_pos} = source
|
||||||
|
|
||||||
if self_loop?(src_callsign, src_pos, callsign, dst_pos) do
|
if self_loop?(src_callsign, callsign) do
|
||||||
{source, hops}
|
{source, hops}
|
||||||
else
|
else
|
||||||
hop = build_hop(src_callsign, src_pos, callsign, dst_pos, heard_at)
|
hop = build_hop(src_callsign, src_pos, callsign, dst_pos, heard_at)
|
||||||
|
|
@ -108,8 +108,8 @@ defmodule Microwaveprop.Aprs.PathParser do
|
||||||
{source, hops}
|
{source, hops}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp self_loop?(src_callsign, src_pos, dst_callsign, dst_pos) do
|
defp self_loop?(src_callsign, dst_callsign) do
|
||||||
src_callsign == dst_callsign or src_pos == dst_pos
|
src_callsign == dst_callsign
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_hop(src_callsign, src_pos, dst_callsign, dst_pos, heard_at) do
|
defp build_hop(src_callsign, src_pos, dst_callsign, dst_pos, heard_at) do
|
||||||
|
|
|
||||||
|
|
@ -128,25 +128,32 @@ defmodule Microwaveprop.Aprs.PathParserTest do
|
||||||
assert hops == []
|
assert hops == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "used callsign with nil lookup is dropped and source does NOT advance" do
|
test "lookup returns nil for a used digi → drop that hop and do NOT advance source" do
|
||||||
lookup =
|
lookup =
|
||||||
lookup_fn(%{
|
lookup_fn(%{
|
||||||
"K5XYZ-3" => {33.0, -96.5}
|
"K5XYZ-3" => {33.0, -96.5},
|
||||||
# W5ABC-7 deliberately missing
|
# W5ABC-7 deliberately missing
|
||||||
|
"K5DEF-1" => {33.5, -95.5}
|
||||||
})
|
})
|
||||||
|
|
||||||
hops =
|
hops =
|
||||||
PathParser.parse_hops(
|
PathParser.parse_hops(
|
||||||
@sender,
|
@sender,
|
||||||
@sender_pos,
|
@sender_pos,
|
||||||
"K5XYZ-3*,W5ABC-7*",
|
"K5XYZ-3*,W5ABC-7*,K5DEF-1*",
|
||||||
@heard_at,
|
@heard_at,
|
||||||
lookup
|
lookup
|
||||||
)
|
)
|
||||||
|
|
||||||
assert [hop] = hops
|
assert length(hops) == 2
|
||||||
assert hop.src_callsign == @sender
|
# First hop is sender → K5XYZ-3 as before.
|
||||||
assert hop.dst_callsign == "K5XYZ-3"
|
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
|
end
|
||||||
|
|
||||||
test "empty path string yields no hops" do
|
test "empty path string yields no hops" do
|
||||||
|
|
@ -172,6 +179,21 @@ defmodule Microwaveprop.Aprs.PathParserTest do
|
||||||
assert hops == []
|
assert hops == []
|
||||||
end
|
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
|
test "Q-construct followed by IGate name yields no hops" do
|
||||||
lookup = lookup_fn(%{"IGATE-CALL" => {33.0, -96.0}})
|
lookup = lookup_fn(%{"IGATE-CALL" => {33.0, -96.0}})
|
||||||
|
|
||||||
|
|
@ -187,14 +209,16 @@ defmodule Microwaveprop.Aprs.PathParserTest do
|
||||||
assert hops == []
|
assert hops == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "self-loop (callsign resolves to sender's own position) yields no hops" do
|
test "self-loop suppressed when used digi callsign matches sender callsign" do
|
||||||
lookup = lookup_fn(%{"K5GVL-10" => @sender_pos})
|
# Sender sees its own callsign reflected in the digi path (corrupted
|
||||||
|
# frame). Should be skipped.
|
||||||
|
lookup = lookup_fn(%{@sender => {99.0, 99.0}})
|
||||||
|
|
||||||
hops =
|
hops =
|
||||||
PathParser.parse_hops(
|
PathParser.parse_hops(
|
||||||
@sender,
|
@sender,
|
||||||
@sender_pos,
|
@sender_pos,
|
||||||
"K5GVL-10*",
|
"#{@sender}*",
|
||||||
@heard_at,
|
@heard_at,
|
||||||
lookup
|
lookup
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue