ALLM.Sandbox (allm v0.4.0)

Copy Markdown View Source

Per-process engine resolution for tests — Layer B test-injection helper.

Lives in lib/ (NOT test/support/) so integrators can call it from their own test suites without depending on ALLM's internal test tree. The functions are pure process-dict and $callers reads — no GenServer, no ETS, no application state.

When to reach for it

When a test fans work out across Task.async/1 / Task.async_stream/3 workers — or any other process whose $callers chain includes the registering pid — Sandbox.set_engine/1 makes the test's %Engine{} visible to those workers via Sandbox.get_engine/0. The pattern mirrors Mox.allow/3 and Ecto.Adapters.SQL.Sandbox.allow/3 so integrators don't have to learn a new mental model.

Storage and isolation

  • Storage: Process.put(:"$allm_test_engine", engine) on the registering process.
  • Reads: walk Process.get(:"$callers", []) head → tail, returning the first ancestor's registration; fall back to the current process's own dict.
  • async: true isolation: every ExUnit test process owns its own process dict; registrations in test N never leak to test N+1.

Example

iex> engine = ALLM.Engine.new(adapter: ALLM.Providers.Fake,
...>   adapter_opts: [script: [{:text, "ok"}, {:finish, :stop}]])
iex> :ok = ALLM.Sandbox.set_engine(engine)
iex> ALLM.Sandbox.get_engine() == engine
true
iex> ALLM.Sandbox.unset_engine()
:ok
iex> ALLM.Sandbox.get_engine()
nil

Summary

Types

An %ALLM.Engine{} registered for cross-process resolution.

Functions

Resolve the registered engine for the current process.

Register an engine for the current process.

Clear the current process's registered engine. Idempotent — calling without a prior set_engine/1 returns :ok.

Scope an engine registration to a callback's lifetime.

Types

engine_value()

@type engine_value() :: ALLM.Engine.t()

An %ALLM.Engine{} registered for cross-process resolution.

Functions

get_engine()

@spec get_engine() :: engine_value() | nil

Resolve the registered engine for the current process.

Walks Process.get(:"$callers", []) head → tail (most-recent ancestor first) and returns the first ancestor whose process dict carries a registration. Falls back to the current process's own dict, then to nil when no ancestor has registered.

Examples

iex> ALLM.Sandbox.get_engine()
nil

iex> engine = ALLM.Engine.new(adapter: ALLM.Providers.Fake)
iex> :ok = ALLM.Sandbox.set_engine(engine)
iex> ALLM.Sandbox.get_engine() == engine
true

set_engine(engine)

@spec set_engine(engine_value()) :: :ok

Register an engine for the current process.

Visible to any process whose $callers chain includes the calling pid — Task.async/1, Task.async_stream/3, and any other process spawned via OTP's proc_lib machinery propagate $callers automatically.

Returns :ok.

Examples

iex> engine = ALLM.Engine.new(adapter: ALLM.Providers.Fake)
iex> ALLM.Sandbox.set_engine(engine)
:ok

unset_engine()

@spec unset_engine() :: :ok

Clear the current process's registered engine. Idempotent — calling without a prior set_engine/1 returns :ok.

Examples

iex> ALLM.Sandbox.unset_engine()
:ok

with_engine(engine, fun)

@spec with_engine(engine_value(), (-> result)) :: result when result: term()

Scope an engine registration to a callback's lifetime.

Equivalent to set_engine/1 immediately followed by unset_engine/0 in try/after, but additionally restores a prior registration (if any) on callback exit.

Returns the callback's return value.

Examples

iex> engine = ALLM.Engine.new(adapter: ALLM.Providers.Fake)
iex> ALLM.Sandbox.with_engine(engine, fn -> ALLM.Sandbox.get_engine() == engine end)
true
iex> ALLM.Sandbox.get_engine()
nil