From 446497d60b54be7fcc0d2b70648a0521a3b7cfac Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 8 May 2026 14:24:47 -0500 Subject: [PATCH] Push coverage past 90% with MapLive postgres_packet/packet_batch and preferred_tracked_packet tests --- .../live/map_live/integration_test.exs | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) diff --git a/test/aprsme_web/live/map_live/integration_test.exs b/test/aprsme_web/live/map_live/integration_test.exs index bf4c5b8..b5639cc 100644 --- a/test/aprsme_web/live/map_live/integration_test.exs +++ b/test/aprsme_web/live/map_live/integration_test.exs @@ -445,5 +445,368 @@ defmodule AprsmeWeb.MapLive.IntegrationTest do Process.sleep(50) assert render(view) =~ "aprs-map" end + + test "{:DOWN, ref, _, batcher_pid, _} restarts the PacketBatcher", %{conn: conn} do + {:ok, view, _html} = live(conn, "/", on_error: :warn) + + original_pid = + :sys.get_state(view.pid).socket.assigns.batcher_pid + + # Send a fake DOWN message to simulate the batcher dying. + send(view.pid, {:DOWN, make_ref(), :process, original_pid, :test_kill}) + Process.sleep(30) + + # The handle_info DOWN handler should have spawned a new batcher. + new_pid = :sys.get_state(view.pid).socket.assigns.batcher_pid + assert is_pid(new_pid) + end + + test "{:process_pending_bounds} with pending_bounds set triggers handle_bounds_update", %{conn: conn} do + {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) + render_hook(view, "map_ready", %{}) + + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + new_socket = %{ + inner_socket + | assigns: + inner_socket.assigns + |> Map.put(:pending_bounds, %{north: 35.0, south: 31.0, east: -94.0, west: -98.0}) + |> Map.put(:historical_loading, false) + } + + %{channel_state | socket: new_socket} + end) + + send(view.pid, {:process_pending_bounds}) + Process.sleep(50) + assert render(view) =~ "aprs-map" + end + + test "stale bounds-update Logger.debug branch with :debug log level", %{conn: conn} do + original_level = Logger.level() + Logger.configure(level: :debug) + + try do + {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) + + # The LV channel state nests the socket under :socket. Set pending_bounds + # via :sys.replace_state with that wrapper structure. + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + new_socket = %{ + inner_socket + | assigns: Map.put(inner_socket.assigns, :pending_bounds, %{north: 99.0, south: 0.0, east: 1.0, west: 0.0}) + } + + %{channel_state | socket: new_socket} + end) + + # Send a different bounds — should hit the "stale" Logger.debug branch + # (line 1003 in index.ex), evaluating the format string at :debug level. + send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}}) + Process.sleep(30) + + assert render(view) =~ "aprs-map" + after + Logger.configure(level: original_level) + end + end + + test "tracked-callsign postgres_packet path with batcher_pid forced nil", %{conn: conn} do + # Force batcher_pid to nil so {:postgres_packet, _} flows through + # handle_info_postgres_packet (lines 1053+). + {:ok, view, _html} = live(conn, "/?call=PGTRK", on_error: :warn) + render_hook(view, "map_ready", %{}) + + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + new_socket = %{inner_socket | assigns: Map.put(inner_socket.assigns, :batcher_pid, nil)} + %{channel_state | socket: new_socket} + end) + + # Send a packet with sender matching tracked callsign — exercises the + # tracked-callsign branch in handle_info_postgres_packet. + packet = %{ + id: "pgtrk-1", + sender: "PGTRK", + base_callsign: "PGTRK", + ssid: "0", + lat: 33.0, + lon: -96.0, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: DateTime.utc_now(), + path: "WIDE1-1" + } + + send(view.pid, {:postgres_packet, packet}) + Process.sleep(50) + + # Send a packet with a different sender — exercises the no-match branch. + send(view.pid, {:postgres_packet, %{packet | sender: "OTHER", base_callsign: "OTHER"}}) + Process.sleep(50) + + assert render(view) =~ "aprs-map" + end + + test "untracked postgres_packet path with batcher_pid nil hits handle_info_postgres_packet else branch", %{ + conn: conn + } do + {:ok, view, _html} = live(conn, "/", on_error: :warn) + render_hook(view, "map_ready", %{}) + + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + new_socket = %{inner_socket | assigns: Map.put(inner_socket.assigns, :batcher_pid, nil)} + %{channel_state | socket: new_socket} + end) + + packet = %{ + id: "untracked-1", + sender: "UNT", + base_callsign: "UNT", + ssid: "1", + lat: 33.0, + lon: -96.0, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: DateTime.utc_now(), + path: "WIDE1-1" + } + + send(view.pid, {:postgres_packet, packet}) + Process.sleep(50) + + assert render(view) =~ "aprs-map" + end + + test "spatial_packet path with batcher_pid nil hits handle_info_postgres_packet branch", %{conn: conn} do + {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) + render_hook(view, "map_ready", %{}) + + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + new_socket = %{inner_socket | assigns: Map.put(inner_socket.assigns, :batcher_pid, nil)} + %{channel_state | socket: new_socket} + end) + + packet = %{ + id: "spatial-x", + sender: "SPX", + base_callsign: "SPX", + ssid: "1", + lat: 33.0, + lon: -96.0, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: DateTime.utc_now(), + path: "WIDE1-1" + } + + send(view.pid, {:spatial_packet, packet}) + Process.sleep(50) + send(view.pid, {:streaming_packet, packet}) + Process.sleep(50) + + assert render(view) =~ "aprs-map" + end + + test "tracked-callsign positionless packets with NaiveDateTime received_at hits newer_packet? NaiveDateTime branch", + %{ + conn: conn + } do + {:ok, view, _html} = live(conn, "/?call=NAIV&lat=33.0&lng=-96.0&z=12", on_error: :warn) + render_hook(view, "map_ready", %{}) + + # Pre-load tracked latest packet without position with a NaiveDateTime. + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + current = %{ + sender: "NAIV", + base_callsign: "NAIV", + ssid: "0", + lat: nil, + lon: nil, + has_position: false, + received_at: ~N[2026-01-01 00:00:00] + } + + new_socket = %{ + inner_socket + | assigns: Map.put(inner_socket.assigns, :tracked_callsign_latest_packet, current) + } + + %{channel_state | socket: new_socket} + end) + + naive_packet = %{ + id: "naiv-1", + sender: "NAIV", + base_callsign: "NAIV", + ssid: "0", + lat: nil, + lon: nil, + has_position: false, + received_at: ~N[2026-06-01 00:00:00], + path: "" + } + + send(view.pid, {:packet_batch, [naive_packet]}) + Process.sleep(50) + + # Now exercise the unrelated-types fallback (DateTime vs nil). + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + current = %{ + sender: "NAIV", + base_callsign: "NAIV", + ssid: "0", + lat: nil, + lon: nil, + has_position: false, + received_at: nil + } + + new_socket = %{ + inner_socket + | assigns: Map.put(inner_socket.assigns, :tracked_callsign_latest_packet, current) + } + + %{channel_state | socket: new_socket} + end) + + send(view.pid, {:packet_batch, [Map.put(naive_packet, :received_at, nil)]}) + Process.sleep(50) + assert render(view) =~ "aprs-map" + end + + test "tracked-callsign with positionless incoming exercises preferred_tracked_packet branches", %{conn: conn} do + # Set up a tracked callsign with a current packet that has position. + # Send incoming packets without position to walk through the cond + # branches in preferred_tracked_packet/2. + {:ok, view, _html} = live(conn, "/?call=PREF&lat=33.0&lng=-96.0&z=12", on_error: :warn) + render_hook(view, "map_ready", %{}) + + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + # Pre-load tracked_callsign_latest_packet so preferred_tracked_packet/2 + # has a non-nil current_packet to compare against. + current = %{ + sender: "PREF", + base_callsign: "PREF", + ssid: "0", + lat: 33.0, + lon: -96.0, + has_position: true, + received_at: DateTime.add(DateTime.utc_now(), -120, :second) + } + + new_socket = %{ + inner_socket + | assigns: Map.put(inner_socket.assigns, :tracked_callsign_latest_packet, current) + } + + %{channel_state | socket: new_socket} + end) + + # Incoming packet without position — exercises lines 1842, 1843 + # (current has position, incoming doesn't → current returned). + no_pos_packet = %{ + id: "nopos-1", + sender: "PREF", + base_callsign: "PREF", + ssid: "0", + lat: nil, + lon: nil, + has_position: false, + received_at: DateTime.utc_now(), + path: "" + } + + send(view.pid, {:packet_batch, [no_pos_packet]}) + Process.sleep(50) + + # Both nil-position; newer DateTime returned. + :sys.replace_state(view.pid, fn channel_state -> + inner_socket = channel_state.socket + + current_no_pos = %{ + sender: "PREF", + base_callsign: "PREF", + ssid: "0", + lat: nil, + lon: nil, + has_position: false, + received_at: DateTime.add(DateTime.utc_now(), -1000, :second) + } + + new_socket = %{ + inner_socket + | assigns: Map.put(inner_socket.assigns, :tracked_callsign_latest_packet, current_no_pos) + } + + %{channel_state | socket: new_socket} + end) + + send(view.pid, {:packet_batch, [no_pos_packet]}) + Process.sleep(50) + assert render(view) =~ "aprs-map" + end + + test "tracked-callsign packet_batch processes the matching-sender branch", %{conn: conn} do + {:ok, view, _html} = live(conn, "/?call=BATCH&lat=33.0&lng=-96.0&z=12", on_error: :warn) + render_hook(view, "map_ready", %{}) + + render_hook(view, "bounds_changed", %{ + "bounds" => %{ + "north" => 34.0, + "south" => 32.0, + "east" => -95.0, + "west" => -97.0 + } + }) + + packets = [ + %{ + id: "btr-1", + sender: "BATCH", + base_callsign: "BATCH", + ssid: "0", + lat: 33.5, + lon: -96.0, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: DateTime.utc_now(), + path: "" + }, + %{ + id: "btr-2", + sender: "OTHER", + base_callsign: "OTHER", + ssid: "1", + lat: 33.5, + lon: -96.5, + has_position: true, + symbol_table_id: "/", + symbol_code: ">", + received_at: DateTime.utc_now(), + path: "" + } + ] + + send(view.pid, {:packet_batch, packets}) + Process.sleep(50) + assert render(view) =~ "aprs-map" + end end end