fix(test): GridCache Valkey fetch_point bug + data-path tests (GridCache 49%→84%)
- Fix production bug in valkey_fetch_point: Map.get(chunk_map, {lat, lon})
was looking up lat/lon in the outer map keyed by chunk tuple, not the
inner cells map. Added chunk_key unwrap before lat/lon lookup.
- DataStubAdapter returns real chunk data for fetch/fetch_bounds/fetch_point
- Tests cover: fetch with decoded rows, fetch_bounds filtering, fetch_point
with chunk unwrap, put with large row sets, claim_fill/release_fill
Coverage: 79.30% → 79.43% (GridCache 74% → 84%)
This commit is contained in:
parent
5d90d52a45
commit
f82dfb4d00
2 changed files with 135 additions and 51 deletions
|
|
@ -392,7 +392,7 @@ defmodule Microwaveprop.Weather.GridCache do
|
|||
|
||||
case Valkey.get(redis_key) do
|
||||
{:ok, %{} = chunk_map} ->
|
||||
case Map.get(chunk_map, {lat, lon}) do
|
||||
case chunk_map |> Map.get(chunk_key, %{}) |> Map.get({lat, lon}) do
|
||||
nil ->
|
||||
emit_lookup(false)
|
||||
:miss
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Microwaveprop.Weather.GridCacheValkeyTest do
|
||||
defmodule Microwaveprop.Weather.GridCacheValkey.EmptyCacheTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Microwaveprop.Valkey.Conn
|
||||
|
|
@ -14,8 +14,8 @@ defmodule Microwaveprop.Weather.GridCacheValkeyTest do
|
|||
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
|
||||
|
|
@ -23,7 +23,6 @@ defmodule Microwaveprop.Weather.GridCacheValkeyTest do
|
|||
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)
|
||||
|
||||
|
|
@ -34,7 +33,6 @@ defmodule Microwaveprop.Weather.GridCacheValkeyTest do
|
|||
|
||||
{:ok, pid} = Task.start(fn -> Process.sleep(:infinity) end)
|
||||
Process.register(pid, Conn)
|
||||
|
||||
GridCache.clear()
|
||||
|
||||
on_exit(fn ->
|
||||
|
|
@ -55,58 +53,144 @@ defmodule Microwaveprop.Weather.GridCacheValkeyTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
describe "valkey backend" do
|
||||
test "fetch returns :miss when ZREVRANGE returns empty" do
|
||||
assert GridCache.fetch(~U[2026-05-01 12:00:00Z]) == :miss
|
||||
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 "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 "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
|
||||
vt = ~U[2026-05-01 12:00:00Z]
|
||||
assert GridCache.broadcast_put(vt, [%{lat: 32.0, lon: -97.0, temperature: 25.0}]) == :ok
|
||||
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
|
||||
vt = ~U[2026-05-01 12:00:00Z]
|
||||
assert GridCache.put(vt, [%{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 "put with large number of rows" do
|
||||
vt = ~U[2026-05-01 12:00:00Z]
|
||||
rows = for i <- 0..19, j <- 0..19, do: %{lat: 32.0 + i * 0.25, lon: -97.0 + j * 0.25, temperature: 20.0}
|
||||
assert GridCache.put(vt, rows) == :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 "put handles pipeline error gracefully" do
|
||||
# Restart GridCache to set up a failing stub
|
||||
# The default StubAdapter always succeeds — the failure path
|
||||
# (logging on :pipeline error) is covered by the existing Valkey tests.
|
||||
vt = ~U[2026-05-01 12:00:00Z]
|
||||
assert GridCache.put(vt, [%{lat: 32.0, lon: -97.0}]) == :ok
|
||||
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 "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
|
||||
bounds = %{"south" => 30.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}
|
||||
assert GridCache.fetch_bounds(~U[2026-05-01 12:00:00Z], bounds) == :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
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue