aprs.me/test/mix_unused/analyzer_test.exs
Graham McIntire 4c5c730ee7
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.
2026-05-09 10:56:03 -05:00

172 lines
5.5 KiB
Elixir

defmodule MixUnused.AnalyzerTest do
use ExUnit.Case, async: false
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
test "renders a friendly message when there are no entries" do
assert Analyzer.format([]) =~ "No unused public functions"
end
test "groups by module, lists each entry, and includes file:line" do
entries = [
%{module: Foo, name: :a, arity: 1, file: "lib/foo.ex", line: 5},
%{module: Foo, name: :b, arity: 0, file: "lib/foo.ex", line: 10},
%{module: Bar, name: :c, arity: 2, file: "lib/bar.ex", line: 7}
]
out = Analyzer.format(entries)
assert out =~ "Unused public functions"
assert out =~ "Foo"
assert out =~ "def a/1"
assert out =~ "def b/0"
assert out =~ "lib/foo.ex:5"
assert out =~ "lib/foo.ex:10"
assert out =~ "Bar"
assert out =~ "def c/2"
assert out =~ "lib/bar.ex:7"
end
test "shows just the file when line is nil" do
entries = [
%{module: Foo, name: :x, arity: 0, file: "lib/foo.ex", line: nil}
]
out = Analyzer.format(entries)
assert out =~ "(lib/foo.ex)"
end
test "shows '?' when both file and line are missing" do
entries = [
%{module: Foo, name: :x, arity: 0, file: nil, line: nil}
]
out = Analyzer.format(entries)
assert out =~ "(?)"
end
end
describe "analyze/1 against the running project" do
test "returns a list of unused-entry maps", %{baseline: result} do
assert is_list(result)
# Every entry has the expected shape.
Enum.each(result, fn entry ->
assert is_atom(entry.module)
assert is_atom(entry.name)
assert is_integer(entry.arity)
assert is_nil(entry.file) or is_binary(entry.file)
assert is_nil(entry.line) or is_integer(entry.line)
end)
end
test "result is sorted by module then name then arity", %{baseline: result} do
sorted =
Enum.sort_by(result, fn e -> {Atom.to_string(e.module), e.name, e.arity} end)
# The analyzer's own sort key uses the atom directly, but inspect-string
# ordering differs only for funky atoms — so we just confirm the result
# is non-decreasing under our string-based sort.
assert Enum.map(result, & &1.module) ==
Enum.map(sorted, & &1.module)
end
test "exclude option removes whole modules from the output", %{baseline: result} do
[first | _] = result
assert first
excluded = Analyzer.analyze(exclude: [first.module])
refute Enum.any?(excluded, &(&1.module == first.module))
end
test "exclude option removes specific MFA triples", %{baseline: result} do
[first | _] = result
assert first
excluded =
Analyzer.analyze(exclude: [{first.module, first.name, first.arity}])
refute Enum.any?(
excluded,
&(&1.module == first.module and &1.name == first.name and &1.arity == first.arity)
)
end
test "honors :app option — empty list when filtered to a non-existent app" do
result = Analyzer.analyze(app: :no_such_app_for_real)
assert result == []
end
test ":app=nil hits in_app?(_, nil) → true (no filtering)" do
# When app is nil, in_app? returns true for every module — so the
# analyzer scans everything in the compile path, not just the project.
# This exercises line 86 (defp in_app?(_module, nil), do: true).
tmp = Path.join(System.tmp_dir!(), "mu_app_nil_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp)
try do
result = Analyzer.analyze(app: nil, compile_path: tmp)
assert result == []
after
File.rm_rf(tmp)
end
end
test "honors :compile_path option pointing to an empty directory" do
tmp = Path.join(System.tmp_dir!(), "mu_empty_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp)
try do
result = Analyzer.analyze(compile_path: tmp)
assert result == []
after
File.rm_rf(tmp)
end
end
test "honors :compile_path option pointing to a non-existent directory" do
result = Analyzer.analyze(compile_path: "/nonexistent/path/for/test/#{System.unique_integer([:positive])}")
assert result == []
end
test "exclude with both module atoms and MFA tuples filters both kinds", %{baseline: result} do
[first, second | _] = result
excluded =
Analyzer.analyze(exclude: [first.module, {second.module, second.name, second.arity}])
refute Enum.any?(excluded, &(&1.module == first.module))
refute Enum.any?(
excluded,
&(&1.module == second.module and &1.name == second.name and &1.arity == second.arity)
)
end
end
describe "format/1 with grouped multiple-modules and missing line metadata" do
test "formats a mix of present and missing line numbers" do
entries = [
%{module: A, name: :one, arity: 1, file: "lib/a.ex", line: 5},
%{module: A, name: :two, arity: 0, file: "lib/a.ex", line: nil},
%{module: B, name: :three, arity: 2, file: nil, line: nil}
]
out = Analyzer.format(entries)
assert out =~ "A"
assert out =~ "B"
assert out =~ "lib/a.ex:5"
assert out =~ "(lib/a.ex)"
assert out =~ "(?)"
end
end
end