test: expand coverage on cluster, shutdown, signal, packets helpers
This commit is contained in:
parent
340707ebf9
commit
84a61bd95c
6 changed files with 226 additions and 2 deletions
|
|
@ -303,4 +303,71 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
|
|||
assert Process.alive?(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info direct callbacks" do
|
||||
test "handle_info/2 ignores unknown messages without changing state" do
|
||||
state = %{connection_started: false}
|
||||
assert {:noreply, ^state} = ConnectionManager.handle_info(:totally_unrelated, state)
|
||||
assert {:noreply, ^state} = ConnectionManager.handle_info({:foo, :bar}, state)
|
||||
end
|
||||
|
||||
test "handle_info/2 :check_initial_state when not leader returns state unchanged" do
|
||||
# Ensure LeaderElection is NOT running so leader_check returns false (via :exit catch)
|
||||
if Process.whereis(LeaderElection) do
|
||||
GenServer.stop(LeaderElection)
|
||||
end
|
||||
|
||||
state = %{connection_started: false}
|
||||
|
||||
assert {:noreply, ^state} =
|
||||
ConnectionManager.handle_info(:check_initial_state, state)
|
||||
end
|
||||
|
||||
test "handle_info/2 :check_initial_state with running leader starts connection" do
|
||||
Application.put_env(:aprsme, :cluster_enabled, false)
|
||||
|
||||
case Process.whereis(LeaderElection) do
|
||||
nil ->
|
||||
{:ok, _} = LeaderElection.start_link([])
|
||||
Process.sleep(200)
|
||||
|
||||
_pid ->
|
||||
:ok
|
||||
end
|
||||
|
||||
state = %{connection_started: false}
|
||||
|
||||
# Triggers start_aprs_connection via DynamicSupervisor — may succeed,
|
||||
# fail, or report already_started, but must not crash.
|
||||
assert {:noreply, new_state} =
|
||||
ConnectionManager.handle_info(:check_initial_state, state)
|
||||
|
||||
assert new_state.connection_started == true
|
||||
end
|
||||
|
||||
test "handle_info/2 leadership_change — us becoming leader starts connection" do
|
||||
state = %{connection_started: false}
|
||||
|
||||
assert {:noreply, new_state} =
|
||||
ConnectionManager.handle_info({:leadership_change, node(), true}, state)
|
||||
|
||||
assert new_state.connection_started == true
|
||||
end
|
||||
|
||||
test "handle_info/2 leadership_change — us losing leadership stops connection" do
|
||||
state = %{connection_started: true}
|
||||
|
||||
assert {:noreply, new_state} =
|
||||
ConnectionManager.handle_info({:leadership_change, node(), false}, state)
|
||||
|
||||
assert new_state.connection_started == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "terminate/2" do
|
||||
test "unsubscribes from the cluster:leadership topic and returns :ok" do
|
||||
state = %{connection_started: false}
|
||||
assert :ok = ConnectionManager.terminate(:normal, state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -479,4 +479,60 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
|
|||
assert {:reply, :node_a, ^state} = LeaderElection.handle_call(:current_leader, self(), state)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_cluster_aprs_status/0 detailed branches" do
|
||||
test "returns a map with cluster_info when clustering is enabled and no peers connected" do
|
||||
# Single-node cluster — Node.list() == []; get_cluster_wide_status
|
||||
# falls into the "no connected statuses" branch.
|
||||
Application.put_env(:aprsme, :cluster_enabled, true)
|
||||
|
||||
status = LeaderElection.get_cluster_aprs_status()
|
||||
assert is_map(status)
|
||||
|
||||
if Map.has_key?(status, :cluster_info) do
|
||||
assert status.cluster_info.total_nodes >= 1
|
||||
assert is_list(status.cluster_info.all_nodes)
|
||||
# leader_node is a string (including "none" when no leader).
|
||||
assert is_binary(status.cluster_info.leader_node)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info(:check_leadership) direct callback" do
|
||||
test "no-op for a non-leader — reschedules and keeps state" do
|
||||
state = %LeaderElection{is_leader: false, leader_node: nil}
|
||||
|
||||
assert {:noreply, new_state} =
|
||||
LeaderElection.handle_info(:check_leadership, state)
|
||||
|
||||
assert new_state.is_leader == false
|
||||
end
|
||||
|
||||
test "leader with intact global registration stays leader" do
|
||||
# Pre-register ourselves globally so verify_leadership/1 sees pid == self().
|
||||
:global.unregister_name(@election_key)
|
||||
:global.register_name(@election_key, self())
|
||||
|
||||
state = %LeaderElection{is_leader: true, leader_node: node()}
|
||||
|
||||
assert {:noreply, new_state} =
|
||||
LeaderElection.handle_info(:check_leadership, state)
|
||||
|
||||
assert new_state.is_leader == true
|
||||
|
||||
:global.unregister_name(@election_key)
|
||||
end
|
||||
|
||||
test "leader whose registration was lost steps down" do
|
||||
:global.unregister_name(@election_key)
|
||||
|
||||
state = %LeaderElection{is_leader: true, leader_node: node()}
|
||||
|
||||
assert {:noreply, new_state} =
|
||||
LeaderElection.handle_info(:check_leadership, state)
|
||||
|
||||
assert new_state.is_leader == false
|
||||
assert new_state.leader_node == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -92,6 +92,18 @@ defmodule Aprsme.DeviceIdentificationTest do
|
|||
test "returns empty list for unknown manufacturer" do
|
||||
assert DeviceIdentification.known_models("Unknown") == []
|
||||
end
|
||||
|
||||
test "returns list of known models for Byonics" do
|
||||
models = DeviceIdentification.known_models("Byonics")
|
||||
assert "TinyTrack3" in models
|
||||
assert "TinyTrack4" in models
|
||||
end
|
||||
|
||||
test "returns list of known models for SCS GmbH & Co." do
|
||||
models = DeviceIdentification.known_models("SCS GmbH & Co.")
|
||||
assert "P4dragon DR-7400 modems" in models
|
||||
assert "P4dragon DR-7800 modems" in models
|
||||
end
|
||||
end
|
||||
|
||||
describe "upsert_devices/1" do
|
||||
|
|
|
|||
|
|
@ -367,6 +367,58 @@ defmodule Aprsme.PacketsTest do
|
|||
# Either stores successfully without position or validation fails.
|
||||
assert match?({:ok, _}, result) or match?({:error, _}, result)
|
||||
end
|
||||
|
||||
test "extracts southern-hemisphere coordinates from a MicE struct via apply_direction" do
|
||||
packet_data = %{
|
||||
sender: "MICESOUTH",
|
||||
base_callsign: "MICESOUTH",
|
||||
ssid: "0",
|
||||
destination: "APRS",
|
||||
raw_packet: "MICESOUTH>APRS",
|
||||
data_type: "mic_e",
|
||||
data_extended: %MicE{
|
||||
lat_degrees: 33,
|
||||
lat_minutes: 30,
|
||||
lat_direction: :south,
|
||||
lon_degrees: 96,
|
||||
lon_minutes: 30,
|
||||
lon_direction: :east
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, packet} = Packets.store_packet(packet_data)
|
||||
# :south branch must negate the latitude; :east keeps longitude positive.
|
||||
# Packet.lat is stored as Decimal — compare via Decimal.to_float.
|
||||
lat = Decimal.to_float(packet.lat)
|
||||
lon = Decimal.to_float(packet.lon)
|
||||
assert lat < 0
|
||||
assert lon > 0
|
||||
end
|
||||
|
||||
test "extracts northern + western coordinates from a MicE struct via apply_direction" do
|
||||
packet_data = %{
|
||||
sender: "MICENW",
|
||||
base_callsign: "MICENW",
|
||||
ssid: "0",
|
||||
destination: "APRS",
|
||||
raw_packet: "MICENW>APRS",
|
||||
data_type: "mic_e",
|
||||
data_extended: %MicE{
|
||||
lat_degrees: 33,
|
||||
lat_minutes: 30,
|
||||
lat_direction: :north,
|
||||
lon_degrees: 96,
|
||||
lon_minutes: 30,
|
||||
lon_direction: :west
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, packet} = Packets.store_packet(packet_data)
|
||||
lat = Decimal.to_float(packet.lat)
|
||||
lon = Decimal.to_float(packet.lon)
|
||||
assert lat > 0
|
||||
assert lon < 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "store_packet/1 error paths" do
|
||||
|
|
|
|||
|
|
@ -123,4 +123,26 @@ defmodule Aprsme.ShutdownHandlerTest do
|
|||
assert function_exported?(ShutdownHandler, :shutting_down?, 0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "terminate/2 with abnormal reason" do
|
||||
test "runs drain path with a tiny drain_timeout and returns :ok" do
|
||||
# Use a near-zero drain_timeout so the Process.sleep inside the graceful
|
||||
# drain path is effectively instant. This exercises mark_unhealthy +
|
||||
# stop_accepting_connections + Process.sleep without blocking the suite.
|
||||
prev_status = Application.get_env(:aprsme, :health_status)
|
||||
state = %{shutting_down: false, drain_timeout: 1}
|
||||
|
||||
try do
|
||||
assert :ok = ShutdownHandler.terminate(:killed, state)
|
||||
# Drain path flips the app's health status to :draining.
|
||||
assert Application.get_env(:aprsme, :health_status) == :draining
|
||||
after
|
||||
if prev_status do
|
||||
Application.put_env(:aprsme, :health_status, prev_status)
|
||||
else
|
||||
Application.delete_env(:aprsme, :health_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,8 +10,23 @@ defmodule Aprsme.SignalHandlerTest do
|
|||
assert SignalHandler.handle_info({:other, "msg"}, state) == {:noreply, state}
|
||||
end
|
||||
|
||||
# We can't safely exercise the :sigterm clause here — it calls
|
||||
# Aprsme.ShutdownHandler.shutdown() which would actually shut the app down.
|
||||
test "sigterm branch spawn_links a shutdown task without crashing the caller" do
|
||||
# Trap exits so the spawn_link from handle_info doesn't bring the test down
|
||||
# when the shutdown task inevitably fails (ShutdownHandler is either not
|
||||
# running or would actually stop the VM if it were).
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
assert {:noreply, :state_x} =
|
||||
SignalHandler.handle_info({:signal, :sigterm}, :state_x)
|
||||
|
||||
# Drain any :EXIT messages from the spawned task; we don't care about
|
||||
# the outcome, only that the callback ran.
|
||||
receive do
|
||||
{:EXIT, _pid, _reason} -> :ok
|
||||
after
|
||||
200 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "init/1" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue