StatifierPersistence. Testing. StorageConformance
(StatifierPersistence v0.10.0)
Copy Markdown
View Source
The conformance suite every StatifierPersistence.Storage.Adapter must
pass. Ships in lib/ (ADR-0003 decision 5, st-ADR-0053's shape) so an
adapter in another package runs the identical suite:
defmodule MyApp.EctoAdapterConformanceTest do
use StatifierPersistence.Testing.StorageConformance,
adapter: MyApp.EctoAdapter,
opts: [repo: MyApp.Repo]
endEvery test generated here goes through either the adapter directly (the
callbacks in StatifierPersistence.Storage.Adapter) or through
StatifierPersistence.Storage, the guarded facade every adapter sits
behind. Nothing here reaches into test/ - the fixtures come from
StatifierPersistence.Testing.Charts, the sibling module this one is
named alongside, so the one-way StatifierPersistence.Testing.* rule
(ADR-0003 decision 5) holds for both.
init/1 is called once per test, in setup, so every generated test
starts from a fresh handle. When the adapter under test exports the
optional StatifierPersistence.Storage.Adapter.isolate/1 callback,
setup calls it right after init/1 - the hook an adapter backed by a
shared resource (a database connection, a sandbox checkout) uses to wrap
the test that follows in its own isolated unit. An adapter that exports
no such callback, like StatifierPersistence.Storage.InMemory, is
unaffected: the check is a function_exported?/3 guard, not a
requirement.
The optional run metadata map (ADR-0006) is treated differently again:
its cases are generated for every adapter and assert the answer this
adapter gives - a round trip when it declares support through
StatifierPersistence.Storage.Adapter.supports_metadata?/1, a
{:error, :metadata_unsupported} refusal at open when it does not.
Refusing is conformance; silently dropping the map is not, and that is
the failure these cases exist to catch.
The optional StatifierPersistence.Storage.Adapter.lock_run/3 gets the
same treatment at generation time: when the adapter under test exports
it, the suite generates the per-run lock tests (mutual exclusion of two
concurrent bodies, release after a raising fun); when it does not, they
are not generated at all - exporting the callback is what opts an
adapter into its contract.
Opting out by not exporting is the whole story for an adapter written
from scratch. It is not the whole story for
StatifierPersistence.Storage.Ecto, which exports lock_run/3,
list_runs_by_metadata/2 and list_run_states_by_metadata/2 for every
Ecto backend but implements all three in Postgres-only SQL
(pg_advisory_xact_lock plus FOR UPDATE; jsonb containment). Point
that adapter at a backend that is not Postgres and the four cases those
three callbacks generate are generated and fail: the lock pair on SQL
the backend does not parse, the two listings on the refusal they answer
with instead. list_runs_by_metadata/2 and
list_run_states_by_metadata/2 consult supports_metadata?/1 before
they issue anything, so off Postgres they return
{:error, :metadata_unsupported} rather than raising (sp-4eo) - a
cleaner answer, but not the list these two cases assert over, so their
tag stays where the raise put it.
So those four carry @tag :postgres, and such a host excludes them by
tag rather than forking the suite:
mix test --exclude postgresNothing else in the suite is tagged: every remaining case runs, and a
green run with four excluded is the honest report of what that backend
supports. It is honest only alongside actually declining what the tag
excludes - serialization: pointed at the host's own strategy rather
than the adapter's lock_run/3, and no reliance on the child listings.
Excluding the tag while still routing serialization through a lock the
backend cannot honor hides a failure instead of opting out of a
contract. docs/non-postgres-backends.md in this package is the guide:
what declining costs, and how to verify.