Spots that include subsquare (EM12kl), extended-square (EM12kl37), or extended-extended-subsquare (EM12kl37ab) precision now keep that resolution end-to-end. Previously we truncated everything to 4 chars at parse time, which collapsed distinct paths into the same row whenever two spotters shared a field. The aggregation key uses the full grid as reported, so two spotters in different subsquares of `EM12` produce two rows — correct, since they are physically different paths. Any spot that fails Maidenhead validation (odd length, illegal char at position N) is still dropped via the same error path. Storage normalizes to uppercase so the unique index doesn't split on case. Maidenhead.valid?/1 already accepts both cases on input.
79 lines
2.2 KiB
Elixir
79 lines
2.2 KiB
Elixir
defmodule Microwaveprop.Pskr.AggregatorTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Ecto.Adapters.SQL.Sandbox
|
|
alias Microwaveprop.Pskr.Aggregator
|
|
alias Microwaveprop.Pskr.SpotHourly
|
|
alias Microwaveprop.Repo
|
|
|
|
@sample %{
|
|
"f" => 144_174_000,
|
|
"md" => "FT8",
|
|
"rp" => -5,
|
|
"t_tx" => 1_662_407_697,
|
|
"sc" => "K5ABC",
|
|
"sl" => "EM12kl",
|
|
"rc" => "K7XYZ",
|
|
"rl" => "DM43st",
|
|
"sa" => 291,
|
|
"ra" => 291,
|
|
"b" => "2m"
|
|
}
|
|
|
|
setup do
|
|
pid = start_supervised!({Aggregator, name: :"agg_#{:erlang.unique_integer([:positive])}", flush_ms: 0})
|
|
Sandbox.allow(Repo, self(), pid)
|
|
%{agg: pid}
|
|
end
|
|
|
|
test "ingest collapses repeats on the same path-hour", %{agg: agg} do
|
|
payloads = [
|
|
Jason.encode!(@sample),
|
|
Jason.encode!(%{@sample | "rp" => -3}),
|
|
Jason.encode!(%{@sample | "rp" => -12, "md" => "FT4"})
|
|
]
|
|
|
|
Enum.each(payloads, &Aggregator.ingest(agg, &1))
|
|
|
|
assert Aggregator.flush(agg) == 1
|
|
[row] = Repo.all(SpotHourly)
|
|
|
|
assert row.spot_count == 3
|
|
assert row.max_snr_db == -3
|
|
assert row.min_snr_db == -12
|
|
assert Enum.sort(row.modes) == ["FT4", "FT8"]
|
|
assert row.band == "2m"
|
|
assert row.sender_grid == "EM12KL"
|
|
assert row.receiver_grid == "DM43ST"
|
|
assert row.distance_km > 0
|
|
end
|
|
|
|
test "two paths in the same hour produce two rows", %{agg: agg} do
|
|
Aggregator.ingest(agg, Jason.encode!(@sample))
|
|
Aggregator.ingest(agg, Jason.encode!(%{@sample | "sl" => "FN31pr"}))
|
|
|
|
assert Aggregator.flush(agg) == 2
|
|
assert Repo.aggregate(SpotHourly, :count) == 2
|
|
end
|
|
|
|
test "second flush merges into existing row via upsert", %{agg: agg} do
|
|
Aggregator.ingest(agg, Jason.encode!(@sample))
|
|
assert Aggregator.flush(agg) == 1
|
|
|
|
Aggregator.ingest(agg, Jason.encode!(%{@sample | "rp" => 5}))
|
|
assert Aggregator.flush(agg) == 1
|
|
|
|
[row] = Repo.all(SpotHourly)
|
|
assert row.spot_count == 2
|
|
assert row.max_snr_db == 5
|
|
assert row.min_snr_db == -5
|
|
end
|
|
|
|
test "malformed payloads are dropped silently", %{agg: agg} do
|
|
Aggregator.ingest(agg, "not json")
|
|
Aggregator.ingest(agg, Jason.encode!(%{"oops" => true}))
|
|
Aggregator.ingest(agg, Jason.encode!(@sample))
|
|
|
|
assert Aggregator.flush(agg) == 1
|
|
end
|
|
end
|