Documentation
¶
Overview ¶
Package backendconformance is a reusable test suite that a config backend adapter runs against its own backend to prove it behaves like a first-class layer.
It is the remote-source counterpart of config/conformance, which does the same job for file codecs. A codec plugs into shared file machinery, so that suite can construct everything from a byte sample. A backend owns its whole interaction with a remote system — reading, versioning, staging, committing, watching — so this suite cannot build one itself. The adapter supplies a factory instead (Suite.NewBackend), plus a Control that stands in for another client of the same system: it changes the backing store out of band and re-opens a backend over it.
The trap this exists to make unmissable is the same one config/conformance guards for files, wearing a remote costume: the conflict fingerprint — here a version — must be captured at Load, not at Prepare. A backend that fetches a fresh version inside Prepare and compares Verify against that compares the intruder's data with itself, so every stale write is accepted and conflict detection silently never fires. The conflict_detected subtest catches it.
An adapter writes one test:
func TestConformance(t *testing.T) {
backendconformance.Run(t, backendconformance.Suite{
NewBackend: func(t *testing.T, seed map[string]any) (config.Backend, backendconformance.Control) {
remote := newFakeRemote(seed) // nil seed => absent source
return newConsulBackend(remote, "app/"), &control{remote}
},
Seed: map[string]any{"server": map[string]any{"port": 9090}},
Defines: map[string]string{"server.port": "9090"},
WriteKey: "server.port", WriteValue: "8080",
})
}
It uses the standard library testing package and nothing else — deliberately no testify — so an adapter that runs it takes on no assertion-library dependency it would otherwise avoid.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run executes the whole suite against s, one named subtest per contract, so a failing adapter sees exactly which one it breaks.
Every backend is checked for per-key merge and precedence with a layer of another format, tolerance of an absent source, and provenance naming the backend. A config.WritableBackend additionally has its write round-trip and — the trap the suite exists for — its refusal of a change that landed between load and commit with config.ErrConflict checked; a read-only backend instead has its layer confirmed skipped by write routing — or, when it is sensitive (a secrets backend), the routed-beneath write confirmed refused with config.ErrSensitiveLeak. A config.WatchableBackend has a foreign change confirmed to reach observers.
Types ¶
type Control ¶
type Control interface {
// Mutate changes the backing store as another client would: it moves the
// version the backend's conflict check compares against, and — for a
// watchable backend — emits the backend's change signal. It must change at
// least one value visible in the merged config, so a reload is not coalesced
// away as a no-op.
Mutate(t *testing.T)
// Reopen returns a fresh backend over the same backing store its NewBackend
// call created, so the suite can prove a committed write actually landed
// there rather than only in the Store's in-memory snapshot.
Reopen(t *testing.T) config.Backend
}
Control stands in for another client of the same backing store. It is how the suite simulates a foreign change — for the conflict and watch assertions — and how it re-reads the store to prove a committed write reached it.
type Suite ¶
type Suite struct {
// NewBackend builds a fresh backend over a fresh backing store seeded with
// the given nested values, plus a [Control] over that same store. A nil seed
// means an absent source: the backend's Load must report it not there
// (fs.ErrNotExist) rather than erroring. Each call must be independent — two
// calls must not share a backing store — except through the [Control], whose
// Reopen returns a backend over the store its NewBackend call created.
NewBackend func(t *testing.T, seed map[string]any) (config.Backend, Control)
// Seed is the nested values the backend is seeded with, and Defines is what
// they read back through a View. Both are required, with at least one key.
// Seed carries native types ({"server": {"port": 9090}}); Defines is the
// string each reads back (GetString), so the suite asserts decode, merge and
// provenance without knowing the system.
Seed map[string]any
Defines map[string]string
// WriteKey and WriteValue are a key the backend can set and the value it is
// set to — required only when NewBackend returns a [config.WritableBackend].
// The value must read back after the write.
WriteKey string
WriteValue string
}
Suite describes a backend and the minimal knowledge the assertions need. The capability subtests — write and watch — run only when the backend the factory returns satisfies config.WritableBackend or config.WatchableBackend, so a read-only backend runs the read contract and no more.