test: cut runtime on slow tests, fix time-sensitive flake
Performance: - HealthController: honor `:timeout` from `:redis` config (was hardcoded 2s) so the 503-when-unreachable test exits immediately - DevicePollerBranches: drop `collect_events` window 1500ms → 100ms (PubSub broadcasts are synchronous from `run_perform`) - Towerops.Unused: cache xref analysis in `:persistent_term` keyed by BEAM mtimes so repeated `mix unused` invocations skip re-analysis - DiscoveryWorker: make wait_loop interval configurable via `:discovery_wait_interval_ms` (25ms in tests vs 500ms in prod) - ProfileWatcher: configurable `:profile_reloader` so tests can stub the YAML reload instead of paying the full disk-load cost - PingExecutor: tag the loopback test `:network` (it shells out to `ping`); coverage already provided by 4 other `:network`-tagged tests Flake fix: - FrequencyChange tests used hardcoded `~U[2026-05-09 12:00:00Z]`, which now falls outside the rule's 24h lookback window. Switched to `DateTime.utc_now()` so scans stay inside the window regardless of clock progression.
This commit is contained in:
parent
6a8ee862bd
commit
b1c803e1d2
9 changed files with 71 additions and 12 deletions
|
|
@ -125,5 +125,7 @@ config :towerops,
|
|||
agent_poll_interval_ms: 100,
|
||||
# Reduce device deletion delay for faster tests (10ms vs 500ms in prod)
|
||||
device_deletion_delay_ms: 10,
|
||||
# Reduce DiscoveryWorker poll cadence (25ms vs 500ms in prod)
|
||||
discovery_wait_interval_ms: 25,
|
||||
# Stripe test configuration
|
||||
stripe_product_id: "prod_test"
|
||||
|
|
|
|||
|
|
@ -53,10 +53,15 @@ defmodule Towerops.Profiles.ProfileWatcher do
|
|||
if yaml_file?(path) and should_reload?(events) do
|
||||
Logger.info("ProfileWatcher: Detected change in #{Path.relative_to_cwd(path)}, reloading profiles...")
|
||||
|
||||
case YamlProfiles.reload() do
|
||||
reloader = Application.get_env(:towerops, :profile_reloader, YamlProfiles)
|
||||
|
||||
case reloader.reload() do
|
||||
{:ok, profiles} when is_list(profiles) ->
|
||||
Logger.info("ProfileWatcher: Successfully reloaded #{length(profiles)} profiles")
|
||||
|
||||
{:ok, count} when is_integer(count) ->
|
||||
Logger.info("ProfileWatcher: Successfully reloaded #{count} profiles")
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("ProfileWatcher: Failed to reload profiles: #{inspect(reason)}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,11 +32,39 @@ defmodule Towerops.Unused do
|
|||
@spec unused() :: [{module(), atom(), arity(), Path.t(), pos_integer()}]
|
||||
def unused do
|
||||
case beam_paths() do
|
||||
[] -> []
|
||||
beams -> analyse_beams(beams)
|
||||
[] ->
|
||||
[]
|
||||
|
||||
beams ->
|
||||
# `:xref` analysis is expensive (~1s) and pure for a fixed set of
|
||||
# BEAM files. Cache by the (sorted) list of BEAM mtimes so multiple
|
||||
# callers in the same OS process — most often `mix unused` followed
|
||||
# by `mix unused --format json` in tests — reuse one analysis pass.
|
||||
key = cache_key(beams)
|
||||
|
||||
case :persistent_term.get({__MODULE__, :unused_cache}, nil) do
|
||||
{^key, cached} ->
|
||||
cached
|
||||
|
||||
_ ->
|
||||
result = analyse_beams(beams)
|
||||
:persistent_term.put({__MODULE__, :unused_cache}, {key, result})
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_key(beams) do
|
||||
beams
|
||||
|> Enum.sort()
|
||||
|> Enum.map(fn beam ->
|
||||
case File.stat(beam, time: :posix) do
|
||||
{:ok, %File.Stat{mtime: m, size: s}} -> {beam, m, s}
|
||||
_ -> {beam, 0, 0}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec keep?({module(), atom(), arity()}) :: boolean()
|
||||
def keep?({_mod, fun, arity} = mfa) do
|
||||
|
|
|
|||
|
|
@ -518,8 +518,8 @@ defmodule Towerops.Workers.DiscoveryWorker do
|
|||
elapsed
|
||||
) do
|
||||
if device.last_discovery_at == initial_discovery_at do
|
||||
# Wait 500ms before checking again
|
||||
Process.sleep(500)
|
||||
# Wait before checking again (500ms in prod; tests override to ~25ms)
|
||||
Process.sleep(Application.get_env(:towerops, :discovery_wait_interval_ms, 500))
|
||||
wait_loop(device_id, agent_token_id, initial_discovery_at, start_time, timeout_ms)
|
||||
else
|
||||
Logger.info(
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ defmodule ToweropsWeb.HealthController do
|
|||
|
||||
if Keyword.has_key?(redis_config, :host) do
|
||||
# Redis is configured, check connectivity with short timeout for health check
|
||||
case Towerops.RedisHealthCheck.check_health(redis_config, 2_000) do
|
||||
timeout = Keyword.get(redis_config, :timeout, 2_000)
|
||||
|
||||
case Towerops.RedisHealthCheck.check_health(redis_config, timeout) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ defmodule Towerops.Monitoring.Executors.PingExecutorTest do
|
|||
assert is_binary(reason)
|
||||
end
|
||||
|
||||
@tag :network
|
||||
test "exercises run_ping path against loopback (no network required)" do
|
||||
# 127.0.0.1 is always reachable via loopback; this drives the
|
||||
# System.cmd/Task.async/Task.yield branches that the :network-
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
defmodule FakeReloader do
|
||||
@moduledoc false
|
||||
def reload, do: {:ok, 0}
|
||||
end
|
||||
|
||||
defmodule Towerops.Profiles.ProfileWatcherTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
|
|
@ -95,6 +100,18 @@ defmodule Towerops.Profiles.ProfileWatcherTest do
|
|||
end
|
||||
|
||||
test "yaml file event with reload trigger doesn't crash and preserves state" do
|
||||
# Stub out the reloader so we don't pay the full YamlProfiles.reload
|
||||
# cost (loads ~hundreds of YAML files from disk) just to verify the
|
||||
# handle_info branch.
|
||||
previous = Application.get_env(:towerops, :profile_reloader)
|
||||
Application.put_env(:towerops, :profile_reloader, FakeReloader)
|
||||
|
||||
on_exit(fn ->
|
||||
if previous,
|
||||
do: Application.put_env(:towerops, :profile_reloader, previous),
|
||||
else: Application.delete_env(:towerops, :profile_reloader)
|
||||
end)
|
||||
|
||||
msg = {:file_event, self(), {"priv/profiles/test.yaml", [:modified]}}
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ defmodule Towerops.Recommendations.Rules.FrequencyChangeTest do
|
|||
org = organization_fixture(user.id)
|
||||
device = device_fixture(%{organization_id: org.id, name: "AP-North"})
|
||||
|
||||
# Use `now` so scans always fall inside the rule's 24h lookback —
|
||||
# hardcoded dates here would silently age out of the window.
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
{:ok, snmp} =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{
|
||||
|
|
@ -30,11 +34,11 @@ defmodule Towerops.Recommendations.Rules.FrequencyChangeTest do
|
|||
current_channel: 149,
|
||||
current_frequency_mhz: 5745,
|
||||
current_channel_width_mhz: 80,
|
||||
last_radio_seen_at: ~U[2026-05-09 12:00:00Z]
|
||||
last_radio_seen_at: now
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
%{org: org, device: device, snmp: snmp}
|
||||
%{org: org, device: device, snmp: snmp, now: now}
|
||||
end
|
||||
|
||||
defp insert_scan(org, device, attrs) do
|
||||
|
|
@ -49,7 +53,7 @@ defmodule Towerops.Recommendations.Rules.FrequencyChangeTest do
|
|||
channel_width_mhz: 80,
|
||||
rssi_dbm: -70,
|
||||
is_own_fleet: false,
|
||||
seen_at: ~U[2026-05-09 12:00:00Z]
|
||||
seen_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
},
|
||||
attrs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ defmodule Towerops.Workers.DevicePollerWorkerBranchesTest do
|
|||
|
||||
assert :ok = run_perform(device)
|
||||
|
||||
received_events = collect_events([], 1_500)
|
||||
received_events = collect_events([], 100)
|
||||
|
||||
assert Enum.any?(received_events, fn e ->
|
||||
Map.get(e, :event_type) == "interface_speed_change"
|
||||
|
|
@ -471,7 +471,7 @@ defmodule Towerops.Workers.DevicePollerWorkerBranchesTest do
|
|||
|
||||
assert :ok = run_perform(device)
|
||||
|
||||
received_events = collect_events([], 1_500)
|
||||
received_events = collect_events([], 100)
|
||||
|
||||
assert Enum.any?(received_events, fn e ->
|
||||
Map.get(e, :event_type) == "interface_mac_change"
|
||||
|
|
@ -527,7 +527,7 @@ defmodule Towerops.Workers.DevicePollerWorkerBranchesTest do
|
|||
|
||||
assert :ok = run_perform(device)
|
||||
|
||||
events = collect_events([], 1_500)
|
||||
events = collect_events([], 100)
|
||||
|
||||
assert Enum.any?(events, fn e ->
|
||||
Map.get(e, :event_type) == "interface_up"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue