test: fix slow + flaky tests (Mox global, env pollution, pool starvation)

- Remove Mox.set_mox_global() in DPW extra test; rely on $callers
  instead so concurrent vendor SNMP tests stop intermittently failing
  with VerificationError.
- Make ToweropsWeb.Plugs.GraphQLIntrospectionTest async: false. It
  mutates Application.put_env(:towerops, :env, :prod), which polluted
  every async vendor test that reads :env via
  Towerops.Snmp.Client.phoenix_snmp_disabled/0.
- Bump test pool queue_target/queue_interval. AgentChannelTest spawns
  many `:proc_lib` channel processes that hold sandbox connections;
  the default 50ms queue caused intermittent
  'could not checkout the connection' errors.
- Tag real-System.cmd("ping") tests as :network so they're excluded
  by default. Saves ~20s on every full run.
- Make JobCleanupTask's internal 1s settle sleep configurable via
  :job_cleanup_settle_ms; the test overrides it to 0.

Plus the pending CoverageLive.Form edit-save / EIRP recompute and
TraceLive deep-link tests from before the compact.
This commit is contained in:
Graham McIntire 2026-05-08 14:43:30 -05:00
parent c90e34de0b
commit c50dc08ad4
9 changed files with 147 additions and 5 deletions

View file

@ -62,7 +62,14 @@ if database_url = System.get_env("DATABASE_URL") do
config :towerops, Towerops.Repo,
url: database_url,
pool: Sandbox,
pool_size: System.schedulers_online() * 2
pool_size: System.schedulers_online() * 2,
# Bumped from default 50ms — channel-heavy test files (notably
# AgentChannelTest) intermittently exhaust the pool while waiting on
# background `:proc_lib` processes to release their checked-out
# connections. A longer queue lets the test pool ride out the spike
# rather than crashing with `connection not available`.
queue_target: 500,
queue_interval: 5_000
else
config :towerops, Towerops.Repo,
username: "postgres",
@ -70,7 +77,9 @@ else
hostname: "localhost",
database: "towerops_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Sandbox,
pool_size: System.schedulers_online() * 2
pool_size: System.schedulers_online() * 2,
queue_target: 500,
queue_interval: 5_000
end
# Configure Cloak encryption for testing

View file

@ -38,7 +38,7 @@ defmodule Towerops.Workers.JobCleanupTask do
cancel_all_polling_jobs()
# Wait a moment for cancellations to process
Process.sleep(1000)
Process.sleep(Application.get_env(:towerops, :job_cleanup_settle_ms, 1000))
# Reschedule polling for all enabled devices
reschedule_all_devices()

View file

@ -5,6 +5,7 @@ defmodule Towerops.Monitoring.Executors.PingExecutorTest do
alias Towerops.Monitoring.Executors.PingExecutor
describe "execute/2" do
@tag :network
test "returns success with valid ping output" do
config = %{"host" => "127.0.0.1", "count" => 1}
@ -20,6 +21,7 @@ defmodule Towerops.Monitoring.Executors.PingExecutorTest do
end
end
@tag :network
test "defaults count to 3 when not provided" do
config = %{"host" => "127.0.0.1"}

View file

@ -133,6 +133,7 @@ defmodule Towerops.Monitoring.PingTest do
# correctly handles various response formats by testing with valid IPs
# that return quickly (localhost)
@tag :network
test "localhost ping returns valid response format" do
result = Ping.ping("127.0.0.1", 2000)
@ -148,6 +149,7 @@ defmodule Towerops.Monitoring.PingTest do
end
end
@tag :network
test "default-arity ping/1 dispatches to ping/2" do
# ping/1 calls ping(ip, 5000); we mostly want to exercise the arity
# without slowing the suite down for a real timeout.
@ -159,6 +161,7 @@ defmodule Towerops.Monitoring.PingTest do
end
end
@tag :network
test "unreachable IP returns :timeout error" do
# 192.0.2.0/24 is reserved for documentation per RFC 5737 — never routed.
result = Ping.ping("192.0.2.1", 1000)

View file

@ -33,7 +33,16 @@ defmodule Towerops.Workers.DevicePollerWorkerExtraTest do
setup :verify_on_exit!
setup do
Mox.set_mox_global()
# NOTE: Do NOT call `Mox.set_mox_global/0` here. This file is `async: false`,
# but `set_mox_global` makes the SnmpMock visible to *every* process in the
# VM, which has historically stolen expectations from concurrent async
# vendor profile tests (Siae/Moxa/Geist/Himoinsa…) and caused flaky
# `Mox.VerificationError` failures.
#
# Mox 1.0+ already follows the `$callers` chain set by `Task.async/1` and
# `Task.async_stream/3`, which is what `DevicePollerWorker.poll_device/1`
# uses internally. So expectations defined on the test process flow into
# those tasks without any global flag.
old_adapter = Application.get_env(:towerops, :snmp_adapter)
Application.put_env(:towerops, :snmp_adapter, SnmpMock)

View file

@ -9,6 +9,11 @@ defmodule Towerops.Workers.JobCleanupTaskTest do
setup do
previous_env = Application.get_env(:towerops, :env)
previous_settle = Application.get_env(:towerops, :job_cleanup_settle_ms)
# Make the internal "wait for cancellations to process" sleep a no-op
# in tests so the prod-path test runs in <1ms instead of >1s.
Application.put_env(:towerops, :job_cleanup_settle_ms, 0)
on_exit(fn ->
if previous_env do
@ -16,6 +21,12 @@ defmodule Towerops.Workers.JobCleanupTaskTest do
else
Application.delete_env(:towerops, :env)
end
if previous_settle do
Application.put_env(:towerops, :job_cleanup_settle_ms, previous_settle)
else
Application.delete_env(:towerops, :job_cleanup_settle_ms)
end
end)
:ok

View file

@ -80,4 +80,73 @@ defmodule ToweropsWeb.CoverageLive.FormTest do
assert render_click(view, "use_site_location", %{})
end
end
describe "edit save" do
test "edit save with valid attrs redirects to show", %{conn: conn, org: org, site: site} do
cov = Towerops.CoveragesFixtures.coverage_fixture(org.id, site.id)
{:ok, view, _html} = live(conn, ~p"/coverage/#{cov.id}/edit")
result =
view
|> form("form", coverage: %{"name" => "Renamed Coverage"})
|> render_submit()
assert is_binary(result) or match?({:error, {:redirect, _}}, result) or
match?({:error, {:live_redirect, _}}, result)
end
test "edit save with invalid name re-renders the form", %{
conn: conn,
org: org,
site: site
} do
cov = Towerops.CoveragesFixtures.coverage_fixture(org.id, site.id)
{:ok, view, _html} = live(conn, ~p"/coverage/#{cov.id}/edit")
html =
view
|> form("form", coverage: %{"name" => ""})
|> render_submit()
assert is_binary(html)
end
end
describe "validate event triggers EIRP recompute" do
test "validate event with antenna_slug computes a non-zero EIRP", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/coverage/new")
html =
view
|> form("form",
coverage: %{
"name" => "Test",
"antenna_slug" => "simulate-isotropic-omni-0",
"tx_power_dbm" => "20.0"
}
)
|> render_change()
assert is_binary(html)
end
test "validate event with bogus number string falls back gracefully", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/coverage/new")
html =
view
|> form("form",
coverage: %{
"name" => "Test",
"antenna_slug" => "simulate-isotropic-omni-0",
"tx_power_dbm" => "not-a-number"
}
)
|> render_change()
assert is_binary(html)
end
end
end

View file

@ -108,4 +108,38 @@ defmodule ToweropsWeb.TraceLive.IndexTest do
assert Index.format_ms("x") == "x"
end
end
describe "deep-link with trace data renders issues_callout" do
test "trace_type=device with a real device renders the trace shell",
%{conn: conn, organization: organization} do
{:ok, site} = Towerops.Sites.create_site(%{name: "Trace Test Site", organization_id: organization.id})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Trace Dev",
ip_address: "10.10.10.1",
site_id: site.id,
organization_id: organization.id
})
{:ok, _view, html} = live(conn, ~p"/trace?type=device&id=#{device.id}")
# Either renders the trace card or the empty state. Both exercise
# handle_params with type+id which calls Trace.assemble_trace/3.
assert html =~ "Trace" or html =~ device.name
end
test "trace_type=site with a real site renders the trace shell",
%{conn: conn, organization: organization} do
{:ok, site} = Towerops.Sites.create_site(%{name: "Trace Site", organization_id: organization.id})
{:ok, _view, html} = live(conn, ~p"/trace?type=site&id=#{site.id}")
assert html =~ "Trace" or html =~ site.name
end
test "trace_type=account with an unknown id still renders without crash",
%{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/trace?type=account&id=#{Ecto.UUID.generate()}")
assert html =~ "Trace"
end
end
end

View file

@ -1,5 +1,10 @@
defmodule ToweropsWeb.Plugs.GraphQLIntrospectionTest do
use ToweropsWeb.ConnCase, async: true
# NOTE: async: false because the "production" describe block mutates the
# global `Application.put_env(:towerops, :env, :prod)`. Other async tests
# (notably the vendor SNMP profile tests) check this env to decide whether
# to short-circuit SNMP calls, so running this concurrently caused
# intermittent vendor test failures. See `Towerops.Snmp.Client.phoenix_snmp_disabled/0`.
use ToweropsWeb.ConnCase, async: false
alias ToweropsWeb.Plugs.GraphQLIntrospection