Packages
double_down
0.46.0
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
double_down
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.46.0]
### Added
- `query/1,2,3` and `query!/1,2,3` added to `DoubleDown.Repo`
contract. Raw SQL operations from ecto_sql — adding them to the
contract makes them interceptable via expects/stubs so code paths
that call `Repo.query!` can be tested without a database.
- FK backfill on insert. When inserting a struct with a loaded
`belongs_to` association but a nil FK field, InMemory now copies
the parent's PK into the FK field — matching real Ecto.Repo
behaviour. Makes ExMachina factories work transparently:
`insert(:child, parent: parent)` automatically sets the FK.
Implemented in `Repo.Impl.EctoParity.backfill_foreign_keys/1`.
- Association fields are reset to `%Ecto.Association.NotLoaded{}`
on insert, matching real Ecto.Repo behaviour. Struct equality
comparisons between `insert(:thing)` and `Repo.get!(Thing, id)`
now work without comparing individual fields. Implemented in
`Repo.Impl.EctoParity.reset_associations/1`. Runs after FK
backfill (which needs the loaded association to extract the FK).
- `Repo.Impl.EctoParity` — new module for Ecto schema-introspection
concerns that make the in-memory fakes behave more like real Ecto.
### Changed
- InMemory's `get!`, `get_by!`, `one!` now raise
`Ecto.NoResultsError` (was `ArgumentError`). `one`, `one!`,
`get_by!` now raise `Ecto.MultipleResultsError` when multiple
records match (was `ArgumentError`). Matches real Ecto.Repo
behaviour so tests with `assert_raise Ecto.NoResultsError` work
without modification.
## [0.45.0]
### Added
- In-memory transaction rollback support. `rollback/1` in the
stateful test adapters (`Repo.InMemory` and `Repo.OpenInMemory`)
now restores the store to its pre-transaction state — inserts,
updates, and deletes within a rolled-back transaction are undone.
Implemented by snapshotting the store at `transact` start and
restoring via `Contract.Dispatch.restore_state/2` on rollback.
Only the Repo contract's state is restored; other contracts are
unaffected.
- `DoubleDown.Contract.Dispatch.get_state/1` — read the current
domain state for a contract. Returns `fallback_state` for
Double-managed handlers, raw state for `set_stateful_handler`.
- `DoubleDown.Contract.Dispatch.restore_state/2` — replace a single
contract's state in NimbleOwnership, leaving the handler function
and all other contracts' state untouched. Scoped to a single
contract by design.
## [0.44.0]
### Added
- Bang write operations: `insert!/1,2`, `update!/1,2`, `delete!/1,2`
added to `DoubleDown.Repo` contract and all three test doubles.
These were lost when auto-bang generation was removed in v0.38.0.
Needed for ExMachina integration (`ExMachina` calls `Repo.insert!`).
- `insert`/`insert!` now accept bare structs in addition to
changesets, matching `Ecto.Repo` behaviour. `delete`/`delete!`
now accept changesets in addition to structs.
- ExMachina integration tests demonstrating the factory + InMemory
pattern: factory-inserted records readable via `all`, `get`,
`get_by`, `exists?`, `aggregate` — no database, `async: true`,
at in-memory speed. `ex_machina ~> 2.7` added as a test-only
dependency.
- ExMachina integration documentation in `docs/repo.md` with
worked example: factory definition, test setup, reads, aggregates,
read-after-write, failure simulation. Cross-referenced from
`docs/getting-started.md`.
### Changed
- **Breaking:** `DoubleDown.Repo.Port` (test facade) renamed to
`DoubleDown.Test.Repo`. Natural alias gives `Repo.*` without
`as:` clause.
## [0.43.0]
### Changed
- **Breaking:** `DoubleDown.Facade` renamed to `DoubleDown.ContractFacade`.
Symmetric `<qualifier>Facade` naming across all three facade builders:
`ContractFacade`, `BehaviourFacade`, `DynamicFacade`.
- **Breaking:** `DoubleDown.Dynamic` renamed to `DoubleDown.DynamicFacade`.
- **Breaking:** `DoubleDown.Dispatch` renamed to
`DoubleDown.Contract.Dispatch`. The dispatch machinery is keyed by
contract module and belongs under Contract, not at the top level.
Child modules (`Defer`, `FakeHandler`, `StubHandler`, `Passthrough`)
moved accordingly. Moved to "Internals" doc group.
- **Breaking:** `DoubleDown.Repo.Test` renamed to `DoubleDown.Repo.Stub`.
The name now communicates what the module is — a stateless stub —
matching the test-double taxonomy (stub/mock/fake).
- **Breaking:** `DoubleDown.Repo.InMemory` renamed to
`DoubleDown.Repo.OpenInMemory` (open-world, fallback-based).
`DoubleDown.Repo.ClosedInMemory` renamed to
`DoubleDown.Repo.InMemory` (closed-world, recommended default).
The unqualified `InMemory` name now refers to the closed-world
store — the one most users should reach for, especially with
ExMachina factories.
- **Breaking:** `DoubleDown.Repo.Autogenerate` renamed to
`DoubleDown.Repo.Impl.Autogenerate`.
`DoubleDown.Repo.MultiStepper` renamed to
`DoubleDown.Repo.Impl.MultiStepper`.
`DoubleDown.Repo.InMemory.Shared` renamed to
`DoubleDown.Repo.Impl.InMemoryShared`.
Internal helpers moved to `Repo.Impl.*` namespace.
- `DoubleDown.BehaviourFacade.CompileHelper` renamed to
`DoubleDown.Facade.CompileHelper`. The `Facade.*` namespace is
shared internal infrastructure for all facade builders.
- Updated all documentation (README, getting-started, testing,
dynamic, repo, migration, process-sharing, logging) for the
new module names.
## [0.42.0]
### Added
- `DoubleDown.Repo.ClosedInMemory` — closed-world stateful Repo fake.
Unlike `Repo.InMemory` (open-world, where absence means "I don't
know"), `ClosedInMemory` treats the state as the complete truth —
if a record isn't in the state, it doesn't exist. This makes the
adapter authoritative for bare schema queryables without needing a
fallback function:
- **PK reads:** `get`/`get!` return `nil`/raise on miss (no fallback)
- **Clause reads:** `get_by`/`get_by!` scan and filter all records
- **Collection reads:** `all`, `one`/`one!`, `exists?` scan state
- **Aggregates:** `count`/`sum`/`avg`/`min`/`max` computed from state
- **Bulk writes:** `insert_all`, `delete_all`, `update_all`
(with `set:` updates)
`Ecto.Query` queryables still fall through to the fallback function
(or raise), since evaluating query expressions requires a query
engine. The fallback is the escape hatch, not the default path.
Enables the pattern of using ExMachina factories to write test data
into an in-memory store and testing against it without a database:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.ClosedInMemory)
insert(:user, name: "Alice", email: "alice@example.com")
assert [%User{}] = MyApp.Repo.all(User)
assert %User{} = MyApp.Repo.get_by(User, email: "alice@example.com")
- `DoubleDown.Repo.InMemory.Shared` — extracted shared helpers (state
access, writes, transactions, fallback dispatch, query helpers) from
`Repo.InMemory` into a shared module for reuse by `ClosedInMemory`.
Pure refactor of `Repo.InMemory` — no behaviour change.
- Updated documentation across `docs/repo.md` (ClosedInMemory section
with comparison table and ExMachina example), README (features table),
and `mix.exs` (module groups).
## [0.41.1]
### Fixed
- `BehaviourFacade` compilation failure on clean builds when the
behaviour and facade are in the same `elixirc_paths` batch.
`Code.Typespec.fetch_callbacks/1` needs the behaviour's `.beam`
file on disk, but during `mix compile` all files in the same
batch are compiled together — `.beam` files aren't written until
the batch finishes.
### Added
- `DoubleDown.BehaviourFacade.CompileHelper.ensure_compiled!/1` —
explicitly compiles a behaviour source file and writes its `.beam`
to the build directory. Only needed when the behaviour and facade
are in the same compilation batch (e.g. both in `test/support/`).
In normal usage the behaviour would be in `lib/` or a dependency,
already compiled in a prior batch.
- `BehaviourIntrospection` now falls back to `:code.get_object_code/1`
when `Code.Typespec.fetch_callbacks/1` can't find the `.beam` on the
standard code path.
## [0.41.0]
### Added
- `DoubleDown.BehaviourFacade` — generates dispatch facades for vanilla
Elixir `@behaviour` modules. Reads `@callback` declarations from
compiled behaviour modules via `Code.Typespec.fetch_callbacks/1` and
generates the same dispatch facade, `@spec` declarations, and
`__key__` helpers as `DoubleDown.Facade`. Supports all dispatch
paths (`test_dispatch?`, `static_dispatch?`, config-based).
defmodule MyApp.Todos do
use DoubleDown.BehaviourFacade,
behaviour: MyApp.Todos.Behaviour,
otp_app: :my_app
end
Use this for behaviours you don't control — third-party libraries,
existing `@behaviour` modules, or any module with `@callback`
declarations that you don't want to convert to `defcallback`.
For behaviours you do control, `DoubleDown.Facade` with
`defcallback` remains recommended (richer features: pre_dispatch
transforms, `@doc` tag sync, combined contract + facade,
compile-time spec mismatch warnings).
- `DoubleDown.Facade.Codegen` — extracted shared code generation
(`generate_facade`, `generate_key_helper`, dispatch option
resolution, static impl resolution, moduledoc generation) from
`DoubleDown.Facade` into a shared module. Used by both `Facade`
and `BehaviourFacade`. Pure refactor — no behaviour change.
- `DoubleDown.Facade.BehaviourIntrospection` — reads `@callback`
declarations from compiled vanilla behaviour modules and converts
them to the operation map format used by `Facade.Codegen`. Handles
annotated params (`id :: String.t()`), bare types (`map()`), type
variables from `when` clauses, zero-arg callbacks, and mixed
param styles.
- `when` clause support in generated `@spec` declarations. Specs
with bounded type variables (e.g.
`@callback transform(input) :: output when input: term(), output: term()`)
now preserve the `when` constraints in the facade's `@spec`.
- `DoubleDown.BehaviourFacade` and `DoubleDown.Dynamic` added to
`groups_for_modules` in ex_doc config.
- Updated documentation across README, getting-started.md,
dynamic.md, and all facade module `@moduledoc`s with the
three-facade taxonomy and comparison tables.
## [0.40.0]
### Added
- `DoubleDown.Double.dynamic/1` — convenience for setting up a
dynamically-faked module with its original implementation as the
fallback. Pipes naturally with expects and stubs:
SomeClient
|> Double.dynamic()
|> Double.expect(:fetch, fn [_] -> {:error, :timeout} end)
Raises if the module hasn't been set up with `Dynamic.setup/1`.
## [0.39.0]
### Added
- `DoubleDown.Dynamic` — Mimic-style dynamic dispatch facades.
`Dynamic.setup(Module)` copies a module's bytecode to a backup
and replaces it with a dispatch shim, enabling the full Double
API (expects, stubs, fakes, stateful responders, passthrough,
cross-contract state access) without defining a contract or
facade. Call in `test_helper.exs` before `ExUnit.start()`.
Tests that don't install a handler get the original module's
behaviour automatically. Async-safe.
- Guardrails: `Dynamic.setup/1` refuses DoubleDown contracts,
DoubleDown internals, NimbleOwnership, and Erlang/OTP modules.
- `Dynamic.setup?/1` — check if a module has been set up.
- `Dynamic.original_module/1` — get the backup module name.
- `docs/dynamic.md` — full documentation with setup, usage,
comparison table, and migration path from dynamic to
contract-based facades.
## [0.38.0]
### Added
- Static dispatch facades now generate direct function calls
(`Module.function(args)`) with `@compile {:inline, ...}` instead
of `apply(Module, :function, [args])`. This allows the BEAM to
inline facade functions at call sites — zero dispatch overhead,
zero extra stack frames. Falls back to `apply` for operations
with `pre_dispatch:` transforms where args are computed at runtime.
### Changed
- **Breaking:** Removed auto-bang variant generation from
`defcallback`. The `:bang` option is no longer supported. Previously
`defcallback insert(...) :: {:ok, T} | {:error, E}` would
auto-generate `insert!/1`. This added complexity (bang_mode,
extract_success_type, has_ok_error_pattern?) for limited value —
Ecto already provides its own bang functions, and the generic
wrappers produced unhelpful error messages.
- `bang: false` is no longer needed on `get!`, `get_by!`, `one!`,
`transact`, and `rollback` declarations — they are now regular
`defcallback` operations with no special treatment.
- `bang_mode` removed from `__callbacks__/0` introspection maps.
### Fixed
- Module fakes (`Double.fake(contract, Module)`) now run via
`%Defer{}` in the calling process instead of inside the
NimbleOwnership GenServer. Fixes `DBConnection.OwnershipError`
when using `Double.fake(Repo, Backend.Repo)` for integration
tests with real Ecto implementations.
## [0.37.2]
### Fixed
- Module fakes (`Double.fake(contract, Module)`) now run in the
calling process instead of the NimbleOwnership GenServer process.
Previously `invoke_module_fallback` called `apply(module, op, args)`
directly inside `get_and_update`, which meant real implementations
doing I/O (e.g. Ecto queries) ran in the GenServer — a process
with no Ecto sandbox checkout. Now uses `%Defer{}` to move the
`apply` outside the lock, matching how `transact` already works.
This fixes `DBConnection.OwnershipError` when using
`Double.fake(Repo, Backend.Repo)` in integration tests.
## [0.37.1]
### Added
- `DoubleDown.Double.allow/2,3` — convenience delegate to
`DoubleDown.Testing.allow/2,3` for discoverability when using the
Double API exclusively.
- Documented `:warn_on_typespec_mismatch?` option in `defcallback`
`@doc`.
### Fixed
- Moved `DoubleDown.Defer` to `DoubleDown.Dispatch.Defer` — it's an
internal dispatch mechanism, not user-facing.
- `invoke_stateful_fallback` now validates return tuple shape
consistently with `invoke_expect` and `invoke_stub`. A stateful
fake returning a bare value instead of `{result, new_state}` now
raises a descriptive `ArgumentError` instead of a raw `MatchError`.
- `stub_handler?/1` and `fake_handler?/1` now check `@behaviour`
declarations instead of duck-typing function exports. Previously
any module with `new/2` would match `stub_handler?`.
- `Testing.allow/2` now has an explicit `@spec` (was missing for the
2-arity form generated by the default argument).
- `verify!` error message no longer incorrectly says `verify!/0`
when called via `verify!/1`.
## [0.37.0]
### Added
- `DoubleDown.Dispatch.StubHandler` behaviour for stateless stub
handler modules. Implement `new/2` to make a stub usable by module
name in `Double.stub`:
Double.stub(Repo, Repo.Test)
Double.stub(Repo, Repo.Test, fn :all, [User] -> [] end)
- `Repo.Test` implements `StubHandler`. `new/2` accepts a fallback
function as the first arg and opts as the second. The legacy
`new(fallback_fn: fn ...)` keyword form is still supported.
- `Double.stub/2` auto-detects StubHandler modules. `Double.stub/3`
disambiguates StubHandler modules from per-operation stubs by
checking if the second arg is a loaded module implementing the
behaviour.
## [0.36.0]
### Added
- `DoubleDown.Dispatch.FakeHandler` behaviour for stateful fake
handler modules. Implement `new/2` and `dispatch/3` (or `/4`) to
make a fake usable by module name in `Double.fake`:
Double.fake(Repo, Repo.InMemory)
Double.fake(Repo, Repo.InMemory, [%User{id: 1}])
Double.fake(Repo, Repo.InMemory, [%User{id: 1}], fallback_fn: fn ...)
- `Repo.InMemory` implements `FakeHandler`. `new/2` accepts seed
data as the first arg (list of structs or pre-built store map)
and opts as the second. The legacy `new(seed: [...], fallback_fn: ...)`
keyword form is still supported.
- `Double.fake/2` auto-detects FakeHandler modules — if the module
implements the behaviour, it's used as a stateful fake with default
state. Non-FakeHandler modules are still treated as module fakes.
## [0.35.0]
### Added
- Stateful per-operation stubs. `DoubleDown.Double.stub/3` now accepts
2-arity and 3-arity responder functions that can read and update the
fake's state, with the same semantics as stateful expect responders.
All arities can return `Double.passthrough()` to conditionally
delegate to the fallback/fake. This enables the pattern "intercept
every call, decide per-call whether to handle or delegate, without
knowing the call count."
## [0.34.0]
### Added
- `DoubleDown.Double.passthrough/0` — returns a sentinel value that
expect responders can return to conditionally delegate to the
fallback/fake. The expect is still consumed for `verify!` counting.
This enables patterns like "fail if duplicate, otherwise let the
fake handle it" without duplicating the fake's logic. Works with
all responder arities (1, 2, 3).
## [0.33.0]
### Added
- Stateful expect responders. `DoubleDown.Double.expect` now accepts
2-arity and 3-arity responder functions that can read and update the
stateful fake's state:
- **2-arity:** `fn [args], state -> {result, new_state} end`
- **3-arity:** `fn [args], state, all_states -> {result, new_state} end`
(cross-contract state access)
Stateful responders require `Double.fake/3` to be called first.
`ArgumentError` is raised at `expect` time if no stateful fake is
configured, and at dispatch time if the responder doesn't return a
`{result, new_state}` tuple. 1-arity expects are unchanged.
### Fixed
- Removed stale "Limitation: no inline passthrough" notes from
`DoubleDown.Double` moduledoc and `docs/testing.md` — this
limitation no longer exists with stateful expect responders.
- Fixed historical `.Port.` module name references in docs.
- Removed stale `Skuld` reference in contract.ex comment.
## [0.32.0]
### Added
- 4-arity stateful handlers with read-only cross-contract state
access. Handlers registered with
`fn operation, args, state, all_states -> {result, new_state} end`
receive a snapshot of all contract states as the 4th argument. This
enables the "two-contract" pattern where a Queries handler reads
the Repo InMemory store. Works with both `DoubleDown.Double.fake/3`
and `DoubleDown.Testing.set_stateful_handler/3`. Existing 3-arity
handlers are unchanged (non-breaking).
- `DoubleDown.Contract.GlobalState` sentinel key in the global state
map. If a handler accidentally returns the global map instead of
its own state, a clear `ArgumentError` is raised.
### Fixed
- Exceptions inside stateful handlers no longer crash the
NimbleOwnership GenServer. Raises, throws, and exits that occur
inside `NimbleOwnership.get_and_update` are now caught and
transported to the calling process via `%Defer{}`, where they
re-raise safely. Previously these would crash the ownership
server — a singleton for the entire test run — aborting the suite.
## [0.31.1]
### Fixed
- Exceptions inside stateful handlers no longer crash the
NimbleOwnership GenServer. Raises, throws, and exits that occur
inside `NimbleOwnership.get_and_update` (e.g. a module fallback
hitting a dead Ecto sandbox connection during test teardown) are
now caught and transported to the calling process via `%Defer{}`,
where they re-raise safely. Previously these would crash the
ownership server — a singleton for the entire test run — aborting
the suite.
## [0.31.0]
### Added
- Compile-time spec mismatch detection between `defcallback` type specs
and the production implementation's `@spec` declarations. When a
facade is compiled with a known static impl, param types and return
types are compared and a `CompileError` is raised on mismatch. This
catches the class of bug where a `defcallback` declares a narrower
type than the impl accepts (e.g. `keyword()` vs `list()`), which
would otherwise only surface as a non-local Dialyzer error.
- `warn_on_typespec_mismatch?: true` option on `defcallback` to
downgrade the compile error to a warning for individual operations
during migration.
- `DoubleDown.Contract.SpecWarnings` — private module handling spec
fetching, type AST normalization, and comparison.
## [0.30.1]
### Fixed
- `transact` return type spec now includes the `Ecto.Multi` 4-tuple
error shape: `{:error, term(), term(), term()}`. Previously the
spec only declared `{:ok, term()} | {:error, term()}`, causing
Dialyzer to conclude that code handling Multi's
`{:error, failed_op, failed_value, changes_so_far}` return was
unreachable.
## [0.30.0]
### Added
- Opts-accepting variants for all `DoubleDown.Repo` contract operations.
Every operation now has both a base arity and an `+ opts` arity
(e.g. `insert/1` and `insert/2`, `get/2` and `get/3`), matching
`Ecto.Repo`'s actual API where every function accepts an optional
`opts` keyword list. This fixes `UndefinedFunctionError` when
`Ecto.Multi.update/4` (and `insert/4`, `delete/4`) receive a
function argument — Multi's internal `:run` callbacks call
`repo.update(changeset, opts)` with 2 args, which previously had
no matching facade function.
- `Repo.Test` and `Repo.InMemory` handle opts-accepting dispatches
by stripping opts and delegating to base-arity logic.
## [0.29.0]
### Added
- `get_by`/`get_by!` in `Repo.InMemory` now use 3-stage dispatch
(state → fallback → error) when the queryable is a bare schema
module and the clauses include all primary key fields. PK lookup
uses the existing store index — no scan required. If found, any
additional non-PK clauses are verified against the record. If not
found in state, falls through to the fallback function (absence is
not authoritative). Non-PK clauses, `Ecto.Query` queryables, and
partial composite PKs still delegate to the fallback as before.
- Composite PK support in `get_by`/`get_by!` — all PK fields must
be present in the clauses for a direct state lookup.
## [0.28.1]
### Changed
- `defcallback` macro `@doc` now includes full rationale for why
`defcallback` is used instead of plain `@callback` (parameter names,
combined contract+facade, LSP docs, additional metadata).
- `repo.md`: rollback section, operation dispatch table updated,
`{:defer, fn}` references updated to `%DoubleDown.Defer{}`.
- `DoubleDown.Contract` `@moduledoc`: "typed port contracts" →
"contract behaviours".
## [0.28.0]
### Added
- `rollback/1` added to `DoubleDown.Repo` contract (now 17 operations).
Throws `{:rollback, value}` via `%Defer{}`, caught by `transact`
which returns `{:error, value}`. Matches `Ecto.Repo.rollback/1` API.
Both `Repo.Test` and `Repo.InMemory` support rollback — state
mutations from earlier operations are not undone (documented
limitation).
- Nested transact tests for both `Repo.Test` and `Repo.InMemory`,
including via `Double.stub` and `Double.fake`.
## [0.27.0]
### Added
- `%DoubleDown.Defer{fn: fun}` struct — dedicated deferred execution
marker, replacing the `{:defer, fn}` tuple convention. Eliminates
clash risk with legitimate return values and enables deferred
execution in all dispatch paths (fn, module, stateful).
- `Repo.Test` now returns `%Defer{}` for `transact` operations, so
`Double.stub(contract, Repo.Test.new())` works correctly with
transact — no NimbleOwnership deadlock.
- Regression tests for transact-via-`Double.stub` scenario.
### Changed
- **Breaking:** `{:defer, fn}` tuple convention replaced by
`%DoubleDown.Defer{fn: fun}` throughout. Affects `Repo.Test`,
`Repo.InMemory`, `DoubleDown.Dispatch`, and `DoubleDown.Double`.
Only relevant if you were returning `{:defer, fn}` from custom
stateful handlers — replace with `%DoubleDown.Defer{fn: fun}`.
### Fixed
- NimbleOwnership deadlock when using `Double.stub(contract,
Repo.Test.new())` with contracts that include re-entrant operations
like `transact`.
- Async test race condition: added `Code.ensure_loaded` before
`function_exported?` in contract tests.
- Documentation: "contract behaviour" and "dispatch facade" compound
forms at first-mention points, intro paragraphs on all doc pages,
production Repo as zero-cost passthrough.
## [0.26.0]
### Changed
- **Breaking:** `DoubleDown.Handler` renamed to `DoubleDown.Double`.
`stub` for module and stateful fallbacks split out into `fake`:
- `Double.stub(contract, :op, fun)` — per-operation stub (canned value)
- `Double.stub(contract, fun)` — 2-arity function fallback
- `Double.fake(contract, module)` — module fake
- `Double.fake(contract, fun, init_state)` — stateful fake
- **Breaking:** `DoubleDown.Log` API simplified — `match` and `reject`
no longer take a contract parameter. The contract is specified once
at `verify!` time. `verify!` now returns `{:ok, log}` on success.
- Handler error messages now include the contract name and args.
- `.formatter.exs` updated for `defcallback` rename.
### Added
- `DoubleDown.Log.verify!` returns `{:ok, log}` on success and
includes the full dispatch log in all error messages — useful for
REPL debugging.
- `:static_dispatch?` option on `use DoubleDown.Facade` — resolves
the implementation module at compile time and generates direct
function calls, eliminating `Application.get_env` overhead entirely.
Defaults to `fn -> Mix.env() == :prod end`.
- Comprehensive docs review: restructured testing.md with `Double` as
primary API, updated all examples to use `Double.expect`/`stub`/`fake`
instead of raw `set_*_handler` APIs, consistent terminology throughout.
## [0.25.0]
### Changed
- **Breaking:** `defport` renamed to `defcallback`, `__port_operations__/0`
renamed to `__callbacks__/0`. The `defcallback` macro uses the same
syntax as `@callback` — replace the keyword and you're done.
- **Breaking:** `DoubleDown.Repo.Contract` renamed to `DoubleDown.Repo`.
Less verbose in `Handler.stub` and `Handler.expect` calls.
- **Breaking:** `DoubleDown.Log` API simplified — `match` and `reject`
no longer take a contract parameter. The contract is specified once
at `verify!` time: `Log.match(:op, fn _ -> true end) |> Log.verify!(MyContract)`.
### Added
- `:static_dispatch?` option on `use DoubleDown.Facade` — resolves
the implementation module at compile time and generates direct
function calls, eliminating `Application.get_env` overhead entirely.
Defaults to `fn -> Mix.env() == :prod end`. Falls back to runtime
config dispatch when compile-time config is unavailable.
- README rewritten with new "Why DoubleDown?" section, Mox comparison,
failure scenario example, and implementation snippet.
- Comprehensive docs review: "port" → "contract" throughout,
terminology updated, fail-fast pattern documented, Skuld references
simplified, LSP docs bullet added to `defcallback` rationale.
## [0.24.0]
### Changed
- **Breaking:** Library renamed from `hex_port` / `HexPort` to
`double_down` / `DoubleDown`. All module names, app name, package
name, and GitHub URLs updated. The emphasis has shifted from
hexagonal architecture boundaries to the distinctive test double
capabilities.
## [0.23.0]
### Changed
- **Breaking:** `DoubleDown.Double` API simplified — `expect` and `stub`
now write directly to NimbleOwnership with immediate effect. Removed
`%DoubleDown.Double{}` struct, `new/0`, and `install!/1`. All functions
return the contract module atom for Mimic-style piping:
MyContract
|> DoubleDown.Double.stub(MyImpl)
|> DoubleDown.Double.expect(:get, fn [id] -> %Thing{id: id} end)
A canonical handler function is installed on first touch and reads
dispatch config from state — no builder assembly step needed.
## [0.22.0]
### Added
- `DoubleDown.Double.expect/4..5` now accepts `:passthrough` as the
handler argument. A `:passthrough` expect delegates to the
configured fallback (fn, stateful, or module) while consuming the
expect for `verify!` counting. Supports `times: n`. Enables
call-counting without changing behaviour, and can be mixed with
function expects for patterns like "first insert succeeds through
InMemory, second fails".
- Documentation in `docs/repo.md` for using `DoubleDown.Double` with
`Repo.Test` and `Repo.InMemory` for failure scenario testing,
including error simulation, `:passthrough` call counting, and
combined Handler + Log assertions.
### Fixed
- Added `@spec` clauses for all `stub/2..4` forms to satisfy
Dialyzer.
## [0.21.0]
### Added
- `DoubleDown.Double.stub/3` (with accumulator: `stub/4`) for module
fallback — delegates unhandled operations to a module implementing
the contract's `@behaviour`. Validated at `install!` time.
- `DoubleDown.Double.stub/3` (with accumulator: `stub/4`) for stateful
fallback — accepts a 3-arity `fn operation, args, state ->
{result, new_state} end` with initial state, same signature as
`set_stateful_handler`. Integrates stateful fakes (e.g.
`Repo.InMemory`) into the Handler dispatch chain. Expects that
short-circuit (e.g. error simulation) leave the fallback state
unchanged.
- Fallback types are now a tagged union (`{:fn, fun}`,
`{:stateful, fun, init_state}`, `{:module, module}`) — mutually
exclusive, setting one replaces the other.
## [0.20.0]
### Added
- `DoubleDown.Double.verify_on_exit!/0` — registers an `on_exit`
callback that automatically verifies all expectations after each
test. Usable as `setup :verify_on_exit!`. Uses
`NimbleOwnership.set_owner_to_manual_cleanup/2` to preserve
ownership data until the on_exit callback runs.
- `DoubleDown.Double.verify!/1` — verifies expectations for a
specific process pid, used internally by `verify_on_exit!/0`.
### Fixed
- Added `:ex_unit` to `plt_add_apps` in `mix.exs` so Dialyzer can
resolve the `ExUnit.Callbacks.on_exit/2` call in
`DoubleDown.Double.verify_on_exit!/0`.
## [0.19.0]
### Added
- `DoubleDown.Double.stub/2` and `stub/3` (with accumulator) for
2-arity contract-wide fallback stubs. Accepts
`fn operation, args -> result end` — the same signature as
`set_fn_handler` — as a catch-all for operations without a
specific expect or per-operation stub. Dispatch priority:
expects > per-operation stubs > fallback stub > raise.
## [0.18.0]
### Added
- `DoubleDown.Double` — Mox-style expect/stub handler builder. Builds
stateful handler functions from a declarative specification with
multi-contract chaining and ordered expectations. API:
`expect/3..5`, `stub/3..4`, `install!/1`, `verify!/0`.
- `DoubleDown.Log` — log-based expectation matcher. Declares structured
expectations against the dispatch log after execution, matching on
the full `{contract, operation, args, result}` tuple. Supports
loose (default) and strict matching modes, `times: n` counting,
and `reject` expectations. API: `match/3..5`, `reject/2..3`,
`verify!/1..2`.
- Terminology mapping and glossary in README and getting-started
guide, mapping DoubleDown concepts (contract, facade, test double,
port) to familiar Elixir/Mox equivalents with a stub/mock/fake
breakdown.
## [0.17.0]
### Changed
- **Breaking:** Renamed generated key helper from `key/N` to `__key__/N`
on facade modules, following the Elixir convention for generated
introspection functions. This avoids clashes with user-defined
`defcallback key(...)` operations.
### Fixed
- Added `:mix` to `plt_add_apps` in `mix.exs` so Dialyzer can resolve
the compile-time `Mix.env/0` call in `DoubleDown.Facade.__using__/1`.
## [0.16.1]
### Fixed
- Added `:mix` to `plt_add_apps` in `mix.exs` so Dialyzer can resolve
the compile-time `Mix.env/0` call in `DoubleDown.Facade.__using__/1`.
### Changed
- Documentation updates for `:test_dispatch?` in `docs/getting-started.md`
(dispatch resolution section) and `docs/testing.md` (setup section).
## [0.16.0]
### Added
- `:test_dispatch?` option for `use DoubleDown.Facade` — controls whether
the generated facade includes the `NimbleOwnership`-based test handler
resolution step. Accepts `true`, `false`, or a zero-arity function
returning a boolean, evaluated at compile time. Defaults to
`fn -> Mix.env() != :prod end`, so production builds get a config-only
dispatch path with zero `NimbleOwnership` overhead (no
`GenServer.whereis` ETS lookup).
- `DoubleDown.Dispatch.call_config/4` — config-only dispatch function that
skips test handler resolution entirely. Used by facades compiled with
`test_dispatch?: false`.
## [0.15.0]
### Added
- `pre_dispatch` option for `defcallback` — a generic mechanism for
transforming arguments before dispatch. Accepts a function
`(args, facade_module) -> args` declared at the contract level,
spliced into the generated facade function as AST.
- `Repo.Test` tests split into dedicated `test/double_down/repo/test_test.exs`
module.
### Changed
- 1-arity `transact` functions are now wrapped into 0-arity thunks
at the facade boundary via `pre_dispatch`. The thunk closes over
the facade module, so calls inside the function (e.g.
`repo.insert(cs)`) go through the facade dispatch chain. This
ensures facade-level concerns (logging, telemetry) apply in both
test and production.
- `Repo.Test` and `Repo.InMemory` adapters no longer handle 1-arity
transaction functions — they always receive 0-arity thunks (from
`pre_dispatch` wrapping) or `Ecto.Multi` structs.
- The hardcoded `:transact` special-case in `DoubleDown.Facade` has been
removed. The Repo-specific facade injection is now declared on the
`defcallback` in `DoubleDown.Repo` using the generic
`pre_dispatch` mechanism.
### Fixed
- User-supplied fallback functions in `Repo.InMemory` that raise
non-`FunctionClauseError` exceptions (e.g. `RuntimeError`,
`ArgumentError`) no longer crash the NimbleOwnership GenServer.
Exceptions are captured and re-raised in the calling test process
via `{:defer, fn -> reraise ... end}`.
## [0.14.0]
### Added
- `DoubleDown.Repo.insert_all/3` — standalone bulk insert
operation, dispatched via fallback in both test adapters.
- `DoubleDown.Testing.set_mode_to_global/0` and `set_mode_to_private/0`
— global handler mode for testing through supervision trees,
Broadway pipelines, and other process trees where individual pids
are not accessible. Uses NimbleOwnership shared mode. Incompatible
with `async: true`.
- `DoubleDown.Repo.Autogenerate` — shared helper module for
autogenerating primary keys and timestamps in test adapters.
Handles `:id` (integer auto-increment), `:binary_id` (UUID),
parameterized types (`Ecto.UUID`, `Uniq.UUID`, etc.), and
`@primary_key false` schemas.
- `docs/migration.md` — incremental adoption guide covering the
two-contract pattern, coexisting with direct Ecto.Repo calls, and
the fail-fast test config pattern.
- Process-testing patterns in `docs/testing.md` — decision table,
GenServer example, supervision tree example.
### Changed
- Test adapters (`Repo.Test`, `Repo.InMemory`) now check
`changeset.valid?` before applying changes — invalid changesets
return `{:error, changeset}`, matching real Ecto.Repo behaviour.
- Test adapters now populate `inserted_at`/`updated_at` timestamps
via Ecto's `__schema__(:autogenerate)` metadata. Custom field
names and timestamp types are handled automatically.
- 1-arity `transact` functions now receive the facade module instead
of `nil`, enabling `fn repo -> repo.insert(cs) end` patterns.
- The internal opts key for threading the facade module through
transact was renamed from `:repo_facade` to `DoubleDown.Repo.Facade`
for proper namespacing.
- Primary key autogeneration is now metadata-driven — supports
`:binary_id` (UUID), `Ecto.UUID`, and other parameterized types.
Raises `ArgumentError` when autogeneration is not configured and
no PK value is provided.
- Autogeneration logic extracted from `Repo.Test` and
`Repo.InMemory` into shared `DoubleDown.Repo.Autogenerate` module.
- Repo contract now has 16 operations (was 15).
### Fixed
- Invalid changesets passed to `Repo.Test` or `Repo.InMemory`
`insert`/`update` no longer silently succeed — they return
`{:error, changeset}`.
- `Repo.InMemory` store is unchanged after a failed insert/update
with an invalid changeset.
## [0.13.0]
### Added
- Fail-fast documentation for `impl: nil` test configuration.
### Changed
- Improved error messages when no implementation is configured in
test mode.
## [0.12.0]
### Changed
- Removed unused Ecto wrapper macro.
- Version now read from `VERSION` file.
## [0.11.1]
### Changed
- Documentation improvements (README, hexdocs, testing guide).
- Removed unnecessary `reset` calls from test examples.
## [0.11.0]
### Fixed
- Fixed compiler warnings.
## [0.10.0]
### Added
- `Facade` without implicit `Contract` — `use DoubleDown.Facade` with
an explicit `:contract` option for separate contract modules.
- Documentation explaining why `defcallback` is used instead of standard
`@callback` declarations.
## [0.9.0]
### Added
- Single-module `Contract + Facade` — `use DoubleDown.Facade` without
a `:contract` option implicitly sets up the contract in the same
module.
### Changed
- Dispatch references the contract module, not the facade.
## [0.8.0]
### Added
- `DoubleDown.Repo` — built-in 15-operation Ecto Repo
contract with `Repo.Test` (stateless) and `Repo.InMemory`
(stateful) test doubles.
- `MultiStepper` for stepping through `Ecto.Multi` operations
without a database.
### Changed
- Renamed `Port` to `Facade` throughout.
- Removed separate `.Behaviour` module — behaviours are generated
directly on the contract module.
## [0.7.0]
### Changed
- `Repo.InMemory` fallback function now receives state as a third
argument `(operation, args, state)`, enabling fallbacks that
compose canned data with records inserted during the test.
## [0.6.0]
### Fixed
- Made `DoubleDown.Contract.__using__/1` idempotent — safe to `use`
multiple times.
## [0.5.0]
### Changed
- Improved `Repo.Test` stateless handler.
## [0.4.0]
### Added
- `Repo.InMemory` — stateful in-memory Repo implementation with
read-after-write consistency for PK-based lookups.
- NimbleOwnership-based process-scoped handler isolation for
`async: true` tests.
## [0.3.1]
### Fixed
- Expand type aliases at macro time in `defcallback` to resolve
Dialyzer `unknown_type` errors.
## [0.3.0]
### Added
- `transact` defcallback with `{:defer, fn}` support for stateful
dispatch — avoids NimbleOwnership deadlocks.
- `Repo.transact!` for `Ecto.Multi` operations.
## [0.2.0]
### Changed
- Split `DoubleDown` into `DoubleDown.Contract` and `DoubleDown.Port`
(later renamed to `Facade`).
## [0.1.0]
### Added
- Initial release — `defcallback` macro, `DoubleDown.Contract`,
`DoubleDown.Testing` with NimbleOwnership, `Repo.Test` stateless
adapter, CI setup, Credo, Dialyzer.
[Unreleased]: https://github.com/mccraigmccraig/double_down/compare/v0.46.0...HEAD
[0.46.0]: https://github.com/mccraigmccraig/double_down/compare/v0.45.0...v0.46.0
[0.45.0]: https://github.com/mccraigmccraig/double_down/compare/v0.44.0...v0.45.0
[0.44.0]: https://github.com/mccraigmccraig/double_down/compare/v0.43.0...v0.44.0
[0.43.0]: https://github.com/mccraigmccraig/double_down/compare/v0.42.0...v0.43.0
[0.42.0]: https://github.com/mccraigmccraig/double_down/compare/v0.41.1...v0.42.0
[0.41.1]: https://github.com/mccraigmccraig/double_down/compare/v0.41.0...v0.41.1
[0.41.0]: https://github.com/mccraigmccraig/double_down/compare/v0.40.0...v0.41.0
[0.40.0]: https://github.com/mccraigmccraig/double_down/compare/v0.39.0...v0.40.0
[0.39.0]: https://github.com/mccraigmccraig/double_down/compare/v0.38.0...v0.39.0
[0.38.0]: https://github.com/mccraigmccraig/double_down/compare/v0.37.2...v0.38.0
[0.37.2]: https://github.com/mccraigmccraig/double_down/compare/v0.37.1...v0.37.2
[0.37.1]: https://github.com/mccraigmccraig/double_down/compare/v0.37.0...v0.37.1
[0.37.0]: https://github.com/mccraigmccraig/double_down/compare/v0.36.0...v0.37.0
[0.36.0]: https://github.com/mccraigmccraig/double_down/compare/v0.35.0...v0.36.0
[0.35.0]: https://github.com/mccraigmccraig/double_down/compare/v0.34.0...v0.35.0
[0.34.0]: https://github.com/mccraigmccraig/double_down/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/mccraigmccraig/double_down/compare/v0.32.0...v0.33.0
[0.32.0]: https://github.com/mccraigmccraig/double_down/compare/v0.31.1...v0.32.0
[0.31.1]: https://github.com/mccraigmccraig/double_down/compare/v0.31.0...v0.31.1
[0.31.0]: https://github.com/mccraigmccraig/double_down/compare/v0.30.1...v0.31.0
[0.30.1]: https://github.com/mccraigmccraig/double_down/compare/v0.30.0...v0.30.1
[0.30.0]: https://github.com/mccraigmccraig/double_down/compare/v0.29.0...v0.30.0
[0.29.0]: https://github.com/mccraigmccraig/double_down/compare/v0.28.1...v0.29.0
[0.28.1]: https://github.com/mccraigmccraig/double_down/compare/v0.28.0...v0.28.1
[0.28.0]: https://github.com/mccraigmccraig/double_down/compare/v0.27.0...v0.28.0
[0.27.0]: https://github.com/mccraigmccraig/double_down/compare/v0.26.0...v0.27.0
[0.26.0]: https://github.com/mccraigmccraig/double_down/compare/v0.25.0...v0.26.0
[0.25.0]: https://github.com/mccraigmccraig/double_down/compare/v0.24.0...v0.25.0
[0.24.0]: https://github.com/mccraigmccraig/double_down/compare/v0.23.0...v0.24.0
[0.23.0]: https://github.com/mccraigmccraig/double_down/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/mccraigmccraig/double_down/compare/v0.21.0...v0.22.0
[0.21.0]: https://github.com/mccraigmccraig/double_down/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/mccraigmccraig/double_down/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/mccraigmccraig/double_down/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/mccraigmccraig/double_down/compare/v0.17.0...v0.18.0
[0.17.0]: https://github.com/mccraigmccraig/double_down/compare/v0.16.1...v0.17.0
[0.16.1]: https://github.com/mccraigmccraig/double_down/compare/v0.16.0...v0.16.1
[0.16.0]: https://github.com/mccraigmccraig/double_down/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/mccraigmccraig/double_down/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/mccraigmccraig/double_down/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/mccraigmccraig/double_down/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/mccraigmccraig/double_down/compare/v0.11.1...v0.12.0
[0.11.1]: https://github.com/mccraigmccraig/double_down/compare/v0.11.0...v0.11.1
[0.11.0]: https://github.com/mccraigmccraig/double_down/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/mccraigmccraig/double_down/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/mccraigmccraig/double_down/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/mccraigmccraig/double_down/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/mccraigmccraig/double_down/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/mccraigmccraig/double_down/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/mccraigmccraig/double_down/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/mccraigmccraig/double_down/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/mccraigmccraig/double_down/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/mccraigmccraig/double_down/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/mccraigmccraig/double_down/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/mccraigmccraig/double_down/releases/tag/v0.1.0