prop/test/support/data_case.ex
Graham McIntire 82bcaa5a54
Some checks failed
Build and Push / Build and Push Docker Image (push) Has been cancelled
refactor: idiomatic test sandbox setup (manual mode, single reset hook)
Replace the non-idiomatic :auto sandbox mode with :manual, eliminating
the root cause of stale data across test runs. The :auto mode let any
process auto-checkout and commit outside test transactions — Oban in
testing:inline already disables plugins/queues, so :auto was never
needed.

Changes:
- test_helper: Sandbox.mode(Repo, :manual); drop manual app boot (mix
  now starts the app idiomatically), drop --no-start alias, drop
  table-cleanup loop
- DataCase: single reset_test_state/0 consolidates GridCache.clear,
  ScoreCache.clear, score-file wipe, and default HTTP stubs; drop
  the :auto-restore on_exit hack; setup_sandbox now just start_owner
- ConnCase: delegates shared reset to DataCase, removes duplicate
  cache clears and stub installs
- config: pool Ecto.Adapters.SQL.Sandbox (idiomatic canonical form)

Note: GridCache/ScoreCache are globally-registered GenServers, so
the architecturally clean per-test start_supervised! approach
conflicts with async:true (parallel tests can't register the same
name). The centralized clear in reset_test_state is the practical
compromise — one place, documented, single call.
2026-08-04 17:47:49 -05:00

76 lines
2 KiB
Elixir

defmodule Microwaveprop.DataCase do
@moduledoc """
Test case template for tests that need database access.
"""
use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL.Sandbox
alias Microwaveprop.Propagation.ScoreCache
alias Microwaveprop.Weather.GridCache
using do
quote do
import Ecto
import Ecto.Changeset
import Ecto.Query
import Microwaveprop.DataCase
alias Microwaveprop.Repo
end
end
setup tags do
pid = Microwaveprop.DataCase.setup_sandbox(tags)
Microwaveprop.DataCase.reset_test_state()
Req.Test.set_req_test_from_context(tags)
:ok
end
@doc """
Reset state that persists across tests within a single ExUnit run.
The sandbox isolates the database, but ETS-backed caches
(GridCache, ScoreCache) and on-disk score files survive across
test boundaries because the cache GenServers and filesystem live
in the app's BEAM process tree, not per-test. A single reset
here avoids per-file setup duplication.
"""
@spec reset_test_state :: :ok
def reset_test_state do
dir = Application.get_env(:microwaveprop, :propagation_scores_dir)
if is_binary(dir), do: File.rm_rf(dir)
GridCache.clear()
ScoreCache.clear()
Req.Test.stub(Microwaveprop.Weather.NexradClient, fn conn ->
Plug.Conn.send_resp(conn, 404, "not found")
end)
Req.Test.stub(Microwaveprop.Weather.HrrrClient, fn conn ->
Plug.Conn.send_resp(conn, 404, "not found")
end)
:ok
end
@spec setup_sandbox(map()) :: pid()
def setup_sandbox(tags) do
shared? = not tags[:async]
pid = Sandbox.start_owner!(Microwaveprop.Repo, shared: shared?)
on_exit(fn -> Sandbox.stop_owner(pid) end)
pid
end
@spec errors_on(Ecto.Changeset.t()) :: map()
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end