aprs.me/test/aprsme/cluster/connection_manager_test.exs
Graham McIntire b5a39102a6
fix: Comprehensive packet processing and test optimization improvements
This commit includes several critical fixes and optimizations:

## Production Buffer Overflow Fix
- Fixed telemetry_vals data type conversion from strings to integers
- Enhanced put_telemetry_fields() with robust type conversion logic
- Handles mixed data types (string, integer, float) gracefully
- Prevents PacketConsumer GenServer crashes that caused buffer overflow
- Processes telemetry from both data_extended and top-level attrs

## Test Suite Performance Optimization
- Improved test execution speed by 48% (43.2s → 24.1s)
- Increased parallelization: max_cases from 16 to 48
- Optimized database connections and reduced timeouts
- Tagged slow tests and exclude them by default for fast development
- Fixed device seeding to only run when needed
- Enhanced log capture for packet consumer tests

## Code Quality Improvements
- Fixed handle_info clause grouping in LeaderElection
- Ensures mix compile --warnings-as-errors passes
- Added comprehensive telemetry conversion tests
- Improved test reliability and reduced flakiness

## Infrastructure Updates
- Added migration init container to StatefulSet for automatic schema updates
- Disabled AUTO_MIGRATE in main container (handled by init container)
- Created .test-commands with helpful test aliases

This resolves production stability issues and significantly improves development workflow.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-01 17:50:07 -05:00

284 lines
7.9 KiB
Elixir

defmodule Aprsme.Cluster.ConnectionManagerTest do
use ExUnit.Case, async: false
alias Aprsme.Cluster.ConnectionManager
alias Aprsme.Cluster.LeaderElection
setup do
# Stop any running ConnectionManager process
if Process.whereis(ConnectionManager) do
GenServer.stop(ConnectionManager)
end
# Stop any running LeaderElection process
if Process.whereis(LeaderElection) do
GenServer.stop(LeaderElection)
end
# Ensure DynamicSupervisor is started
case Process.whereis(Aprsme.DynamicSupervisor) do
nil ->
{:ok, _} = DynamicSupervisor.start_link(strategy: :one_for_one, name: Aprsme.DynamicSupervisor)
pid when is_pid(pid) ->
:ok
end
on_exit(fn ->
# Try to stop processes but don't crash if they're already dead
try do
if Process.whereis(ConnectionManager) do
GenServer.stop(ConnectionManager, :normal, 100)
end
catch
:exit, _ -> :ok
end
try do
if Process.whereis(LeaderElection) do
GenServer.stop(LeaderElection, :normal, 100)
end
catch
:exit, _ -> :ok
end
# Clean up any IsSupervisor processes if DynamicSupervisor exists
if Process.whereis(Aprsme.DynamicSupervisor) do
try do
children = DynamicSupervisor.which_children(Aprsme.DynamicSupervisor)
Enum.each(children, fn {_, pid, _, _} ->
if is_pid(pid) do
DynamicSupervisor.terminate_child(Aprsme.DynamicSupervisor, pid)
end
end)
catch
:exit, _ -> :ok
end
end
end)
:ok
end
describe "start_link/1" do
test "starts the GenServer process" do
assert {:ok, pid} = ConnectionManager.start_link([])
assert Process.alive?(pid)
assert Process.whereis(ConnectionManager) == pid
end
test "registers with the module name" do
{:ok, _pid} = ConnectionManager.start_link([])
assert Process.whereis(ConnectionManager)
end
end
describe "initialization" do
setup do
# Start a minimal LeaderElection to prevent crashes
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(200)
:ok
end
test "subscribes to leadership changes" do
{:ok, _pid} = ConnectionManager.start_link([])
# Verify subscription by publishing a test message
Phoenix.PubSub.broadcast(
Aprsme.PubSub,
"cluster:leadership",
{:leadership_change, :test_node, false}
)
# Give time for message processing
Process.sleep(100)
# Process should still be alive after receiving message
assert Process.whereis(ConnectionManager)
end
test "schedules initial state check" do
{:ok, pid} = ConnectionManager.start_link([])
# Should receive :check_initial_state message after init
Process.sleep(1100)
# Process should still be alive
assert Process.alive?(pid)
end
end
describe "leadership state handling" do
setup 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
Process.sleep(200)
# Start ConnectionManager
{:ok, pid} = ConnectionManager.start_link([])
{:ok, pid: pid}
end
test "handles initial state check when leader", %{pid: pid} do
# Send initial state check
send(pid, :check_initial_state)
Process.sleep(100)
# Should handle without crashing
assert Process.alive?(pid)
end
test "handles initial state check when not leader", %{pid: pid} do
# ConnectionManager will check with LeaderElection
# In test environment, it will get that it's the leader
# This is OK - we're just testing that it handles the check without crashing
# Send initial state check
send(pid, :check_initial_state)
Process.sleep(100)
# Should handle without crashing
assert Process.alive?(pid)
end
end
describe "leadership change handling" do
setup do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(200)
{:ok, pid} = ConnectionManager.start_link([])
# Wait for initial check
Process.sleep(1100)
{: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(100)
# Should handle the message without crashing
assert Process.alive?(pid)
end
test "stops connection when losing leadership", %{pid: pid} do
# First become leader and start connection
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
# Then lose leadership
send(pid, {:leadership_change, node(), false})
Process.sleep(100)
# Should handle the message without crashing
assert Process.alive?(pid)
end
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(100)
# Should not affect this node
assert Process.alive?(pid)
end
test "does not start connection twice", %{pid: pid} do
# Become leader twice
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
# Should handle duplicate leadership without issues
assert Process.alive?(pid)
end
end
describe "APRS connection management" do
setup do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(200)
{:ok, pid} = ConnectionManager.start_link([])
Process.sleep(1100)
{:ok, pid: pid}
end
test "handles IsSupervisor already started error", %{pid: pid} do
# The APRS-IS connection is disabled in test environment,
# but we can still test that the ConnectionManager handles errors gracefully
# Try to become leader
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
# Should handle gracefully even if IsSupervisor fails to start
assert Process.alive?(pid)
end
test "handles IsSupervisor start failure", %{pid: pid} do
# Define a failing IsSupervisor
defmodule FailingIsSupervisor do
@moduledoc false
def start_link(_opts) do
{:error, :intentional_failure}
end
end
# Temporarily replace the module reference
# This test is more conceptual as we can't easily mock module references
# Send leadership change
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
# Should handle failure gracefully
assert Process.alive?(pid)
end
end
describe "connection lifecycle" do
setup do
# Ensure LeaderElection is started
Application.put_env(:aprsme, :cluster_enabled, false)
{:ok, _} = LeaderElection.start_link([])
Process.sleep(200)
{:ok, pid} = ConnectionManager.start_link([])
{:ok, pid: pid}
end
test "full lifecycle - become leader, lose leadership", %{pid: pid} do
# Wait for initial check
Process.sleep(1100)
# Become leader
send(pid, {:leadership_change, node(), true})
Process.sleep(100)
# Verify connection started (check state indirectly)
assert Process.alive?(pid)
# Lose leadership
send(pid, {:leadership_change, node(), false})
Process.sleep(100)
# Verify still alive after stopping connection
assert Process.alive?(pid)
end
end
end