locktest

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package locktest provides a controllable in-memory Driver implementation and a conformance test suite for use in unit tests.

Conformance suites

Two suites are provided to separate concerns:

  • RunDriverConformance: semantic correctness (C-1..C-4, C-7). Runs against any Driver, including FakeDriver. No real-time waits; safe for -race.

  • RunDriverTTLConformance: TTL physics (C-5, C-6). Uses real time.Sleep because it is testing real-clock TTL expiry on backends. Intended for integration tests (e.g., adapters/redis/integration_test.go) where wall-clock waits are acceptable.

FakeDriver conformance tests (conformance_test.go) call only RunDriverConformance. Redis integration tests call both.

Package locktest provides a controllable in-memory Driver implementation and a conformance test suite for use in unit tests.

FakeDriver implements runtime/distlock.Driver in memory and is intended for unit tests of the distlock manager. RunDriverConformance verifies that any Driver implementation behaves identically for token ownership semantics.

Clock injection warning

NewFakeDriver uses real time.Now for TTL expiry checks by default. When testing alongside FakeClock for the manager, you MUST call NewFakeDriverWithClock(fc.Now) (or fd.WithClock(fc.Now)) on the FakeDriver as well — otherwise the manager's logical clock and the driver's TTL clock will diverge, causing intermittent test failures.

Index

Constants

This section is empty.

Variables

View Source
var ErrDriverIO = errors.New("locktest: simulated driver I/O error")

ErrDriverIO is a sentinel error for injecting I/O failures in tests.

Functions

func RunDriverConformance

func RunDriverConformance(t *testing.T, factory DriverFactory)

RunDriverConformance runs the semantic correctness conformance suite (C-1..C-4, C-7) against the supplied factory. Both FakeDriver (locktest) and RedisDriver (adapters/redis) must pass this suite.

This suite makes no real-time waits and is safe to run with -race.

Mirrors the outboxtest.RunStoreConformanceSuite pattern.

func RunDriverTTLConformance

func RunDriverTTLConformance(t *testing.T, factory DriverFactory)

RunDriverTTLConformance runs the TTL physics conformance suite (C-5, C-6) against the supplied factory. These tests use real time.Sleep because they are validating actual backend TTL expiry.

Call this from integration tests where wall-clock waits are acceptable (e.g., adapters/redis/integration_test.go). Do NOT call it for FakeDriver conformance — FakeDriver's TTL expiry is clock-injected and tested via the manager's FakeClock-driven renew cycles instead.

C-5 also covers the Lock.Orphan() backend expectation: after renewal stops (no Renew calls, no Release call), the key persists and expires only via TTL, at which point SetNX succeeds for a competitor. The Driver has no Orphan method — the orphan semantic lives entirely in the manager layer (handleOrphan stops renewal without any Driver I/O); C-5 validates the underlying physics that makes that contract possible.

Types

type DriverFactory

type DriverFactory func(t *testing.T) distlock.Driver

DriverFactory returns a fresh Driver and its initial clock for each test sub-case. For real backends the factory should set up a clean namespace/key prefix.

type FakeDriver

type FakeDriver struct {
	// contains filtered or unexported fields
}

FakeDriver is a thread-safe in-memory implementation of distlock.Driver. It is intended for unit tests only.

Controls:

  • NextSetNXResult: if set to false, the next SetNX returns (false, nil) — simulates busy key
  • NextRenewError: if non-nil, the next Renew call returns (false, err) — single-shot
  • persistRenewError: if non-nil, every Renew call returns (false, err) until ClearRenewError
  • NextRenewHeld: if set to false, the next Renew returns (false, nil) — simulates ownership lost
  • NextReleaseError: if non-nil, the next Release call returns err — single-shot
  • blockRenewCh: if non-nil, the next Renew blocks until the channel is closed — use BlockNextRenew() + UnblockRenew() for controlled blocking/unblocking

Use NewFakeDriverWithClock when pairing with FakeClock to ensure the driver's TTL expiry logic uses the same logical time as the manager.

func NewFakeDriver

func NewFakeDriver() *FakeDriver

NewFakeDriver creates a new FakeDriver using real-time clock.

Default uses real time.Now for TTL expiry checks. When testing alongside FakeClock for the manager, you MUST call WithClock(fc.Now) on the FakeDriver as well — otherwise the manager's logical clock and the driver's TTL clock will diverge, causing intermittent test failures.

Use NewFakeDriverWithClock for a one-step constructor that wires the clock.

func NewFakeDriverWithClock

func NewFakeDriverWithClock(now func() time.Time) *FakeDriver

NewFakeDriverWithClock creates a new FakeDriver using the provided clock function for TTL expiry checks. Use this when pairing with FakeClock:

fc := locktest.NewFakeClock(time.Time{})
fd := locktest.NewFakeDriverWithClock(fc.Now)

func (*FakeDriver) BlockNextRenew

func (fd *FakeDriver) BlockNextRenew() <-chan struct{}

BlockNextRenew arms a one-shot block: the next Renew call will block until UnblockRenew() is called (or the Renew context is canceled). The returned entered channel is closed by the Renew call once it has entered its body but before it blocks — tests can wait on it to confirm the goroutine is in-flight.

Usage:

entered := fd.BlockNextRenew()
<-entered            // Renew is now blocked inside FakeDriver
lock.Orphan()        // should not block even though Renew is in-flight
fd.UnblockRenew()    // let Renew complete

func (*FakeDriver) Calls

func (fd *FakeDriver) Calls(method string) int

Calls returns the total number of times the named method was called. method is one of "SetNX", "Renew", "Release".

func (*FakeDriver) ClearRenewError

func (fd *FakeDriver) ClearRenewError()

ClearRenewError clears both the single-shot and persistent renew error injections. After this call, Renew behaves normally (uses in-memory state).

func (*FakeDriver) LastRenewDeadline

func (fd *FakeDriver) LastRenewDeadline() time.Time

LastRenewDeadline returns the deadline extracted from the context passed to the most recent Renew call. Returns the zero time if no Renew has been called or if the context carried no deadline.

Used by TestLocker_TC12_DriftFactor to assert that the Renew RPC context deadline reflects the configured drift factor:

deadline ≈ clock.Now() + ttl − drift

func (*FakeDriver) Release

func (fd *FakeDriver) Release(_ context.Context, key, token string) error

Release implements distlock.Driver.

func (*FakeDriver) Renew

func (fd *FakeDriver) Renew(ctx context.Context, key, token string, ttl time.Duration) (bool, error)

Renew implements distlock.Driver. Records the deadline from ctx for test introspection via LastRenewDeadline.

func (*FakeDriver) ResetCalls

func (fd *FakeDriver) ResetCalls()

ResetCalls resets the call counters for all methods.

func (*FakeDriver) SetNX

func (fd *FakeDriver) SetNX(_ context.Context, key, token string, ttl time.Duration) (bool, error)

SetNX implements distlock.Driver.

func (*FakeDriver) SetNextReleaseError

func (fd *FakeDriver) SetNextReleaseError(err error)

SetNextReleaseError injects an I/O error for the next Release call (single-shot).

func (*FakeDriver) SetNextRenewError

func (fd *FakeDriver) SetNextRenewError(err error)

SetNextRenewError injects an I/O error for the next Renew call (single-shot). After one Renew call consumes the error, subsequent calls behave normally (unless SetRenewErrorPersistent is also set).

func (*FakeDriver) SetNextRenewHeld

func (fd *FakeDriver) SetNextRenewHeld(held bool)

SetNextRenewHeld injects the held result for the next Renew call. false simulates ownership lost (another holder took the key).

func (*FakeDriver) SetNextSetNX

func (fd *FakeDriver) SetNextSetNX(acquired bool)

SetNextSetNX injects the result for the next SetNX call. false simulates "another holder owns the key".

func (*FakeDriver) SetRenewErrorPersistent

func (fd *FakeDriver) SetRenewErrorPersistent(err error)

SetRenewErrorPersistent injects an I/O error that is returned by every Renew call until ClearRenewError is called. Use this to simulate persistent backend unavailability (e.g. to exhaust the retry budget in TC-14).

func (*FakeDriver) Snapshot

func (fd *FakeDriver) Snapshot() map[string]string

Snapshot returns the current keys held by the FakeDriver. Used for white-box assertions in conformance tests.

func (*FakeDriver) UnblockRenew

func (fd *FakeDriver) UnblockRenew()

UnblockRenew unblocks a previously blocked Renew call. Safe to call even if no Renew is currently blocked.

func (*FakeDriver) WithClock

func (fd *FakeDriver) WithClock(now func() time.Time) *FakeDriver

WithClock replaces the time source used for TTL expiry. Useful when tests need to advance time to simulate TTL expiry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL