faster tests

This commit is contained in:
Graham McIntire 2026-04-24 19:00:03 -05:00
parent fa0eb7704b
commit ec0a64cf5f
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
12 changed files with 86 additions and 86 deletions

View file

@ -61,6 +61,16 @@ config :aprsme,
# Disable automatic migrations during tests
config :aprsme, auto_migrate: false
config :aprsme, device_cache_initial_load_delay_ms: 0
# Short Req timeouts so HTTP calls fail fast without real network access.
# retry: false prevents Req's default exponential backoff (1s+2s+4s=7s).
config :aprsme, device_id_req_opts: [connect_options: [timeout: 50], receive_timeout: 50, retry: false]
config :aprsme, election_initial_delay_ms: 0
# Speed up time-sensitive delays in test environment
config :aprsme, is_call_timeout_ms: 100
config :aprsme, is_init_delay_ms: 0
# Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1

View file

@ -81,7 +81,11 @@ defmodule Aprsme.Cluster.LeaderElection do
# Non-clustered mode: elect immediately.
defp schedule_initial_election(false) do
Logger.info("Clustering disabled - proceeding with immediate leader election")
Process.send_after(self(), :attempt_election, 100)
delay = Application.get_env(:aprsme, :election_initial_delay_ms, 100)
if delay == 0,
do: send(self(), :attempt_election),
else: Process.send_after(self(), :attempt_election, delay)
end
@impl true
@ -100,7 +104,7 @@ defmodule Aprsme.Cluster.LeaderElection do
else
Logger.info("Cluster formed with #{length(connected_nodes)} other nodes: #{inspect(connected_nodes)}")
Logger.info("Proceeding with leader election")
Process.send_after(self(), :attempt_election, 100)
schedule_election_attempt()
{:noreply, %{state | election_forced: true}}
end
end
@ -123,7 +127,7 @@ defmodule Aprsme.Cluster.LeaderElection do
)
end
Process.send_after(self(), :attempt_election, 100)
schedule_election_attempt()
{:noreply, %{state | election_forced: true}}
else
{:noreply, state}
@ -157,7 +161,7 @@ defmodule Aprsme.Cluster.LeaderElection do
# Re-attempt election if we're not leader
_ =
if not state.is_leader do
Process.send_after(self(), :attempt_election, 100)
schedule_election_attempt()
end
# Schedule next check
@ -198,6 +202,14 @@ defmodule Aprsme.Cluster.LeaderElection do
# Verify that a node which thinks it's leader still holds the :global registration.
# After :global conflict resolution (e.g. two partitions merging), the losing PID
# is silently unregistered — this detects that and steps down.
defp schedule_election_attempt do
delay = Application.get_env(:aprsme, :election_initial_delay_ms, 100)
if delay == 0,
do: send(self(), :attempt_election),
else: Process.send_after(self(), :attempt_election, delay)
end
defp verify_leadership(%{is_leader: true} = state) do
case :global.whereis_name(@election_key) do
pid when pid == self() ->

View file

@ -57,7 +57,7 @@ defmodule Aprsme.DeviceCache do
@impl true
def init(_) do
# Delay initial load to allow Redis connections to establish
Process.send_after(self(), :initial_load, 1_000)
Process.send_after(self(), :initial_load, Application.get_env(:aprsme, :device_cache_initial_load_delay_ms, 1_000))
{:ok, %{initial_load_done: false}}
end

View file

@ -155,7 +155,9 @@ defmodule Aprsme.DeviceIdentification do
@doc false
# Exposed for testing via URL injection.
def fetch_devices_from_url(url \\ @url) do
case Req.get(url) do
req_opts = Application.get_env(:aprsme, :device_id_req_opts, [])
case Req.get(url, req_opts) do
{:ok, %Req.Response{status: 200, body: body}} ->
upsert_devices(body)

View file

@ -66,7 +66,7 @@ defmodule Aprsme.Is do
Process.flag(:trap_exit, true)
# Add a small delay to prevent rapid reconnection attempts
Process.sleep(2000)
Process.sleep(Application.get_env(:aprsme, :is_init_delay_ms, 2_000))
# Get startup parameters
server = Application.get_env(:aprsme, :aprs_is_server, ~c"dallas.aprs2.net")
@ -132,7 +132,7 @@ defmodule Aprsme.Is do
defp status_from(nil), do: disconnected_status(nil)
defp status_from(pid) when is_pid(pid) do
GenServer.call(__MODULE__, :get_status, 5000)
GenServer.call(__MODULE__, :get_status, Application.get_env(:aprsme, :is_call_timeout_ms, 5_000))
catch
# GenServer exists but isn't responding — treat as disconnected but
# fall back to the rotate.aprs2.net default server string.

View file

@ -79,12 +79,12 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
# Start a minimal LeaderElection to prevent crashes
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(150)
LeaderElection.leader?()
:ok
end
test "subscribes to leadership changes" do
{:ok, _pid} = ConnectionManager.start_link([])
{:ok, pid} = ConnectionManager.start_link([])
# Verify subscription by publishing a test message
Phoenix.PubSub.broadcast(
@ -93,8 +93,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
{:leadership_change, :test_node, false}
)
# Give time for message processing
Process.sleep(50)
# Sync with the GenServer so the broadcast has been processed.
:sys.get_state(pid)
# Process should still be alive after receiving message
assert Process.whereis(ConnectionManager)
@ -103,8 +103,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
test "schedules initial state check" do
{:ok, pid} = ConnectionManager.start_link([])
# With delay set to 0 in test config, the check fires immediately
Process.sleep(50)
# Sync so the initial check (delay=0 in test config) has fired.
:sys.get_state(pid)
# Process should still be alive
assert Process.alive?(pid)
@ -116,8 +116,7 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
# Start LeaderElection in non-clustered mode so it becomes leader
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
# Wait for election (100ms timer + processing)
Process.sleep(150)
LeaderElection.leader?()
# Start ConnectionManager
{:ok, pid} = ConnectionManager.start_link([])
@ -125,18 +124,16 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
end
test "handles initial state check when leader", %{pid: pid} do
# Send initial state check
send(pid, :check_initial_state)
Process.sleep(50)
:sys.get_state(pid)
# Should handle without crashing
assert Process.alive?(pid)
end
test "handles initial state check when not leader", %{pid: pid} do
# Send initial state check
send(pid, :check_initial_state)
Process.sleep(50)
:sys.get_state(pid)
# Should handle without crashing
assert Process.alive?(pid)
@ -148,18 +145,16 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(150)
LeaderElection.leader?()
{:ok, pid} = ConnectionManager.start_link([])
# With delay=0, initial check fires immediately; just wait for processing
Process.sleep(50)
:sys.get_state(pid)
{:ok, pid: pid}
end
test "starts connection when becoming leader", %{pid: pid} do
# Send leadership change - this node became leader
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Should handle the message without crashing
assert Process.alive?(pid)
@ -168,11 +163,11 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
test "stops connection when losing leadership", %{pid: pid} do
# First become leader and start connection
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Then lose leadership
send(pid, {:leadership_change, node(), false})
Process.sleep(50)
:sys.get_state(pid)
# Should handle the message without crashing
assert Process.alive?(pid)
@ -181,9 +176,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
test "ignores leadership changes for other nodes", %{pid: pid} do
other_node = :other@host
# Send leadership change for another node
send(pid, {:leadership_change, other_node, true})
Process.sleep(50)
:sys.get_state(pid)
# Should not affect this node
assert Process.alive?(pid)
@ -192,10 +186,10 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
test "does not start connection twice", %{pid: pid} do
# Become leader twice
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Should handle duplicate leadership without issues
assert Process.alive?(pid)
@ -207,18 +201,16 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(150)
LeaderElection.leader?()
{:ok, pid} = ConnectionManager.start_link([])
# With delay=0, initial check fires immediately; just wait for processing
Process.sleep(50)
:sys.get_state(pid)
{:ok, pid: pid}
end
test "handles IsSupervisor already started error", %{pid: pid} do
# Try to become leader
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Should handle gracefully even if IsSupervisor fails to start
assert Process.alive?(pid)
@ -233,9 +225,8 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
end
end
# Send leadership change
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Should handle failure gracefully
assert Process.alive?(pid)
@ -278,26 +269,25 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(150)
LeaderElection.leader?()
{:ok, pid} = ConnectionManager.start_link([])
{:ok, pid: pid}
end
test "full lifecycle - become leader, lose leadership", %{pid: pid} do
# With delay=0, initial check fires immediately; just wait for processing
Process.sleep(50)
:sys.get_state(pid)
# Become leader
send(pid, {:leadership_change, node(), true})
Process.sleep(50)
:sys.get_state(pid)
# Verify connection started (check state indirectly)
assert Process.alive?(pid)
# Lose leadership
send(pid, {:leadership_change, node(), false})
Process.sleep(50)
:sys.get_state(pid)
# Verify still alive after stopping connection
assert Process.alive?(pid)
@ -329,7 +319,7 @@ defmodule Aprsme.Cluster.ConnectionManagerTest do
case Process.whereis(LeaderElection) do
nil ->
{:ok, _} = LeaderElection.start_link([])
Process.sleep(200)
LeaderElection.leader?()
_pid ->
:ok

View file

@ -54,10 +54,7 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
# Ensure clustering is disabled
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, pid} = LeaderElection.start_link([])
# Give time for election to occur
Process.sleep(200)
LeaderElection.leader?()
{:ok, pid: pid}
end
@ -75,7 +72,7 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
setup do
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _pid} = LeaderElection.start_link([])
Process.sleep(200)
LeaderElection.leader?()
:ok
end
@ -104,7 +101,7 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
setup do
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _pid} = LeaderElection.start_link([])
Process.sleep(200)
LeaderElection.leader?()
:ok
end
@ -154,9 +151,7 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
test "handles termination gracefully when leader" do
{:ok, pid} = LeaderElection.start_link([])
Process.sleep(200)
# Verify it's the leader
# Sync so the 0-delay election fires before we assert leader status.
assert LeaderElection.leader?() == true
# Stop the process
@ -196,7 +191,7 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
# For now, we just verify the module handles conflicts without crashing
{:ok, pid1} = LeaderElection.start_link([])
Process.sleep(100)
LeaderElection.leader?()
# Try to register another process with the same key
# This should fail or trigger conflict resolution
@ -204,7 +199,8 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
:global.register_name(@election_key, self())
end)
Process.sleep(100)
# Brief pause for the spawned process to attempt registration.
Process.sleep(10)
# Original process should still be alive
assert Process.alive?(pid1)
@ -260,7 +256,6 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
setup do
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, pid} = LeaderElection.start_link([])
Process.sleep(200)
assert LeaderElection.leader?() == true
{:ok, pid: pid}
end
@ -309,14 +304,13 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
# Lose the registration
:global.unregister_name(@election_key)
# Force check — should step down
# Force check — should step down and immediately queue :attempt_election.
send(pid, :check_leadership)
state = :sys.get_state(pid)
assert state.is_leader == false
# check_leadership also schedules :attempt_election for non-leaders.
# Wait for re-election.
Process.sleep(100)
# With election_initial_delay_ms: 0, :attempt_election is already in the
# mailbox. leader?/0 (a GenServer.call) processes it first, then answers.
assert LeaderElection.leader?() == true
end
end
@ -330,13 +324,13 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
test "handles unknown messages without crashing", %{pid: pid} do
send(pid, :unknown_message)
Process.sleep(100)
:sys.get_state(pid)
assert Process.alive?(pid)
end
test "handles check_leadership message", %{pid: pid} do
send(pid, :check_leadership)
Process.sleep(100)
:sys.get_state(pid)
assert Process.alive?(pid)
end
end

View file

@ -21,8 +21,8 @@ defmodule Aprsme.Cluster.PacketDistributorTest do
# Start LeaderElection in non-clustered mode
{:ok, _} = LeaderElection.start_link([])
# Wait for election to complete (100ms timer + processing)
Process.sleep(300)
# Sync with the GenServer so election (0ms in test config) has run.
LeaderElection.leader?()
on_exit(fn ->
try do
@ -47,19 +47,17 @@ defmodule Aprsme.Cluster.PacketDistributorTest do
PacketDistributor.distribute_packet(@test_packet)
refute_receive {:distributed_packet, _}, 100
refute_receive {:distributed_packet, _}, 10
end
test "distributes packet when cluster is enabled and node is leader" do
Application.put_env(:aprsme, :cluster_enabled, true)
# LeaderElection started in non-clustered mode becomes leader quickly
Process.sleep(100)
PacketDistributor.subscribe()
PacketDistributor.distribute_packet(@test_packet)
assert_receive {:distributed_packet, packet}, 200
assert_receive {:distributed_packet, packet}, 50
assert packet.raw == "TEST>APRS:test packet"
assert packet.sender == "TEST"
end
@ -75,7 +73,7 @@ defmodule Aprsme.Cluster.PacketDistributorTest do
{:distributed_packet, @test_packet}
)
assert_receive {:distributed_packet, _}, 500
assert_receive {:distributed_packet, _}, 50
end
end
@ -94,7 +92,7 @@ defmodule Aprsme.Cluster.PacketDistributorTest do
assert {:ok, %{}} = PacketDistributor.init([])
# Callback subscribes the caller to cluster:packets.
Phoenix.PubSub.broadcast(Aprsme.PubSub, "cluster:packets", :ping)
assert_receive :ping, 100
assert_receive :ping, 50
end
test "handle_info forwards distributed packets" do
@ -112,7 +110,7 @@ defmodule Aprsme.Cluster.PacketDistributorTest do
# After terminate, this process should no longer receive cluster:packets
# broadcasts.
Phoenix.PubSub.broadcast(Aprsme.PubSub, "cluster:packets", :should_not_arrive)
refute_receive :should_not_arrive, 100
refute_receive :should_not_arrive, 10
end
end
end

View file

@ -23,9 +23,9 @@ defmodule Aprsme.Cluster.PacketReceiverTest do
GenServer.stop(LeaderElection)
end
# Start LeaderElection and wait for election
# Start LeaderElection and sync so election (0ms in test config) has run.
{:ok, leader_pid} = LeaderElection.start_link([])
Process.sleep(300)
LeaderElection.leader?()
on_exit(fn ->
try do
@ -64,9 +64,7 @@ defmodule Aprsme.Cluster.PacketReceiverTest do
packet = %{raw: "TEST>APRS:test", sender: "TEST"}
send(pid, {:distributed_packet, packet})
# Give it time to process
Process.sleep(50)
:sys.get_state(pid)
# Process should still be alive (no crash)
assert Process.alive?(pid)
@ -84,9 +82,7 @@ defmodule Aprsme.Cluster.PacketReceiverTest do
packet = %{raw: "TEST>APRS:test", sender: "TEST"}
send(pid, {:distributed_packet, packet})
# Give it time to process
Process.sleep(50)
:sys.get_state(pid)
# Process should still be alive (no crash)
assert Process.alive?(pid)
@ -99,8 +95,7 @@ defmodule Aprsme.Cluster.PacketReceiverTest do
send(pid, :some_unknown_message)
send(pid, {:unexpected, "data"})
Process.sleep(100)
:sys.get_state(pid)
assert Process.alive?(pid)
end

View file

@ -77,7 +77,7 @@ defmodule Aprsme.DeviceCacheTest do
test "init/1 schedules an initial load and returns an empty state" do
assert {:ok, state} = DeviceCache.init([])
assert state == %{initial_load_done: false}
assert_receive :initial_load, 1_500
assert_receive :initial_load, 100
end
test "handle_call(:refresh_cache, _, state) returns :ok" do

View file

@ -163,9 +163,8 @@ defmodule Aprsme.DeviceIdentificationTest do
test "attempts to refresh when there are no devices at all" do
Repo.delete_all(Devices)
# fetch_and_upsert_devices goes through the circuit breaker. Without a
# stub it either errors out (network failure) or returns :ok. Either
# outcome exercises the refresh_if_stale(nil, _) branch.
# fetch_and_upsert_devices goes through the circuit breaker. The short
# Req timeout configured in test.exs makes the HTTP attempt fail fast.
result = DeviceIdentification.maybe_refresh_devices()
assert result == :ok or match?({:error, _}, result)
end

View file

@ -1285,7 +1285,7 @@ defmodule Aprsme.IsTest do
receive do
:stop -> :ok
after
5_000 -> :ok
200 -> :ok
end
send(parent, :done)