Speed up the slowest tests
- packet_replay: stop_replay test reduces fake-task receive timeout from 5s to 200ms - broadcast_task_supervisor: scheduler_usage takes configurable sample seconds; test uses sample-pair API (instant) instead of blocking 1-second sample - health_check: ask_if_alive timeout is now configurable; unresponsive-pid test uses 50ms timeout instead of waiting full 5-second default - mix_unused/analyzer_test: cache analyze() result in setup_all so 4 tests share one analyze pass instead of running 4 separate scans - mix/tasks/compile/unused_test: consolidate three slow severity tests into one - log_sanitizer + packet_field_whitelist: cap property-test runs at 25 (was 100) - historical_loading: trim 200ms post-event sleeps to 50ms - movement: refute_push_event timeout from 200ms to 50ms Total suite time: ~45s -> 40.6s; 2488 tests, 0 failures.
This commit is contained in:
parent
1ddc817f50
commit
4c5c730ee7
11 changed files with 77 additions and 52 deletions
|
|
@ -97,17 +97,34 @@ defmodule Aprsme.BroadcastTaskSupervisor do
|
||||||
if Application.get_env(:aprsme, :env) == :test do
|
if Application.get_env(:aprsme, :env) == :test do
|
||||||
0.0
|
0.0
|
||||||
else
|
else
|
||||||
1
|
sample_seconds = Application.get_env(:aprsme, :scheduler_sample_seconds, 1)
|
||||||
|> :scheduler.utilization()
|
compute_scheduler_usage(sample_seconds)
|
||||||
|> Enum.map(fn {_, usage, _} -> usage end)
|
|
||||||
|> Enum.sum()
|
|
||||||
|> Kernel./(System.schedulers_online())
|
|
||||||
|> Float.round(2)
|
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
_ -> 0.0
|
_ -> 0.0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# When sample_seconds is 0, use sample-pair API to avoid blocking.
|
||||||
|
defp compute_scheduler_usage(0) do
|
||||||
|
sample = :scheduler.sample_all()
|
||||||
|
|
||||||
|
sample
|
||||||
|
|> :scheduler.utilization()
|
||||||
|
|> Enum.map(fn {_, usage, _} -> usage end)
|
||||||
|
|> Enum.sum()
|
||||||
|
|> Kernel./(System.schedulers_online())
|
||||||
|
|> Float.round(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp compute_scheduler_usage(seconds) when is_integer(seconds) and seconds > 0 do
|
||||||
|
seconds
|
||||||
|
|> :scheduler.utilization()
|
||||||
|
|> Enum.map(fn {_, usage, _} -> usage end)
|
||||||
|
|> Enum.sum()
|
||||||
|
|> Kernel./(System.schedulers_online())
|
||||||
|
|> Float.round(2)
|
||||||
|
end
|
||||||
|
|
||||||
# Private helper to broadcast to multiple topics
|
# Private helper to broadcast to multiple topics
|
||||||
defp broadcast_to_topics(topics, message, pubsub) do
|
defp broadcast_to_topics(topics, message, pubsub) do
|
||||||
Enum.each(topics, fn topic ->
|
Enum.each(topics, fn topic ->
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ defmodule AprsmeWeb.Plugs.HealthCheck do
|
||||||
defp ask_if_alive(false, _pid), do: false
|
defp ask_if_alive(false, _pid), do: false
|
||||||
|
|
||||||
defp ask_if_alive(true, pid) do
|
defp ask_if_alive(true, pid) do
|
||||||
GenServer.call(pid, :shutting_down?, 5000)
|
timeout = Application.get_env(:aprsme, :health_check_call_timeout_ms, 5000)
|
||||||
|
GenServer.call(pid, :shutting_down?, timeout)
|
||||||
catch
|
catch
|
||||||
_kind, _reason -> false
|
_kind, _reason -> false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -148,17 +148,23 @@ defmodule Aprsme.BroadcastTaskSupervisorTest do
|
||||||
|
|
||||||
test "scheduler_usage with non-test env exercises the live scheduler sampling" do
|
test "scheduler_usage with non-test env exercises the live scheduler sampling" do
|
||||||
# Mutating :aprsme :env into :prod for one call lets get_stats hit the
|
# Mutating :aprsme :env into :prod for one call lets get_stats hit the
|
||||||
# live :scheduler.utilization/1 branch. We restore env immediately
|
# live :scheduler.utilization branch. Sample seconds=0 uses the
|
||||||
# afterward so we don't disrupt downstream tests.
|
# instant sample-pair API rather than blocking for a full second.
|
||||||
original = Application.get_env(:aprsme, :env)
|
original_env = Application.get_env(:aprsme, :env)
|
||||||
|
original_seconds = Application.get_env(:aprsme, :scheduler_sample_seconds)
|
||||||
Application.put_env(:aprsme, :env, :prod)
|
Application.put_env(:aprsme, :env, :prod)
|
||||||
|
Application.put_env(:aprsme, :scheduler_sample_seconds, 0)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
stats = BroadcastTaskSupervisor.get_stats()
|
stats = BroadcastTaskSupervisor.get_stats()
|
||||||
assert is_float(stats.scheduler_usage)
|
assert is_float(stats.scheduler_usage)
|
||||||
assert stats.scheduler_usage >= 0.0
|
assert stats.scheduler_usage >= 0.0
|
||||||
after
|
after
|
||||||
Application.put_env(:aprsme, :env, original)
|
Application.put_env(:aprsme, :env, original_env)
|
||||||
|
|
||||||
|
if original_seconds,
|
||||||
|
do: Application.put_env(:aprsme, :scheduler_sample_seconds, original_seconds),
|
||||||
|
else: Application.delete_env(:aprsme, :scheduler_sample_seconds)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ defmodule Aprsme.LogSanitizerTest do
|
||||||
|
|
||||||
describe "property: sanitize/1 is idempotent" do
|
describe "property: sanitize/1 is idempotent" do
|
||||||
property "running sanitize twice equals running it once on maps" do
|
property "running sanitize twice equals running it once on maps" do
|
||||||
check all(map <- map_of(atom(:alphanumeric), term())) do
|
check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
|
||||||
once = LogSanitizer.sanitize(map)
|
once = LogSanitizer.sanitize(map)
|
||||||
twice = LogSanitizer.sanitize(once)
|
twice = LogSanitizer.sanitize(once)
|
||||||
assert once == twice
|
assert once == twice
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ defmodule Aprsme.PacketFieldWhitelistTest do
|
||||||
|
|
||||||
describe "property: filter_fields/1 is idempotent" do
|
describe "property: filter_fields/1 is idempotent" do
|
||||||
property "running filter twice equals running once" do
|
property "running filter twice equals running once" do
|
||||||
check all(map <- map_of(atom(:alphanumeric), term())) do
|
check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
|
||||||
once = PacketFieldWhitelist.filter_fields(map)
|
once = PacketFieldWhitelist.filter_fields(map)
|
||||||
twice = PacketFieldWhitelist.filter_fields(once)
|
twice = PacketFieldWhitelist.filter_fields(once)
|
||||||
assert once == twice
|
assert once == twice
|
||||||
|
|
@ -112,14 +112,14 @@ defmodule Aprsme.PacketFieldWhitelistTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
property "every key in the output satisfies allowed?/1" do
|
property "every key in the output satisfies allowed?/1" do
|
||||||
check all(map <- map_of(atom(:alphanumeric), term())) do
|
check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
|
||||||
result = PacketFieldWhitelist.filter_fields(map)
|
result = PacketFieldWhitelist.filter_fields(map)
|
||||||
assert Enum.all?(Map.keys(result), &PacketFieldWhitelist.allowed?/1)
|
assert Enum.all?(Map.keys(result), &PacketFieldWhitelist.allowed?/1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
property "invalid_fields and filter_fields partition the input" do
|
property "invalid_fields and filter_fields partition the input" do
|
||||||
check all(map <- map_of(atom(:alphanumeric), term())) do
|
check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
|
||||||
kept = map |> PacketFieldWhitelist.filter_fields() |> Map.keys() |> Enum.map(&to_string/1)
|
kept = map |> PacketFieldWhitelist.filter_fields() |> Map.keys() |> Enum.map(&to_string/1)
|
||||||
dropped = PacketFieldWhitelist.invalid_fields(map)
|
dropped = PacketFieldWhitelist.invalid_fields(map)
|
||||||
all_keys = map |> Map.keys() |> Enum.map(&to_string/1)
|
all_keys = map |> Map.keys() |> Enum.map(&to_string/1)
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ defmodule Aprsme.PacketReplayTest do
|
||||||
receive do
|
receive do
|
||||||
:stop -> :ok
|
:stop -> :ok
|
||||||
after
|
after
|
||||||
5_000 -> :ok
|
200 -> :ok
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
||||||
|
|
||||||
# Wait for historical loading
|
# Wait for historical loading
|
||||||
Process.sleep(200)
|
Process.sleep(50)
|
||||||
|
|
||||||
# At zoom level 5, the limit should be 100 packets
|
# At zoom level 5, the limit should be 100 packets
|
||||||
# This is defined in HistoricalLoader's @zoom_packet_limits
|
# This is defined in HistoricalLoader's @zoom_packet_limits
|
||||||
|
|
@ -272,7 +272,7 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
||||||
|
|
||||||
# Wait for historical loading
|
# Wait for historical loading
|
||||||
Process.sleep(200)
|
Process.sleep(50)
|
||||||
|
|
||||||
# Verify all packets would be included in the query
|
# Verify all packets would be included in the query
|
||||||
recent_packets =
|
recent_packets =
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,7 @@ defmodule AprsmeWeb.MapLive.MovementTest do
|
||||||
send(view.pid, {:postgres_packet, drift_packet})
|
send(view.pid, {:postgres_packet, drift_packet})
|
||||||
|
|
||||||
# The view should not push a new_packet event for GPS drift
|
# The view should not push a new_packet event for GPS drift
|
||||||
# Using a longer timeout to avoid flaky tests on slower systems
|
refute_push_event(view, "new_packet", %{id: "TEST-1"}, 50)
|
||||||
refute_push_event(view, "new_packet", %{id: "TEST-1"}, 200)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates marker for significant movement", %{conn: conn} do
|
test "updates marker for significant movement", %{conn: conn} do
|
||||||
|
|
|
||||||
|
|
@ -151,16 +151,17 @@ defmodule AprsmeWeb.Plugs.HealthCheckTest do
|
||||||
|
|
||||||
test "returns 200 OK even when registered ShutdownHandler-named pid is unresponsive", %{conn: conn} do
|
test "returns 200 OK even when registered ShutdownHandler-named pid is unresponsive", %{conn: conn} do
|
||||||
Application.put_env(:aprsme, :health_status, :healthy)
|
Application.put_env(:aprsme, :health_status, :healthy)
|
||||||
|
Application.put_env(:aprsme, :health_check_call_timeout_ms, 50)
|
||||||
|
|
||||||
original_pid = Process.whereis(Aprsme.ShutdownHandler)
|
original_pid = Process.whereis(Aprsme.ShutdownHandler)
|
||||||
|
|
||||||
# Register a dummy task pid under the name. GenServer.call to it will
|
# Register a dummy task pid under the name. GenServer.call to it will
|
||||||
# exit; the catch clause in ask_if_alive returns false → readiness OK.
|
# time out (50ms); the catch clause in ask_if_alive returns false → readiness OK.
|
||||||
if original_pid do
|
if original_pid do
|
||||||
:erlang.unregister(Aprsme.ShutdownHandler)
|
:erlang.unregister(Aprsme.ShutdownHandler)
|
||||||
end
|
end
|
||||||
|
|
||||||
task = Task.async(fn -> Process.sleep(2_000) end)
|
task = Task.async(fn -> Process.sleep(500) end)
|
||||||
Process.register(task.pid, Aprsme.ShutdownHandler)
|
Process.register(task.pid, Aprsme.ShutdownHandler)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
|
|
@ -169,6 +170,8 @@ defmodule AprsmeWeb.Plugs.HealthCheckTest do
|
||||||
# The dummy pid is alive but won't respond to :shutting_down? → catch → false → OK
|
# The dummy pid is alive but won't respond to :shutting_down? → catch → false → OK
|
||||||
assert result.status == 200
|
assert result.status == 200
|
||||||
after
|
after
|
||||||
|
Application.delete_env(:aprsme, :health_check_call_timeout_ms)
|
||||||
|
|
||||||
if Process.whereis(Aprsme.ShutdownHandler) == task.pid do
|
if Process.whereis(Aprsme.ShutdownHandler) == task.pid do
|
||||||
:erlang.unregister(Aprsme.ShutdownHandler)
|
:erlang.unregister(Aprsme.ShutdownHandler)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,24 @@ defmodule Mix.Tasks.Compile.UnusedTest do
|
||||||
assert Unused.clean() == :ok
|
assert Unused.clean() == :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "runs against the project and returns either :ok or :error tuple" do
|
test "exercises run/1 across all severity arg variants" do
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
# Each Unused.run/1 call invokes the analyzer (which scans all BEAMs
|
||||||
result = Unused.run([])
|
# and is the slow part). We exercise every severity branch in a
|
||||||
assert match?({:ok, _}, result) or match?({:error, _}, result)
|
# single test so we only pay analyze cost a small number of times.
|
||||||
end)
|
arg_lists = [
|
||||||
end
|
[],
|
||||||
|
["--severity", "error"],
|
||||||
|
["--severity", "warning"],
|
||||||
|
["--severity", "info"],
|
||||||
|
["--severity", "garbage"]
|
||||||
|
]
|
||||||
|
|
||||||
test "honors --severity option override" do
|
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
ExUnit.CaptureIO.capture_io(fn ->
|
||||||
result = Unused.run(["--severity", "error"])
|
for args <- arg_lists do
|
||||||
assert match?({:ok, _}, result) or match?({:error, _}, result)
|
result = Unused.run(args)
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "accepts --severity warning, info, and unknown values" do
|
|
||||||
for severity <- ["warning", "info", "garbage"] do
|
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
|
||||||
result = Unused.run(["--severity", severity])
|
|
||||||
assert match?({:ok, _}, result) or match?({:error, _}, result)
|
assert match?({:ok, _}, result) or match?({:error, _}, result)
|
||||||
end)
|
end
|
||||||
end
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
|
|
||||||
alias MixUnused.Analyzer
|
alias MixUnused.Analyzer
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
# Cache one analyze() result and reuse across tests. analyze() walks
|
||||||
|
# every BEAM file and is the slow part — running it once per module
|
||||||
|
# avoids paying that cost per test.
|
||||||
|
{:ok, baseline: Analyzer.analyze()}
|
||||||
|
end
|
||||||
|
|
||||||
describe "format/1" do
|
describe "format/1" do
|
||||||
test "renders a friendly message when there are no entries" do
|
test "renders a friendly message when there are no entries" do
|
||||||
assert Analyzer.format([]) =~ "No unused public functions"
|
assert Analyzer.format([]) =~ "No unused public functions"
|
||||||
|
|
@ -47,9 +54,7 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "analyze/1 against the running project" do
|
describe "analyze/1 against the running project" do
|
||||||
test "returns a list of unused-entry maps" do
|
test "returns a list of unused-entry maps", %{baseline: result} do
|
||||||
result = Analyzer.analyze()
|
|
||||||
|
|
||||||
assert is_list(result)
|
assert is_list(result)
|
||||||
|
|
||||||
# Every entry has the expected shape.
|
# Every entry has the expected shape.
|
||||||
|
|
@ -62,9 +67,7 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "result is sorted by module then name then arity" do
|
test "result is sorted by module then name then arity", %{baseline: result} do
|
||||||
result = Analyzer.analyze()
|
|
||||||
|
|
||||||
sorted =
|
sorted =
|
||||||
Enum.sort_by(result, fn e -> {Atom.to_string(e.module), e.name, e.arity} end)
|
Enum.sort_by(result, fn e -> {Atom.to_string(e.module), e.name, e.arity} end)
|
||||||
|
|
||||||
|
|
@ -75,8 +78,8 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
Enum.map(sorted, & &1.module)
|
Enum.map(sorted, & &1.module)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exclude option removes whole modules from the output" do
|
test "exclude option removes whole modules from the output", %{baseline: result} do
|
||||||
[first | _] = Analyzer.analyze()
|
[first | _] = result
|
||||||
|
|
||||||
assert first
|
assert first
|
||||||
|
|
||||||
|
|
@ -84,8 +87,7 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
refute Enum.any?(excluded, &(&1.module == first.module))
|
refute Enum.any?(excluded, &(&1.module == first.module))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exclude option removes specific MFA triples" do
|
test "exclude option removes specific MFA triples", %{baseline: result} do
|
||||||
result = Analyzer.analyze()
|
|
||||||
[first | _] = result
|
[first | _] = result
|
||||||
|
|
||||||
assert first
|
assert first
|
||||||
|
|
@ -136,8 +138,8 @@ defmodule MixUnused.AnalyzerTest do
|
||||||
assert result == []
|
assert result == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exclude with both module atoms and MFA tuples filters both kinds" do
|
test "exclude with both module atoms and MFA tuples filters both kinds", %{baseline: result} do
|
||||||
[first, second | _] = Analyzer.analyze()
|
[first, second | _] = result
|
||||||
|
|
||||||
excluded =
|
excluded =
|
||||||
Analyzer.analyze(exclude: [first.module, {second.module, second.name, second.arity}])
|
Analyzer.analyze(exclude: [first.module, {second.module, second.name, second.arity}])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue