From b2a1d8e7e96eb2f1cb6dd359d6357a140f959ee8 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 8 May 2026 13:09:24 -0500 Subject: [PATCH] Add coverage tests for MixUnused.Analyzer and HistoricalLoader edge cases --- .../live/map_live/historical_loader_test.exs | 103 ++++++++++++++++++ test/mix_unused/analyzer_test.exs | 48 ++++++++ 2 files changed, 151 insertions(+) diff --git a/test/aprsme_web/live/map_live/historical_loader_test.exs b/test/aprsme_web/live/map_live/historical_loader_test.exs index 39313ed..6ac8ff2 100644 --- a/test/aprsme_web/live/map_live/historical_loader_test.exs +++ b/test/aprsme_web/live/map_live/historical_loader_test.exs @@ -444,6 +444,109 @@ defmodule AprsmeWeb.MapLive.HistoricalLoaderTest do end end + describe "low zoom heat map path with string-keyed packet ids" do + test "string-keyed :id packet uses get_packet_key string-key clause" do + now = DateTime.utc_now() + + Mox.stub(Aprsme.PacketsMock, :get_recent_packets, fn _opts -> + [ + %{ + "id" => "str-keyed-1", + "sender" => "STR-1", + "callsign" => "STR-1", + "received_at" => now, + "lat" => 33.0, + "lon" => -96.0, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + path: "" + } + ] + end) + + socket = + build_socket(%{ + map_bounds: %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}, + map_zoom: 5, + historical_loading: true, + loading_generation: 1, + total_batches: 3 + }) + + result = HistoricalLoader.load_historical_batch(socket, 0, nil) + # The string-keyed id was placed into historical_packets via the + # get_packet_key/1 string-key clause. + assert is_map(result.assigns.historical_packets) + end + end + + describe "very zoomed out (zoom < 5) get_loading_params_for_zoom fallback" do + test "zoom 2 uses the catch-all params {50, 5}" do + Mox.stub(Aprsme.PacketsMock, :get_recent_packets, fn _opts -> [] end) + + socket = + build_socket(%{ + map_bounds: %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}, + map_zoom: 2, + loading_generation: 0 + }) + + result = HistoricalLoader.start_progressive_historical_loading(socket) + # Catch-all clause produces 5 batches. + assert result.assigns.total_batches == 5 + end + end + + describe "apply_memory_limit pruning path" do + test "exceeds @max_historical_packets and triggers prune_oldest_packets" do + now = DateTime.utc_now() + # Pre-populate historical_packets with > 5000 entries (the cap). + historical_packets = + for i <- 1..5_001, into: %{} do + {to_string(i), + %{ + id: "old-#{i}", + received_at: DateTime.add(now, -i, :second), + lat: 33.0, + lon: -96.0 + }} + end + + Mox.stub(Aprsme.PacketsMock, :get_recent_packets, fn _opts -> + [ + %{ + id: "newcomer", + sender: "NEW-1", + base_callsign: "NEW", + ssid: "1", + lat: 33.5, + lon: -96.5, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: now, + path: "" + } + ] + end) + + socket = + build_socket(%{ + map_bounds: %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}, + map_zoom: 5, + historical_loading: true, + historical_packets: historical_packets, + loading_generation: 1, + total_batches: 3 + }) + + result = HistoricalLoader.load_historical_batch(socket, 0, nil) + # After pruning, the historical_packets size should be <= 5000. + assert map_size(result.assigns.historical_packets) <= 5_000 + end + end + describe "schedules next batch when not final" do test "saturated batch with more rows triggers maybe_schedule_next_batch" do now = DateTime.utc_now() diff --git a/test/mix_unused/analyzer_test.exs b/test/mix_unused/analyzer_test.exs index 82243f9..59c7051 100644 --- a/test/mix_unused/analyzer_test.exs +++ b/test/mix_unused/analyzer_test.exs @@ -103,5 +103,53 @@ defmodule MixUnused.AnalyzerTest do result = Analyzer.analyze(app: :no_such_app_for_real) assert result == [] 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" do + [first, second | _] = Analyzer.analyze() + + 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