- Replace apply/2 with direct fully-qualified calls in movement_test - Fix assert_receive timeouts < 1000ms across 8 test files - Move nested import statements to module-level scope - Fix tests with no assertions and add missing doctest - Replace weak type assertions with specific value checks - Fix conditional assertions and length/1 expensive patterns - Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest - Fix tests not calling application code with credo:disable - Add various credo:disable comments for legitimate patterns
146 lines
5.2 KiB
Elixir
146 lines
5.2 KiB
Elixir
defmodule Aprsme.ShutdownHandlerTest do
|
|
# async: false — shares app env and the named ShutdownHandler process.
|
|
use ExUnit.Case, async: false
|
|
|
|
alias Aprsme.ShutdownHandler
|
|
|
|
describe "handle_call(:shutting_down?, _, state)" do
|
|
test "reflects the shutting_down flag in state" do
|
|
# Pure callback invocation — no side effects.
|
|
assert {:reply, false, %{shutting_down: false}} =
|
|
ShutdownHandler.handle_call(:shutting_down?, self(), %{
|
|
shutting_down: false,
|
|
drain_timeout: 0
|
|
})
|
|
|
|
assert {:reply, true, %{shutting_down: true}} =
|
|
ShutdownHandler.handle_call(:shutting_down?, self(), %{
|
|
shutting_down: true,
|
|
drain_timeout: 0
|
|
})
|
|
end
|
|
end
|
|
|
|
describe "handle_call(:shutdown, _, state)" do
|
|
test "returns :already_shutting_down when already in progress" do
|
|
state = %{shutting_down: true, drain_timeout: 0}
|
|
|
|
assert {:reply, :already_shutting_down, ^state} =
|
|
ShutdownHandler.handle_call(:shutdown, self(), state)
|
|
end
|
|
end
|
|
|
|
describe "handle_info({:EXIT, pid, reason}, state)" do
|
|
test "is a no-op that keeps state intact" do
|
|
state = %{shutting_down: false, drain_timeout: 0}
|
|
|
|
assert {:noreply, ^state} =
|
|
ShutdownHandler.handle_info({:EXIT, self(), :normal}, state)
|
|
end
|
|
end
|
|
|
|
describe "shutting_down?/0 without the GenServer running" do
|
|
test "returns false via the rescue clause" do
|
|
# The supervised ShutdownHandler may or may not be running. If the
|
|
# GenServer.call raises, the `rescue _ -> false` kicks in and we get
|
|
# a boolean back either way.
|
|
assert ShutdownHandler.shutting_down?() == false
|
|
end
|
|
end
|
|
|
|
describe "init/1" do
|
|
test "traps exits and reads drain timeout from the environment" do
|
|
# Run in a child process so we don't trip up the test process with a
|
|
# sticky :trap_exit flag.
|
|
task =
|
|
Task.async(fn ->
|
|
prev = System.get_env("DRAIN_TIMEOUT_MS")
|
|
System.put_env("DRAIN_TIMEOUT_MS", "1234")
|
|
|
|
try do
|
|
assert {:ok, state} = ShutdownHandler.init([])
|
|
assert state.shutting_down == false
|
|
assert state.drain_timeout == 1234
|
|
assert Process.info(self(), :trap_exit) == {:trap_exit, true}
|
|
after
|
|
if prev do
|
|
System.put_env("DRAIN_TIMEOUT_MS", prev)
|
|
else
|
|
System.delete_env("DRAIN_TIMEOUT_MS")
|
|
end
|
|
end
|
|
end)
|
|
|
|
Task.await(task)
|
|
end
|
|
end
|
|
|
|
describe "handle_call(:shutdown, _, state) starting shutdown" do
|
|
test "marks state as shutting_down" do
|
|
# initiate_shutdown also schedules :begin_connection_drain after 15s via
|
|
# Process.send_after — too long to wait for in tests. We just verify the
|
|
# state transition; handle_info(:begin_connection_drain) is tested below.
|
|
state = %{shutting_down: false, drain_timeout: 50}
|
|
|
|
assert {:reply, :ok, new_state} =
|
|
ShutdownHandler.handle_call(:shutdown, self(), state)
|
|
|
|
assert new_state.shutting_down == true
|
|
end
|
|
end
|
|
|
|
describe "handle_info(:begin_connection_drain, state)" do
|
|
test "schedules force_shutdown after drain_timeout" do
|
|
state = %{shutting_down: true, drain_timeout: 50}
|
|
|
|
assert {:noreply, ^state} =
|
|
ShutdownHandler.handle_info(:begin_connection_drain, state)
|
|
|
|
assert_receive :force_shutdown, 1000
|
|
end
|
|
end
|
|
|
|
describe "terminate/2" do
|
|
test "returns :ok for normal shutdowns without invoking drain" do
|
|
state = %{shutting_down: false, drain_timeout: 60_000}
|
|
assert :ok = ShutdownHandler.terminate(:shutdown, state)
|
|
assert :ok = ShutdownHandler.terminate({:shutdown, :any}, state)
|
|
assert :ok = ShutdownHandler.terminate(:normal, state)
|
|
end
|
|
|
|
test "returns :ok when state is already shutting_down" do
|
|
# Abnormal reason, but shutting_down is already true — no drain sleep.
|
|
state = %{shutting_down: true, drain_timeout: 60_000}
|
|
assert :ok = ShutdownHandler.terminate(:crash, state)
|
|
end
|
|
end
|
|
|
|
describe "shutdown/0 as an external API" do
|
|
test "returns the call result if the GenServer is up, else raises" do
|
|
# shutting_down?/0 is safe to call — it rescues when the GenServer isn't running.
|
|
assert ShutdownHandler.shutting_down?() == false
|
|
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
|