# `StatifierPersistence.Testing.StorageConformance`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.18.0/lib/statifier_persistence/testing/storage_conformance.ex#L1)

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]
    end

Every 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 `c: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]
    end

A `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
`c: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 `c: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 postgres

Nothing else in the suite is tagged: every remaining case runs, 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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
