StatifierPersistence.Testing.StorageConformance (StatifierPersistence v0.9.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]
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 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 postgres

Nothing 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.