defmodule Microwaveprop.Weather.GridCacheValkey.EmptyCacheTest do use ExUnit.Case, async: false alias Microwaveprop.Valkey.Conn alias Microwaveprop.Weather.GridCache defmodule StubAdapter do @moduledoc false @behaviour Microwaveprop.Valkey.Adapter @impl true def command(_conn, ["ZREVRANGE", "prop:wg:vts", "0", "0"], _opts), do: {:ok, []} def command(_conn, ["PING"], _opts), do: {:ok, "PONG"} def command(_conn, ["ZRANGEBYSCORE", "prop:wg:vts", "-inf", _], _opts), do: {:ok, []} def command(_conn, ["GET" | _], _opts), do: {:ok, nil} def command(_conn, ["MGET" | _], _opts), do: {:ok, [nil]} def command(_conn, ["SCAN", "0", "MATCH", _, "COUNT", "500"], _opts), do: {:ok, ["0", []]} def command(_conn, _, _opts), do: {:ok, ["0", []]} @impl true def pipeline(_conn, cmds, _opts), do: {:ok, List.duplicate("OK", length(cmds))} end setup do prev_url = Application.get_env(:microwaveprop, :valkey_url) prev_adapter = Application.get_env(:microwaveprop, :valkey_adapter) Application.put_env(:microwaveprop, :valkey_url, "redis://localhost:6379") Application.put_env(:microwaveprop, :valkey_adapter, StubAdapter) case Process.whereis(Conn) do nil -> :ok _ -> Process.unregister(Conn) end {:ok, pid} = Task.start(fn -> Process.sleep(:infinity) end) Process.register(pid, Conn) GridCache.clear() on_exit(fn -> if pid && Process.alive?(pid) do Process.unregister(Conn) Process.exit(pid, :kill) end if prev_url, do: Application.put_env(:microwaveprop, :valkey_url, prev_url), else: Application.delete_env(:microwaveprop, :valkey_url) if prev_adapter, do: Application.put_env(:microwaveprop, :valkey_adapter, prev_adapter), else: Application.delete_env(:microwaveprop, :valkey_adapter) end) :ok end test "fetch returns :miss when ZREVRANGE returns empty" do assert GridCache.fetch(~U[2026-05-01 12:00:00Z]) == :miss end test "latest_valid_time returns nil when ZSET empty" do assert GridCache.latest_valid_time() == nil end test "prune_older_than returns 0 when no expired entries" do assert GridCache.prune_older_than(~U[2026-05-01 00:00:00Z]) == 0 end test "broadcast_put writes to valkey" do assert GridCache.broadcast_put(~U[2026-05-01 12:00:00Z], [%{lat: 32.0, lon: -97.0, temperature: 25.0}]) == :ok end test "put writes chunks and updates index" do assert GridCache.put(~U[2026-05-01 12:00:00Z], [%{lat: 32.0, lon: -97.0, temperature: 25.0}]) == :ok end test "fetch_point returns :miss for unknown point" do assert GridCache.fetch_point(~U[2026-05-01 12:00:00Z], 32.0, -97.0) == :miss end test "fetch_bounds returns :miss for empty cache" do assert GridCache.fetch_bounds(~U[2026-05-01 12:00:00Z], %{"south" => 30, "north" => 35, "west" => -100, "east" => -95}) == :miss end test "claim_fill and release_fill work" do vt = ~U[2026-05-01 12:00:00Z] assert GridCache.claim_fill(vt) == true assert GridCache.claim_fill(vt) == false GridCache.release_fill(vt) assert GridCache.claim_fill(vt) == true end end defmodule Microwaveprop.Weather.GridCacheValkey.DataCacheTest do use ExUnit.Case, async: false alias Microwaveprop.Valkey.Conn alias Microwaveprop.Weather.GridCache defmodule DataStubAdapter do @moduledoc false @behaviour Microwaveprop.Valkey.Adapter @vt_iso "2026-05-01T12:00:00Z" @chunk_key "prop:wg:#{@vt_iso}:6:-20" @chunk_cells %{ {32.0, -97.0} => %{lat: 32.0, lon: -97.0, temperature: 25.0}, {40.0, -74.0} => %{lat: 40.0, lon: -74.0, temperature: 20.0} } @chunk_data %{{6, -20} => @chunk_cells} # For fetch_point, the code does Map.get(chunk_map, {lat, lon}) # which looks up directly in the outer map. Precompute what the # actual lookup would return so we can stub correctly. @encoded :erlang.term_to_binary(@chunk_data) @impl true def command(_conn, ["ZREVRANGE", "prop:wg:vts", "0", "0"], _opts), do: {:ok, [@vt_iso]} def command(_conn, ["SCAN", "0", "MATCH", "prop:wg:#{@vt_iso}:*", "COUNT", "500"], _opts), do: {:ok, ["0", [@chunk_key]]} def command(_conn, ["MGET" | tail], _opts) do vals = Enum.map(tail, fn @chunk_key -> @encoded _ -> nil end) {:ok, vals} end def command(_conn, ["GET", @chunk_key], _opts), do: {:ok, @encoded} def command(_conn, ["GET" | _], _opts), do: {:ok, nil} def command(_conn, ["ZRANGEBYSCORE", "prop:wg:vts", "-inf", _], _opts), do: {:ok, []} def command(_conn, _, _opts), do: {:ok, ["0", []]} @impl true def pipeline(_conn, cmds, _opts), do: {:ok, List.duplicate("OK", length(cmds))} end setup do prev_url = Application.get_env(:microwaveprop, :valkey_url) prev_adapter = Application.get_env(:microwaveprop, :valkey_adapter) Application.put_env(:microwaveprop, :valkey_url, "redis://localhost:6379") Application.put_env(:microwaveprop, :valkey_adapter, DataStubAdapter) case Process.whereis(Conn) do nil -> :ok _ -> Process.unregister(Conn) end {:ok, pid} = Task.start(fn -> Process.sleep(:infinity) end) Process.register(pid, Conn) GridCache.clear() on_exit(fn -> if pid && Process.alive?(pid) do Process.unregister(Conn) Process.exit(pid, :kill) end if prev_url, do: Application.put_env(:microwaveprop, :valkey_url, prev_url), else: Application.delete_env(:microwaveprop, :valkey_url) if prev_adapter, do: Application.put_env(:microwaveprop, :valkey_adapter, prev_adapter), else: Application.delete_env(:microwaveprop, :valkey_adapter) end) :ok end test "fetch returns decoded rows" do result = GridCache.fetch(~U[2026-05-01 12:00:00Z]) assert {:ok, rows} = result assert length(rows) == 2 assert hd(rows).lat == 32.0 assert hd(rows).temperature == 25.0 end test "fetch_bounds filters rows within bounds" do bounds = %{"south" => 30.0, "north" => 35.0, "west" => -100.0, "east" => -95.0} assert {:ok, rows} = GridCache.fetch_bounds(~U[2026-05-01 12:00:00Z], bounds) assert length(rows) == 1 assert hd(rows).lat == 32.0 end test "fetch_point returns row for known point" do assert {:ok, row} = GridCache.fetch_point(~U[2026-05-01 12:00:00Z], 32.0, -97.0) assert row.temperature == 25.0 end test "latest_valid_time returns the most recent" do assert GridCache.latest_valid_time() == ~U[2026-05-01 12:00:00Z] end end