prop/test/microwaveprop/pskr_test.exs

235 lines
7.7 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.PskrTest do
use ExUnit.Case, async: true
alias Microwaveprop.Pskr
@sample %{
"sq" => 30_142_870_791,
"f" => 144_174_000,
"md" => "FT8",
"rp" => -5,
"t" => 1_662_407_712,
"t_tx" => 1_662_407_697,
"sc" => "K5ABC",
"sl" => "EM12kl",
"rc" => "K7XYZ",
"rl" => "DM43st",
"sa" => 291,
"ra" => 291,
"b" => "2m"
}
describe "parse_spot/1" do
test "preserves full subsquare precision when reported" do
json = Jason.encode!(@sample)
assert {:ok, spot} = Pskr.parse_spot(json)
assert spot.band == "2m"
assert spot.mode == "FT8"
assert spot.snr_db == -5
assert spot.sender_grid == "EM12KL"
assert spot.receiver_grid == "DM43ST"
assert spot.sender_callsign == "K5ABC"
assert spot.receiver_callsign == "K7XYZ"
assert spot.sender_country == 291
assert spot.receiver_country == 291
assert %DateTime{} = spot.transmit_time
assert DateTime.to_unix(spot.transmit_time) == 1_662_407_697
end
test "rejects 4-char grids on either end (insufficient precision)" do
# A 4-char locator is ~70 km × 100 km — the midpoint and
# distance error swamps any HRRR cell match (3 km native).
# Drop the spot rather than store a calibration sample whose
# geometry is meaningless.
sender_short = @sample |> Map.put("sl", "EM12") |> Jason.encode!()
receiver_short = @sample |> Map.put("rl", "DM43") |> Jason.encode!()
assert {:error, _} = Pskr.parse_spot(sender_short)
assert {:error, _} = Pskr.parse_spot(receiver_short)
end
test "preserves extended-square (8-char) precision" do
json = @sample |> Map.merge(%{"sl" => "EM12kl37", "rl" => "DM43st99"}) |> Jason.encode!()
assert {:ok, spot} = Pskr.parse_spot(json)
assert spot.sender_grid == "EM12KL37"
assert spot.receiver_grid == "DM43ST99"
end
test "rejects odd-length grids (truncated 5-char)" do
json = @sample |> Map.put("sl", "EM12k") |> Jason.encode!()
assert {:error, _} = Pskr.parse_spot(json)
end
test "uses :t when :t_tx is missing" do
json = @sample |> Map.delete("t_tx") |> Jason.encode!()
assert {:ok, spot} = Pskr.parse_spot(json)
assert DateTime.to_unix(spot.transmit_time) == 1_662_407_712
end
test "rejects payloads without grids" do
json = @sample |> Map.delete("sl") |> Jason.encode!()
assert {:error, _} = Pskr.parse_spot(json)
end
test "rejects malformed JSON" do
assert {:error, _} = Pskr.parse_spot("not-json")
end
end
describe "path_key/1" do
test "buckets to the top of the hour using the full grid as reported" do
{:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
{hour, band, snd, rcv} = Pskr.path_key(spot)
assert band == "2m"
assert snd == "EM12KL"
assert rcv == "DM43ST"
# 1_662_407_697 → 2022-09-05T19:54:57Z → bucket at 19:00:00Z
assert hour == ~U[2022-09-05 19:00:00Z]
end
end
describe "merge_spot/2" do
test "initializes accumulator from a single spot" do
{:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
acc = Pskr.merge_spot(nil, spot)
assert acc.spot_count == 1
assert acc.max_snr_db == -5
assert acc.min_snr_db == -5
assert acc.modes == ["FT8"]
assert acc.sender_grid == "EM12KL"
assert acc.receiver_grid == "DM43ST"
assert is_float(acc.distance_km)
assert acc.distance_km > 0
# Midpoint should land between the two grid centers
assert acc.midpoint_lat > min(acc.sender_lat, acc.receiver_lat)
assert acc.midpoint_lat < max(acc.sender_lat, acc.receiver_lat)
end
test "increments count and tracks SNR envelope across spots" do
{:ok, first} = Pskr.parse_spot(Jason.encode!(@sample))
{:ok, louder} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -1, "md" => "FT4"}))
{:ok, quieter} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -18}))
acc =
nil
|> Pskr.merge_spot(first)
|> Pskr.merge_spot(louder)
|> Pskr.merge_spot(quieter)
assert acc.spot_count == 3
assert acc.max_snr_db == -1
assert acc.min_snr_db == -18
assert Enum.sort(acc.modes) == ["FT4", "FT8"]
assert DateTime.compare(acc.first_spot_at, acc.last_spot_at) in [:lt, :eq]
end
end
describe "parse_spot/1 edge cases" do
test "rejects empty band string" do
json = @sample |> Map.put("b", "") |> Jason.encode!()
assert {:error, _} = Pskr.parse_spot(json)
end
test "rejects payload with no timestamp fields" do
json = @sample |> Map.delete("t_tx") |> Map.delete("t") |> Jason.encode!()
assert {:error, _} = Pskr.parse_spot(json)
end
test "handles nil snr gracefully in merge" do
json = @sample |> Map.put("rp", nil) |> Jason.encode!()
{:ok, spot} = Pskr.parse_spot(json)
acc = Pskr.merge_spot(nil, spot)
assert acc.max_snr_db == nil
assert acc.min_snr_db == nil
{:ok, spot2} = Pskr.parse_spot(Jason.encode!(@sample))
acc2 = Pskr.merge_spot(acc, spot2)
assert acc2.max_snr_db == -5
assert acc2.min_snr_db == -5
end
test "handles empty mode string in merge" do
json = @sample |> Map.put("md", "") |> Jason.encode!()
{:ok, spot} = Pskr.parse_spot(json)
acc = Pskr.merge_spot(nil, spot)
assert acc.modes == []
end
test "handles nil mode in merge" do
json = @sample |> Map.delete("md") |> Jason.encode!()
{:ok, spot} = Pskr.parse_spot(json)
acc = Pskr.merge_spot(nil, spot)
assert acc.modes == []
end
test "does not add duplicate modes" do
{:ok, first} = Pskr.parse_spot(Jason.encode!(@sample))
{:ok, second} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -10}))
acc = nil |> Pskr.merge_spot(first) |> Pskr.merge_spot(second)
assert acc.modes == ["FT8"]
end
end
describe "callsign tracking" do
test "collects unique sender and receiver callsigns across merges" do
{:ok, s1} = Pskr.parse_spot(Jason.encode!(@sample))
{:ok, s2} = Pskr.parse_spot(Jason.encode!(%{@sample | "sc" => "K5DEF", "rp" => -3}))
acc =
nil
|> Pskr.merge_spot(s1)
|> Pskr.merge_spot(s2)
assert Enum.sort(acc.sender_callsigns) == ["K5ABC", "K5DEF"]
assert acc.receiver_callsigns == ["K7XYZ"]
assert acc.spot_count == 2
end
test "does not add duplicate callsigns" do
{:ok, s1} = Pskr.parse_spot(Jason.encode!(@sample))
{:ok, s2} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -3}))
acc =
nil
|> Pskr.merge_spot(s1)
|> Pskr.merge_spot(s2)
assert acc.sender_callsigns == ["K5ABC"]
assert acc.receiver_callsigns == ["K7XYZ"]
end
test "handles nil and empty callsigns in payload" do
nil_call = @sample |> Map.put("sc", nil) |> Jason.encode!()
{:ok, s_nil} = Pskr.parse_spot(nil_call)
assert s_nil.sender_callsign == nil
empty_call = @sample |> Map.put("sc", "") |> Jason.encode!()
{:ok, s_empty} = Pskr.parse_spot(empty_call)
assert s_empty.sender_callsign == nil
acc = nil |> Pskr.merge_spot(s_nil) |> Pskr.merge_spot(s_empty)
assert acc.sender_callsigns == []
end
test "uppercases callsigns from payload" do
json = @sample |> Map.merge(%{"sc" => "k5abc", "rc" => "k7xyz"}) |> Jason.encode!()
{:ok, spot} = Pskr.parse_spot(json)
assert spot.sender_callsign == "K5ABC"
assert spot.receiver_callsign == "K7XYZ"
end
test "initializes callsign arrays from first spot" do
{:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
acc = Pskr.merge_spot(nil, spot)
assert acc.sender_callsigns == ["K5ABC"]
assert acc.receiver_callsigns == ["K7XYZ"]
end
end
end