StatifierPersistence. Testing. StorageConformance
(StatifierPersistence v0.12.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.
That setup is the only callback this module registers, and it writes
nothing: it opens a handle and, when the adapter exports isolate/1,
isolates it. Every row a generated case needs it inserts inside the case
body, so the first write against the adapter is always the running
test's own.
That matters because ExUnit runs setup callbacks in the order they are
defined, and the ones this template registers are defined where you write
use. A host whose adapter needs a per-test binding established before
any write - a session parameter, a connection-scoped setting, a sandbox
checkout - must define that setup above the use:
defmodule MyApp.EctoAdapterConformanceTest do
setup do
MyApp.Tenant.bind!(...)
:ok
end
use StatifierPersistence.Testing.StorageConformance,
adapter: MyApp.EctoAdapter,
opts: [repo: MyApp.Repo]
endA setup written below the use runs after every callback this template
registers. What the template guarantees such a callback is that no row
has been written yet - not that nothing has run: the handle in
context.store is already open, and already isolated, by the time it is
called.
The optional execution 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_execution/3 gets the
same treatment at generation time: when the adapter under test exports
it, the suite generates the per-execution 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_execution/3,
list_executions_by_metadata/2 and list_execution_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_executions_by_metadata/2 and
list_execution_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 executions, and a
green execution 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_execution/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.