All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.47.2
Added
DoubleDown.Contract.Dispatch.handler_active?/1— public boolean API to check whether the calling process has a test handler installed for a given contract module. Returnstruewhen a handler is active (viaDouble.fake/2,expect/3, etc.),falseotherwise. Respects the$callerschain. Useful for test infrastructure that needs to skip real-DB side-effects (e.g. Carbonite session variables) when an in-memory handler is intercepting Repo calls.
0.47.1
Fixed
- Code formatting.
0.47.0
Fixed
Transaction rollback now covers all failure paths.
run_in_transactionrestores pre-transaction state on{:error, _}returns, raised exceptions (re-raised after restore), and failedEcto.Multituples — not just explicitRepo.rollback/1calls. Previously, error branches left partially-mutated fake state. (GitHub #1)restore_stateuses correct owner pid.restore_state/3now acceptsowner_pidas an explicit parameter, resolved viaresolve_test_handlerat rollback time. Transactions run inside aTasknow correctly restore state to the owning test process instead of silently no-oping. (GitHub #1)Ecto.Multibulk ops now execute instead of returning{0, nil}.MultiStepperroutesinsert_all,update_all, anddelete_allsteps throughrepo_facade— each fake's own dispatch handles state mutation. Previously these were hardcoded no-ops. (GitHub #2)insert_allraises for missing non-autogenerated PKs. Whenmaybe_autogenerate_idreturns an error for a schema without autogeneration,insert_allnow raisesArgumentError— matching single-rowinsertbehaviour. Previously the error was swallowed and rows collapsed onto a nil key. (GitHub #3)@primary_key falseschemas support multiple rows. No-PK schemas are now stored as lists (reverse insertion order) instead of a%{nil => record}map. All store accessors (put_record,get_record,delete_record,records_for_schema) and bulk ops (delete_all,update_all) handle the dual map/list representation. (GitHub #4)get_by!raisesEcto.NoResultsErrorwhen PK matches but extra clauses don't. Previously returnednilfor bothget_byandget_by!in this case, diverging from real Ecto. (GitHub #5)rollback/1outside a transaction raisesRuntimeError. Uses a process dictionary flag set byrun_in_transactionand cleared viatry/after. BothInMemorySharedandStubupdated. Previously an uncaughtthrow({:rollback, _})surfaced. (GitHub #6)
Changed
- Breaking: All handler function signatures gained
contractas the first parameter. This affectsFakeHandler.dispatch(/3→/4,/4→/5),set_stateful_handlerfns (3-arity → 4-arity, 4-arity → 5-arity),fallback_fn(3-arity → 4-arity), andrestore_state(/2→/3). The contract module now flows through the entire handler chain —invoke_handler,canonical_handler,dispatch_via_fallback,try_fallback— eliminating the hardcodedDoubleDown.Repoin transaction rollback. Enables handlers to know which contract they are serving.
0.46.3
Fixed
- FK backfill now recursively inserts parent structs when the
parent's PK is nil. This fixes the ExMachina pattern where
build(:parent)produces a struct with nil PK that hasn't been inserted yet — matching real Ecto.Repo behaviour of recursively insertingbelongs_toparents before the child.
0.46.2
Fixed
- Added test coverage for FK backfill with parent struct returned
from a prior insert (the exact ExMachina factory pattern). Confirms
backfill correctly uses
related_keyfrom association metadata, not a hardcoded:idfield.
0.46.1
Fixed
FK backfill now explicitly skips
%Ecto.Association.NotLoaded{}associations rather than relying onMap.getreturning nil. Defensive fix for a reported FK backfill failure via the ExMachinainsert!path.Added integration test for
insert!bare struct through theDouble.fakefacade dispatch path to verify FK backfill works end-to-end.
0.46.0
Added
query/1,2,3andquery!/1,2,3added toDoubleDown.Repocontract. Raw SQL operations from ecto_sql — adding them to the contract makes them interceptable via expects/stubs so code paths that callRepo.query!can be tested without a database.FK backfill on insert. When inserting a struct with a loaded
belongs_toassociation 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 inRepo.Impl.EctoParity.backfill_foreign_keys/1.Association fields are reset to
%Ecto.Association.NotLoaded{}on insert, matching real Ecto.Repo behaviour. Struct equality comparisons betweeninsert(:thing)andRepo.get!(Thing, id)now work without comparing individual fields. Implemented inRepo.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 raiseEcto.NoResultsError(wasArgumentError).one,one!,get_by!now raiseEcto.MultipleResultsErrorwhen multiple records match (wasArgumentError). Matches real Ecto.Repo behaviour so tests withassert_raise Ecto.NoResultsErrorwork without modification.
0.45.0
Added
In-memory transaction rollback support.
rollback/1in the stateful test adapters (Repo.InMemoryandRepo.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
transactstart and restoring viaContract.Dispatch.restore_state/2on 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. Returnsfallback_statefor Double-managed handlers, raw state forset_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,2added toDoubleDown.Repocontract and all three test doubles. These were lost when auto-bang generation was removed in v0.38.0. Needed for ExMachina integration (ExMachinacallsRepo.insert!).insert/insert!now accept bare structs in addition to changesets, matchingEcto.Repobehaviour.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.7added as a test-only dependency.ExMachina integration documentation in
docs/repo.mdwith worked example: factory definition, test setup, reads, aggregates, read-after-write, failure simulation. Cross-referenced fromdocs/getting-started.md.
Changed
- Breaking:
DoubleDown.Repo.Port(test facade) renamed toDoubleDown.Test.Repo. Natural alias givesRepo.*withoutas:clause.
0.43.0
Changed
Breaking:
DoubleDown.Facaderenamed toDoubleDown.ContractFacade. Symmetric<qualifier>Facadenaming across all three facade builders:ContractFacade,BehaviourFacade,DynamicFacade.Breaking:
DoubleDown.Dynamicrenamed toDoubleDown.DynamicFacade.Breaking:
DoubleDown.Dispatchrenamed toDoubleDown.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.Testrenamed toDoubleDown.Repo.Stub. The name now communicates what the module is — a stateless stub — matching the test-double taxonomy (stub/mock/fake).Breaking:
DoubleDown.Repo.InMemoryrenamed toDoubleDown.Repo.OpenInMemory(open-world, fallback-based).DoubleDown.Repo.ClosedInMemoryrenamed toDoubleDown.Repo.InMemory(closed-world, recommended default). The unqualifiedInMemoryname now refers to the closed-world store — the one most users should reach for, especially with ExMachina factories.Breaking:
DoubleDown.Repo.Autogeneraterenamed toDoubleDown.Repo.Impl.Autogenerate.DoubleDown.Repo.MultiStepperrenamed toDoubleDown.Repo.Impl.MultiStepper.DoubleDown.Repo.InMemory.Sharedrenamed toDoubleDown.Repo.Impl.InMemoryShared. Internal helpers moved toRepo.Impl.*namespace.DoubleDown.BehaviourFacade.CompileHelperrenamed toDoubleDown.Facade.CompileHelper. TheFacade.*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. UnlikeRepo.InMemory(open-world, where absence means "I don't know"),ClosedInMemorytreats 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!returnnil/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/maxcomputed from state - Bulk writes:
insert_all,delete_all,update_all(withset:updates)
Ecto.Queryqueryables 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")- PK reads:
DoubleDown.Repo.InMemory.Shared— extracted shared helpers (state access, writes, transactions, fallback dispatch, query helpers) fromRepo.InMemoryinto a shared module for reuse byClosedInMemory. Pure refactor ofRepo.InMemory— no behaviour change.Updated documentation across
docs/repo.md(ClosedInMemory section with comparison table and ExMachina example), README (features table), andmix.exs(module groups).
0.41.1
Fixed
BehaviourFacadecompilation failure on clean builds when the behaviour and facade are in the sameelixirc_pathsbatch.Code.Typespec.fetch_callbacks/1needs the behaviour's.beamfile on disk, but duringmix compileall files in the same batch are compiled together —.beamfiles aren't written until the batch finishes.
Added
DoubleDown.BehaviourFacade.CompileHelper.ensure_compiled!/1— explicitly compiles a behaviour source file and writes its.beamto the build directory. Only needed when the behaviour and facade are in the same compilation batch (e.g. both intest/support/). In normal usage the behaviour would be inlib/or a dependency, already compiled in a prior batch.BehaviourIntrospectionnow falls back to:code.get_object_code/1whenCode.Typespec.fetch_callbacks/1can't find the.beamon the standard code path.
0.41.0
Added
DoubleDown.BehaviourFacade— generates dispatch facades for vanilla Elixir@behaviourmodules. Reads@callbackdeclarations from compiled behaviour modules viaCode.Typespec.fetch_callbacks/1and generates the same dispatch facade,@specdeclarations, and__key__helpers asDoubleDown.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 endUse this for behaviours you don't control — third-party libraries, existing
@behaviourmodules, or any module with@callbackdeclarations that you don't want to convert todefcallback. For behaviours you do control,DoubleDown.Facadewithdefcallbackremains recommended (richer features: pre_dispatch transforms,@doctag 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) fromDoubleDown.Facadeinto a shared module. Used by bothFacadeandBehaviourFacade. Pure refactor — no behaviour change.DoubleDown.Facade.BehaviourIntrospection— reads@callbackdeclarations from compiled vanilla behaviour modules and converts them to the operation map format used byFacade.Codegen. Handles annotated params (id :: String.t()), bare types (map()), type variables fromwhenclauses, zero-arg callbacks, and mixed param styles.whenclause support in generated@specdeclarations. Specs with bounded type variables (e.g.@callback transform(input) :: output when input: term(), output: term()) now preserve thewhenconstraints in the facade's@spec.DoubleDown.BehaviourFacadeandDoubleDown.Dynamicadded togroups_for_modulesin ex_doc config.Updated documentation across README, getting-started.md, dynamic.md, and all facade module
@moduledocs 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 intest_helper.exsbeforeExUnit.start(). Tests that don't install a handler get the original module's behaviour automatically. Async-safe.- Guardrails:
Dynamic.setup/1refuses 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 ofapply(Module, :function, [args]). This allows the BEAM to inline facade functions at call sites — zero dispatch overhead, zero extra stack frames. Falls back toapplyfor operations withpre_dispatch:transforms where args are computed at runtime.
Changed
- Breaking: Removed auto-bang variant generation from
defcallback. The:bangoption is no longer supported. Previouslydefcallback insert(...) :: {:ok, T} | {:error, E}would auto-generateinsert!/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: falseis no longer needed onget!,get_by!,one!,transact, androllbackdeclarations — they are now regulardefcallbackoperations with no special treatment.bang_moderemoved from__callbacks__/0introspection maps.
Fixed
- Module fakes (
Double.fake(contract, Module)) now run via%Defer{}in the calling process instead of inside the NimbleOwnership GenServer. FixesDBConnection.OwnershipErrorwhen usingDouble.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. Previouslyinvoke_module_fallbackcalledapply(module, op, args)directly insideget_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 theapplyoutside the lock, matching howtransactalready works. This fixesDBConnection.OwnershipErrorwhen usingDouble.fake(Repo, Backend.Repo)in integration tests.
0.37.1
Added
DoubleDown.Double.allow/2,3— convenience delegate toDoubleDown.Testing.allow/2,3for discoverability when using the Double API exclusively.- Documented
:warn_on_typespec_mismatch?option indefcallback@doc.
Fixed
- Moved
DoubleDown.DefertoDoubleDown.Dispatch.Defer— it's an internal dispatch mechanism, not user-facing. invoke_stateful_fallbacknow validates return tuple shape consistently withinvoke_expectandinvoke_stub. A stateful fake returning a bare value instead of{result, new_state}now raises a descriptiveArgumentErrorinstead of a rawMatchError.stub_handler?/1andfake_handler?/1now check@behaviourdeclarations instead of duck-typing function exports. Previously any module withnew/2would matchstub_handler?.Testing.allow/2now has an explicit@spec(was missing for the 2-arity form generated by the default argument).verify!error message no longer incorrectly saysverify!/0when called viaverify!/1.
0.37.0
Added
DoubleDown.Dispatch.StubHandlerbehaviour for stateless stub handler modules. Implementnew/2to make a stub usable by module name inDouble.stub:Double.stub(Repo, Repo.Test) Double.stub(Repo, Repo.Test, fn :all, [User] -> [] end)Repo.TestimplementsStubHandler.new/2accepts a fallback function as the first arg and opts as the second. The legacynew(fallback_fn: fn ...)keyword form is still supported.Double.stub/2auto-detects StubHandler modules.Double.stub/3disambiguates 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.FakeHandlerbehaviour for stateful fake handler modules. Implementnew/2anddispatch/3(or/4) to make a fake usable by module name inDouble.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.InMemoryimplementsFakeHandler.new/2accepts seed data as the first arg (list of structs or pre-built store map) and opts as the second. The legacynew(seed: [...], fallback_fn: ...)keyword form is still supported.Double.fake/2auto-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/3now 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 returnDouble.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 forverify!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.expectnow 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/3to be called first.ArgumentErroris raised atexpecttime 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.- 2-arity:
Fixed
- Removed stale "Limitation: no inline passthrough" notes from
DoubleDown.Doublemoduledoc anddocs/testing.md— this limitation no longer exists with stateful expect responders. - Fixed historical
.Port.module name references in docs. - Removed stale
Skuldreference 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} endreceive 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 bothDoubleDown.Double.fake/3andDoubleDown.Testing.set_stateful_handler/3. Existing 3-arity handlers are unchanged (non-breaking). DoubleDown.Contract.GlobalStatesentinel key in the global state map. If a handler accidentally returns the global map instead of its own state, a clearArgumentErroris raised.
Fixed
- Exceptions inside stateful handlers no longer crash the
NimbleOwnership GenServer. Raises, throws, and exits that occur
inside
NimbleOwnership.get_and_updateare 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
defcallbacktype specs and the production implementation's@specdeclarations. When a facade is compiled with a known static impl, param types and return types are compared and aCompileErroris raised on mismatch. This catches the class of bug where adefcallbackdeclares a narrower type than the impl accepts (e.g.keyword()vslist()), which would otherwise only surface as a non-local Dialyzer error. warn_on_typespec_mismatch?: trueoption ondefcallbackto 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
transactreturn type spec now includes theEcto.Multi4-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.Repocontract operations. Every operation now has both a base arity and an+ optsarity (e.g.insert/1andinsert/2,get/2andget/3), matchingEcto.Repo's actual API where every function accepts an optionaloptskeyword list. This fixesUndefinedFunctionErrorwhenEcto.Multi.update/4(andinsert/4,delete/4) receive a function argument — Multi's internal:runcallbacks callrepo.update(changeset, opts)with 2 args, which previously had no matching facade function. Repo.TestandRepo.InMemoryhandle opts-accepting dispatches by stripping opts and delegating to base-arity logic.
0.29.0
Added
get_by/get_by!inRepo.InMemorynow 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.Queryqueryables, 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
defcallbackmacro@docnow includes full rationale for whydefcallbackis 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/1added toDoubleDown.Repocontract (now 17 operations). Throws{:rollback, value}via%Defer{}, caught bytransactwhich returns{:error, value}. MatchesEcto.Repo.rollback/1API. BothRepo.TestandRepo.InMemorysupport rollback — state mutations from earlier operations are not undone (documented limitation).- Nested transact tests for both
Repo.TestandRepo.InMemory, including viaDouble.stubandDouble.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.Testnow returns%Defer{}fortransactoperations, soDouble.stub(contract, Repo.Test.new())works correctly with transact — no NimbleOwnership deadlock.- Regression tests for transact-via-
Double.stubscenario.
Changed
- Breaking:
{:defer, fn}tuple convention replaced by%DoubleDown.Defer{fn: fun}throughout. AffectsRepo.Test,Repo.InMemory,DoubleDown.Dispatch, andDoubleDown.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 liketransact. - Async test race condition: added
Code.ensure_loadedbeforefunction_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.Handlerrenamed toDoubleDown.Double.stubfor module and stateful fallbacks split out intofake:Double.stub(contract, :op, fun)— per-operation stub (canned value)Double.stub(contract, fun)— 2-arity function fallbackDouble.fake(contract, module)— module fakeDouble.fake(contract, fun, init_state)— stateful fake
- Breaking:
DoubleDown.LogAPI simplified —matchandrejectno longer take a contract parameter. The contract is specified once atverify!time.verify!now returns{:ok, log}on success. - Handler error messages now include the contract name and args.
.formatter.exsupdated fordefcallbackrename.
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 onuse DoubleDown.Facade— resolves the implementation module at compile time and generates direct function calls, eliminatingApplication.get_envoverhead entirely. Defaults tofn -> Mix.env() == :prod end.- Comprehensive docs review: restructured testing.md with
Doubleas primary API, updated all examples to useDouble.expect/stub/fakeinstead of rawset_*_handlerAPIs, consistent terminology throughout.
0.25.0
Changed
- Breaking:
defportrenamed todefcallback,__port_operations__/0renamed to__callbacks__/0. Thedefcallbackmacro uses the same syntax as@callback— replace the keyword and you're done. - Breaking:
DoubleDown.Repo.Contractrenamed toDoubleDown.Repo. Less verbose inHandler.stubandHandler.expectcalls. - Breaking:
DoubleDown.LogAPI simplified —matchandrejectno longer take a contract parameter. The contract is specified once atverify!time:Log.match(:op, fn _ -> true end) |> Log.verify!(MyContract).
Added
:static_dispatch?option onuse DoubleDown.Facade— resolves the implementation module at compile time and generates direct function calls, eliminatingApplication.get_envoverhead entirely. Defaults tofn -> 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
defcallbackrationale.
0.24.0
Changed
- Breaking: Library renamed from
hex_port/HexPorttodouble_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.DoubleAPI simplified —expectandstubnow write directly to NimbleOwnership with immediate effect. Removed%DoubleDown.Double{}struct,new/0, andinstall!/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..5now accepts:passthroughas the handler argument. A:passthroughexpect delegates to the configured fallback (fn, stateful, or module) while consuming the expect forverify!counting. Supportstimes: 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.mdfor usingDoubleDown.DoublewithRepo.TestandRepo.InMemoryfor failure scenario testing, including error simulation,:passthroughcall counting, and combined Handler + Log assertions.
Fixed
- Added
@specclauses for allstub/2..4forms 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 atinstall!time.DoubleDown.Double.stub/3(with accumulator:stub/4) for stateful fallback — accepts a 3-arityfn operation, args, state -> {result, new_state} endwith initial state, same signature asset_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 anon_exitcallback that automatically verifies all expectations after each test. Usable assetup :verify_on_exit!. UsesNimbleOwnership.set_owner_to_manual_cleanup/2to preserve ownership data until the on_exit callback runs.DoubleDown.Double.verify!/1— verifies expectations for a specific process pid, used internally byverify_on_exit!/0.
Fixed
- Added
:ex_unittoplt_add_appsinmix.exsso Dialyzer can resolve theExUnit.Callbacks.on_exit/2call inDoubleDown.Double.verify_on_exit!/0.
0.19.0
Added
DoubleDown.Double.stub/2andstub/3(with accumulator) for 2-arity contract-wide fallback stubs. Acceptsfn operation, args -> result end— the same signature asset_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: ncounting, andrejectexpectations. 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/Nto__key__/Non facade modules, following the Elixir convention for generated introspection functions. This avoids clashes with user-defineddefcallback key(...)operations.
Fixed
- Added
:mixtoplt_add_appsinmix.exsso Dialyzer can resolve the compile-timeMix.env/0call inDoubleDown.Facade.__using__/1.
0.16.1
Fixed
- Added
:mixtoplt_add_appsinmix.exsso Dialyzer can resolve the compile-timeMix.env/0call inDoubleDown.Facade.__using__/1.
Changed
- Documentation updates for
:test_dispatch?indocs/getting-started.md(dispatch resolution section) anddocs/testing.md(setup section).
0.16.0
Added
:test_dispatch?option foruse DoubleDown.Facade— controls whether the generated facade includes theNimbleOwnership-based test handler resolution step. Acceptstrue,false, or a zero-arity function returning a boolean, evaluated at compile time. Defaults tofn -> Mix.env() != :prod end, so production builds get a config-only dispatch path with zeroNimbleOwnershipoverhead (noGenServer.whereisETS lookup).DoubleDown.Dispatch.call_config/4— config-only dispatch function that skips test handler resolution entirely. Used by facades compiled withtest_dispatch?: false.
0.15.0
Added
pre_dispatchoption fordefcallback— a generic mechanism for transforming arguments before dispatch. Accepts a function(args, facade_module) -> argsdeclared at the contract level, spliced into the generated facade function as AST.Repo.Testtests split into dedicatedtest/double_down/repo/test_test.exsmodule.
Changed
- 1-arity
transactfunctions are now wrapped into 0-arity thunks at the facade boundary viapre_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.TestandRepo.InMemoryadapters no longer handle 1-arity transaction functions — they always receive 0-arity thunks (frompre_dispatchwrapping) orEcto.Multistructs.- The hardcoded
:transactspecial-case inDoubleDown.Facadehas been removed. The Repo-specific facade injection is now declared on thedefcallbackinDoubleDown.Repousing the genericpre_dispatchmechanism.
Fixed
- User-supplied fallback functions in
Repo.InMemorythat raise non-FunctionClauseErrorexceptions (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/0andset_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 withasync: 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 falseschemas.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 checkchangeset.valid?before applying changes — invalid changesets return{:error, changeset}, matching real Ecto.Repo behaviour. - Test adapters now populate
inserted_at/updated_attimestamps via Ecto's__schema__(:autogenerate)metadata. Custom field names and timestamp types are handled automatically. - 1-arity
transactfunctions now receive the facade module instead ofnil, enablingfn repo -> repo.insert(cs) endpatterns. - The internal opts key for threading the facade module through
transact was renamed from
:repo_facadetoDoubleDown.Repo.Facadefor proper namespacing. - Primary key autogeneration is now metadata-driven — supports
:binary_id(UUID),Ecto.UUID, and other parameterized types. RaisesArgumentErrorwhen autogeneration is not configured and no PK value is provided. - Autogeneration logic extracted from
Repo.TestandRepo.InMemoryinto sharedDoubleDown.Repo.Autogeneratemodule. - Repo contract now has 16 operations (was 15).
Fixed
- Invalid changesets passed to
Repo.TestorRepo.InMemoryinsert/updateno longer silently succeed — they return{:error, changeset}. Repo.InMemorystore is unchanged after a failed insert/update with an invalid changeset.
0.13.0
Added
- Fail-fast documentation for
impl: niltest 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
VERSIONfile.
0.11.1
Changed
- Documentation improvements (README, hexdocs, testing guide).
- Removed unnecessary
resetcalls from test examples.
0.11.0
Fixed
- Fixed compiler warnings.
0.10.0
Added
Facadewithout implicitContract—use DoubleDown.Facadewith an explicit:contractoption for separate contract modules.- Documentation explaining why
defcallbackis used instead of standard@callbackdeclarations.
0.9.0
Added
- Single-module
Contract + Facade—use DoubleDown.Facadewithout a:contractoption 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 withRepo.Test(stateless) andRepo.InMemory(stateful) test doubles.MultiStepperfor stepping throughEcto.Multioperations without a database.
Changed
- Renamed
PorttoFacadethroughout. - Removed separate
.Behaviourmodule — behaviours are generated directly on the contract module.
0.7.0
Changed
Repo.InMemoryfallback 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__/1idempotent — safe tousemultiple times.
0.5.0
Changed
- Improved
Repo.Teststateless 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: truetests.
0.3.1
Fixed
- Expand type aliases at macro time in
defcallbackto resolve Dialyzerunknown_typeerrors.
0.3.0
Added
transactdefcallback with{:defer, fn}support for stateful dispatch — avoids NimbleOwnership deadlocks.Repo.transact!forEcto.Multioperations.
0.2.0
Changed
- Split
DoubleDownintoDoubleDown.ContractandDoubleDown.Port(later renamed toFacade).
0.1.0
Added
- Initial release —
defcallbackmacro,DoubleDown.Contract,DoubleDown.Testingwith NimbleOwnership,Repo.Teststateless adapter, CI setup, Credo, Dialyzer.