Remove :slow test tag; make slow tests run fast via configurable timeouts

- Make LeaderElection check_interval, max_cluster_wait, and cluster_check_interval
  configurable via Application env so tests can override with short values
- Slow LeaderElection tests now use 50-150ms timeouts instead of 3-6s
- Remove :slow tag from unused_test (compile cache makes reruns cheap)
- Drop :slow from test_helper exclude list

All 2490 tests now run in ~45s (up from 2485 with 5 excluded).
This commit is contained in:
Graham McIntire 2026-05-09 09:05:16 -05:00
parent 001af22286
commit 1ddc817f50
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 34 additions and 24 deletions

View file

@ -8,9 +8,16 @@ defmodule Aprsme.Cluster.LeaderElection do
require Logger require Logger
@election_key {:aprs_is_leader, __MODULE__} @election_key {:aprs_is_leader, __MODULE__}
@check_interval 5_000 @default_check_interval 5_000
# Maximum time to wait for cluster formation before proceeding with election (30 seconds) # Maximum time to wait for cluster formation before proceeding with election (30 seconds)
@max_cluster_wait 30_000 @default_max_cluster_wait 30_000
@default_cluster_check_interval 2_000
defp check_interval, do: Application.get_env(:aprsme, :election_check_interval_ms, @default_check_interval)
defp max_cluster_wait, do: Application.get_env(:aprsme, :election_max_cluster_wait_ms, @default_max_cluster_wait)
defp cluster_check_interval,
do: Application.get_env(:aprsme, :election_cluster_check_interval_ms, @default_cluster_check_interval)
defstruct is_leader: false, defstruct is_leader: false,
leader_node: nil, leader_node: nil,
@ -63,7 +70,7 @@ defmodule Aprsme.Cluster.LeaderElection do
schedule_initial_election(cluster_enabled) schedule_initial_election(cluster_enabled)
# Schedule periodic checks # Schedule periodic checks
Process.send_after(self(), :check_leadership, @check_interval) Process.send_after(self(), :check_leadership, check_interval())
# Initialize cached leadership state # Initialize cached leadership state
:persistent_term.put({__MODULE__, :is_leader}, false) :persistent_term.put({__MODULE__, :is_leader}, false)
@ -74,8 +81,8 @@ defmodule Aprsme.Cluster.LeaderElection do
# Clustered mode: wait for formation, then elect; schedule a backstop timeout. # Clustered mode: wait for formation, then elect; schedule a backstop timeout.
defp schedule_initial_election(true) do defp schedule_initial_election(true) do
Logger.info("Clustering enabled - waiting for cluster formation before leader election") Logger.info("Clustering enabled - waiting for cluster formation before leader election")
Process.send_after(self(), :check_cluster_and_elect, 2_000) Process.send_after(self(), :check_cluster_and_elect, cluster_check_interval())
Process.send_after(self(), :force_election_timeout, @max_cluster_wait) Process.send_after(self(), :force_election_timeout, max_cluster_wait())
end end
# Non-clustered mode: elect immediately. # Non-clustered mode: elect immediately.
@ -118,12 +125,12 @@ defmodule Aprsme.Cluster.LeaderElection do
if connected_nodes == [] do if connected_nodes == [] do
Logger.warning( Logger.warning(
"Cluster formation timeout reached after #{@max_cluster_wait}ms with no connected nodes. " <> "Cluster formation timeout reached after #{max_cluster_wait()}ms with no connected nodes. " <>
"Proceeding with leader election in single-node mode to ensure APRS-IS connection." "Proceeding with leader election in single-node mode to ensure APRS-IS connection."
) )
else else
Logger.info( Logger.info(
"Forcing leader election after #{@max_cluster_wait}ms wait with #{length(connected_nodes)} connected nodes" "Forcing leader election after #{max_cluster_wait()}ms wait with #{length(connected_nodes)} connected nodes"
) )
end end
@ -165,7 +172,7 @@ defmodule Aprsme.Cluster.LeaderElection do
end end
# Schedule next check # Schedule next check
_ = Process.send_after(self(), :check_leadership, @check_interval) _ = Process.send_after(self(), :check_leadership, check_interval())
{:noreply, state} {:noreply, state}
end end

View file

@ -161,15 +161,17 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
assert :global.whereis_name(@election_key) == :undefined assert :global.whereis_name(@election_key) == :undefined
end end
@tag :slow
test "periodic leadership check continues running" do test "periodic leadership check continues running" do
Application.put_env(:aprsme, :election_check_interval_ms, 50)
on_exit(fn -> Application.delete_env(:aprsme, :election_check_interval_ms) end)
{:ok, _pid} = LeaderElection.start_link([]) {:ok, _pid} = LeaderElection.start_link([])
# Initial state - might not be leader yet # Initial state - might not be leader yet
_initial_leader = LeaderElection.leader?() _initial_leader = LeaderElection.leader?()
# Wait for periodic check # Wait for periodic check (50ms interval, plus buffer)
Process.sleep(6000) Process.sleep(150)
# Should still be able to query leadership # Should still be able to query leadership
current_leader = LeaderElection.leader?() current_leader = LeaderElection.leader?()
@ -208,12 +210,11 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
end end
describe "stale registration cleanup" do describe "stale registration cleanup" do
@tag :slow
test "cleans up registration when process dies" do test "cleans up registration when process dies" do
# Register a dead process # Register a dead process
dead_pid = spawn(fn -> :ok end) dead_pid = spawn(fn -> :ok end)
# Ensure it's dead # Ensure it's dead
Process.sleep(50) Process.sleep(10)
# Force register it globally (simulating stale registration) # Force register it globally (simulating stale registration)
:global.re_register_name(@election_key, dead_pid) :global.re_register_name(@election_key, dead_pid)
@ -222,8 +223,8 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
Application.put_env(:aprsme, :cluster_enabled, false) Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _pid} = LeaderElection.start_link([]) {:ok, _pid} = LeaderElection.start_link([])
# Wait longer for cleanup and election to occur # Wait for cleanup and election to occur
Process.sleep(500) Process.sleep(150)
# Should have taken over leadership # Should have taken over leadership
assert LeaderElection.leader?() == true assert LeaderElection.leader?() == true
@ -236,16 +237,20 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
:ok :ok
end end
@tag :slow
test "waits for cluster formation when enabled" do test "waits for cluster formation when enabled" do
Application.put_env(:aprsme, :election_max_cluster_wait_ms, 100)
Application.put_env(:aprsme, :election_cluster_check_interval_ms, 50)
on_exit(fn ->
Application.delete_env(:aprsme, :election_max_cluster_wait_ms)
Application.delete_env(:aprsme, :election_cluster_check_interval_ms)
end)
{:ok, _pid} = LeaderElection.start_link([]) {:ok, _pid} = LeaderElection.start_link([])
# Immediately after start, might not be leader yet
Process.sleep(100)
# In cluster mode with no other nodes, it will eventually become leader # In cluster mode with no other nodes, it will eventually become leader
# but might take longer than non-clustered mode # after the (now shortened) max_cluster_wait timeout fires
Process.sleep(3000) Process.sleep(250)
# Should eventually attempt election # Should eventually attempt election
assert is_boolean(LeaderElection.leader?()) assert is_boolean(LeaderElection.leader?())

View file

@ -22,7 +22,6 @@ defmodule Mix.Tasks.Compile.UnusedTest do
assert Unused.clean() == :ok assert Unused.clean() == :ok
end end
@tag :slow
test "runs against the project and returns either :ok or :error tuple" do test "runs against the project and returns either :ok or :error tuple" do
ExUnit.CaptureIO.capture_io(fn -> ExUnit.CaptureIO.capture_io(fn ->
result = Unused.run([]) result = Unused.run([])
@ -37,7 +36,6 @@ defmodule Mix.Tasks.Compile.UnusedTest do
end) end)
end end
@tag :slow
test "accepts --severity warning, info, and unknown values" do test "accepts --severity warning, info, and unknown values" do
for severity <- ["warning", "info", "garbage"] do for severity <- ["warning", "info", "garbage"] do
ExUnit.CaptureIO.capture_io(fn -> ExUnit.CaptureIO.capture_io(fn ->

View file

@ -10,7 +10,7 @@ ExUnit.start(
max_cases: max_cases, max_cases: max_cases,
timeout: 30_000, timeout: 30_000,
capture_log: true, capture_log: true,
exclude: [:slow, :integration] exclude: [:integration]
) )
Ecto.Adapters.SQL.Sandbox.mode(Aprsme.Repo, :manual) Ecto.Adapters.SQL.Sandbox.mode(Aprsme.Repo, :manual)