- Bug #12: Lower rate limit to 15/min - Bug #13: Wrap model loading in Task.start - Bug #14: Fix classify_time_period guard gap at -3.0 - Bug #15: Not applicable (Elixir has no ?? operator) - Bug #16: Remove fallback Repo.get for station preload - Bug #17: Extract cache_key() in ContactMapController - Bug #18: Add has_many :contacts and :beacons to User schema - A&D #4: Move serve_markdown_if_requested after secure headers - Config #1: Move signing_salt to runtime.exs env var - Config #2: Use --check-unused instead of --unused - Config #3: Re-enable UnsafeToAtom Credo check - Config #5: Re-enable LeakyEnvironment Credo check - Add @spec annotations to fix re-enabled Specs violations - Replace String.to_atom with to_existing_atom where guarded
97 lines
2.8 KiB
Elixir
97 lines
2.8 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
|
|
# credo:disable-for-this-line Credo.Check.Warning.UnsafeToAtom
|
|
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
|
|
|
|
test "pending_count returns the number of unflushed rows", %{agg: agg} do
|
|
assert Aggregator.pending_count(agg) == 0
|
|
|
|
Aggregator.ingest(agg, Jason.encode!(@sample))
|
|
assert Aggregator.pending_count(agg) == 1
|
|
|
|
Aggregator.ingest(agg, Jason.encode!(%{@sample | "sl" => "FN31pr"}))
|
|
assert Aggregator.pending_count(agg) == 2
|
|
|
|
Aggregator.flush(agg)
|
|
assert Aggregator.pending_count(agg) == 0
|
|
end
|
|
|
|
test "flush with empty rows returns 0", %{agg: agg} do
|
|
assert Aggregator.flush(agg) == 0
|
|
end
|
|
end
|