Add coverage tests for DevicesSeeder, StatusLive helpers, DataBuilder, and HistoricalLoader
This commit is contained in:
parent
1e8e97d5f4
commit
aa809976ab
5 changed files with 259 additions and 0 deletions
84
test/aprsme/devices_seeder_test.exs
Normal file
84
test/aprsme/devices_seeder_test.exs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
defmodule Aprsme.DevicesSeederTest do
|
||||
use Aprsme.DataCase, async: false
|
||||
|
||||
alias Aprsme.Devices
|
||||
alias Aprsme.DevicesSeeder
|
||||
alias Aprsme.Repo
|
||||
|
||||
setup do
|
||||
Repo.delete_all(Devices)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "seed_from_json/1" do
|
||||
test "seeds the devices table from the default test JSON file" do
|
||||
assert {:ok, :seeded} = DevicesSeeder.seed_from_json()
|
||||
assert Repo.aggregate(Devices, :count) > 0
|
||||
end
|
||||
|
||||
test "returns an error tuple when the file does not exist" do
|
||||
assert {:error, message} = DevicesSeeder.seed_from_json("nonexistent/file.json")
|
||||
assert message =~ "Failed to read file"
|
||||
end
|
||||
|
||||
test "returns an error tuple when the JSON is malformed" do
|
||||
tmp = Path.join(System.tmp_dir!(), "bad_devices_#{System.unique_integer([:positive])}.json")
|
||||
File.write!(tmp, "{malformed json")
|
||||
|
||||
try do
|
||||
assert {:error, message} = DevicesSeeder.seed_from_json(tmp)
|
||||
assert message =~ "Failed to decode JSON"
|
||||
after
|
||||
File.rm(tmp)
|
||||
end
|
||||
end
|
||||
|
||||
test "wipes existing devices before seeding" do
|
||||
Repo.insert!(%Devices{identifier: "TOWIPE", vendor: "v", model: "m"})
|
||||
assert {:ok, :seeded} = DevicesSeeder.seed_from_json()
|
||||
assert Repo.get_by(Devices, identifier: "TOWIPE") == nil
|
||||
end
|
||||
|
||||
test "wraps a non-list features value into a single-element list" do
|
||||
tmp = Path.join(System.tmp_dir!(), "single_feature_#{System.unique_integer([:positive])}.json")
|
||||
|
||||
File.write!(
|
||||
tmp,
|
||||
Jason.encode!(%{
|
||||
"tocalls" => %{
|
||||
"APSGL" => %{"vendor" => "V", "model" => "M", "features" => "single-feature"}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
try do
|
||||
assert {:ok, :seeded} = DevicesSeeder.seed_from_json(tmp)
|
||||
dev = Repo.get_by(Devices, identifier: "APSGL")
|
||||
assert dev.features == ["single-feature"]
|
||||
after
|
||||
File.rm(tmp)
|
||||
end
|
||||
end
|
||||
|
||||
test "preserves a list-typed features value as-is" do
|
||||
tmp = Path.join(System.tmp_dir!(), "list_feature_#{System.unique_integer([:positive])}.json")
|
||||
|
||||
File.write!(
|
||||
tmp,
|
||||
Jason.encode!(%{
|
||||
"tocalls" => %{
|
||||
"APLST" => %{"vendor" => "V", "model" => "M", "features" => ["a", "b"]}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
try do
|
||||
assert {:ok, :seeded} = DevicesSeeder.seed_from_json(tmp)
|
||||
dev = Repo.get_by(Devices, identifier: "APLST")
|
||||
assert dev.features == ["a", "b"]
|
||||
after
|
||||
File.rm(tmp)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -199,6 +199,31 @@ defmodule Aprsme.PartitionManagerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "start_link/1" do
|
||||
test "starts the GenServer process with the module name" do
|
||||
# If a previous test left one running, just confirm start_link works.
|
||||
case Process.whereis(PartitionManager) do
|
||||
nil ->
|
||||
{:ok, pid} = PartitionManager.start_link([])
|
||||
assert Process.alive?(pid)
|
||||
GenServer.stop(pid)
|
||||
|
||||
pid ->
|
||||
assert Process.alive?(pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "manage_partitions/0 idempotent paths" do
|
||||
test "running :manage_partitions twice covers the {:ok, []} branches" do
|
||||
# First run creates today/+1/+2 partitions (or finds them existing) and
|
||||
# drops nothing old. Second run finds everything in place — both
|
||||
# ensure_partitions_exist and drop_old_partitions return {:ok, []}.
|
||||
assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})
|
||||
assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "manage_partitions/0 via :manage_partitions handler with non-trivial work" do
|
||||
test "logs created+dropped partitions via the success branches" do
|
||||
# Pre-create a partition far in the past so manage_partitions has
|
||||
|
|
|
|||
|
|
@ -156,6 +156,69 @@ defmodule AprsmeWeb.MapLive.DataBuilderExtrasTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "build_packet_result without :id falls back to callsign-based ID" do
|
||||
test "live packet without :id uses callsign in the generated ID" do
|
||||
# When packet has no :id key, the build_packet_result helper uses the
|
||||
# callsign to generate the marker ID (line 473 in data_builder.ex).
|
||||
packet = %{
|
||||
"sender" => "NOID-1",
|
||||
"callsign" => "NOID-1",
|
||||
"received_at" => DateTime.utc_now(),
|
||||
"lat" => 33.0,
|
||||
"lon" => -96.0,
|
||||
"comment" => "no id"
|
||||
}
|
||||
|
||||
result = DataBuilder.build_packet_data(packet, true)
|
||||
# Build should succeed with a generated id beginning with "live_".
|
||||
if is_map(result) do
|
||||
assert is_binary(result["id"])
|
||||
assert String.starts_with?(result["id"], "live_")
|
||||
else
|
||||
# build_packet_data may filter — that's also OK, the helper still ran.
|
||||
assert true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "convert_tuples_to_strings via popup data with deeply nested tuples" do
|
||||
test "tuples of size > 3 fall through to inspect/1" do
|
||||
packet = %{
|
||||
"id" => "big-tup",
|
||||
"sender" => "BIG-1",
|
||||
"callsign" => "BIG-1",
|
||||
"received_at" => DateTime.utc_now(),
|
||||
"lat" => 33.0,
|
||||
"lon" => -96.0,
|
||||
# Tuples of size 4 and 5 trigger the inspect/1 fallback in
|
||||
# convert_tuples_to_strings/1 (line 587).
|
||||
"data" => %{
|
||||
"quad" => {1, 2, 3, 4},
|
||||
"five" => {1, 2, 3, 4, 5}
|
||||
}
|
||||
}
|
||||
|
||||
_ = DataBuilder.build_simple_popup(packet, false)
|
||||
end
|
||||
|
||||
test "list of tuples is processed via the list head" do
|
||||
packet = %{
|
||||
"id" => "list-tup",
|
||||
"sender" => "LST-1",
|
||||
"callsign" => "LST-1",
|
||||
"received_at" => DateTime.utc_now(),
|
||||
"lat" => 33.0,
|
||||
"lon" => -96.0,
|
||||
"data" => [
|
||||
{1, 2},
|
||||
{3, 4, 5}
|
||||
]
|
||||
}
|
||||
|
||||
_ = DataBuilder.build_simple_popup(packet, false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_packet_data with string-typed weather values exercises convert_unit" do
|
||||
test "string numeric weather values parse via Float.parse branch" do
|
||||
packet = %{
|
||||
|
|
|
|||
|
|
@ -444,6 +444,48 @@ defmodule AprsmeWeb.MapLive.HistoricalLoaderTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "schedules next batch when not final" do
|
||||
test "saturated batch with more rows triggers maybe_schedule_next_batch" do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Return a saturated batch so has_more = true and is_final_batch = false.
|
||||
# Default batch_size for zoom 10 is 500 — return a list of that size.
|
||||
packets =
|
||||
for i <- 1..500 do
|
||||
%{
|
||||
id: "saturate-#{i}",
|
||||
sender: "SAT-#{i}",
|
||||
base_callsign: "SAT",
|
||||
ssid: "1",
|
||||
lat: 33.5 + i * 0.0001,
|
||||
lon: -96.5 + i * 0.0001,
|
||||
has_position: true,
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
received_at: DateTime.add(now, -i, :second),
|
||||
path: ""
|
||||
}
|
||||
end
|
||||
|
||||
Mox.stub(Aprsme.PacketsMock, :get_recent_packets, fn _opts -> packets end)
|
||||
|
||||
socket =
|
||||
build_socket(%{
|
||||
map_bounds: %{north: 34.0, south: 32.0, east: -95.0, west: -97.0},
|
||||
map_zoom: 10,
|
||||
historical_loading: true,
|
||||
loading_generation: 1,
|
||||
# 4 batches expected, this is batch 0 — so not final
|
||||
total_batches: 4
|
||||
})
|
||||
|
||||
result = HistoricalLoader.load_historical_batch(socket, 0, nil)
|
||||
|
||||
# maybe_schedule_next_batch should have queued a timer.
|
||||
assert is_list(result.assigns.pending_batch_tasks)
|
||||
end
|
||||
end
|
||||
|
||||
describe "max packets-for-zoom limit" do
|
||||
test "finishes loading when batch_offset * batch_size exceeds zoom limit" do
|
||||
# At zoom 10, limit=1500, batch_size=500. A batch_offset of 3 exceeds
|
||||
|
|
|
|||
|
|
@ -165,6 +165,51 @@ defmodule AprsmeWeb.StatusLive.IndexTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "format_uptime/1" do
|
||||
test "returns 'Not connected' when seconds <= 0" do
|
||||
assert Index.format_uptime(0) =~ "Not connected"
|
||||
assert Index.format_uptime(-100) =~ "Not connected"
|
||||
end
|
||||
|
||||
test "formats seconds-only durations" do
|
||||
assert Index.format_uptime(45) == "45s"
|
||||
end
|
||||
|
||||
test "formats minutes+seconds durations" do
|
||||
assert Index.format_uptime(125) == "2m 5s"
|
||||
end
|
||||
|
||||
test "formats hours+minutes+seconds durations" do
|
||||
assert Index.format_uptime(3 * 3600 + 45 * 60 + 30) == "3h 45m 30s"
|
||||
end
|
||||
|
||||
test "formats day+hour+minute+second durations" do
|
||||
total = 2 * 86_400 + 3 * 3600 + 4 * 60 + 5
|
||||
assert Index.format_uptime(total) == "2d 3h 4m 5s"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_health_description/2" do
|
||||
test "score 1 returns disconnected" do
|
||||
assert Index.get_health_description(1, false) =~ "Disconnected"
|
||||
end
|
||||
|
||||
test "score 5 connected returns excellent/healthy text" do
|
||||
result = Index.get_health_description(5, true)
|
||||
assert is_binary(result)
|
||||
end
|
||||
|
||||
test "all defined score combinations return non-empty text" do
|
||||
for score <- 1..5 do
|
||||
for connected <- [true, false] do
|
||||
result = Index.get_health_description(score, connected)
|
||||
assert is_binary(result)
|
||||
assert byte_size(result) > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "render with cluster_info populated" do
|
||||
test "renders the Cluster Status section when cluster_info is present", %{conn: conn} do
|
||||
_ =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue