LemonCore. Testing. AsyncHelpers
(lemon_core v0.1.0)
View Source
Shared deterministic synchronization primitives for async test suites.
Replaces ad hoc Process.sleep calls with bounded polling and explicit
coordination barriers so that concurrency tests do not depend on wall-clock
timing and are therefore reproducible across machines and load conditions.
This module lives in lib/ (not test/) so that every app in the umbrella
can import LemonCore.Testing.AsyncHelpers in its test helpers without
adding extra compile-time dependencies.
Primitives
Eventual assertions
assert_eventually(fn -> some_condition() end)
assert_eventually(fn -> some_condition() end, timeout: 2_000, interval: 20)Process-lifecycle helpers
assert_process_dead(pid) # waits until pid is gone
assert_process_alive(pid) # waits until pid registersLatch (single-use barrier, one waiter, one releaser)
latch = latch()
spawn(fn -> do_something(); release(latch) end)
await_latch(latch)Rendez-vous barrier (N participants, all arrive then all proceed)
barrier = barrier(3)
# Each of the 3 participants calls:
arrive(barrier)
await_barrier(barrier) # blocks until all 3 have arrivedOrdered task runner
with_ordered_tasks([fn -> work1() end, fn -> work2() end])
Summary
Functions
Signals that a participant has arrived at the barrier. Thread-safe: uses ETS update_counter for atomic increment.
Asserts that condition_fn returns truthy within timeout milliseconds,
polling every interval milliseconds.
Waits until pid is alive, up to timeout ms.
Fails the test if the process is not alive by the deadline.
Waits until pid is no longer alive, up to timeout ms.
Fails the test if the process is still alive at the deadline.
Waits until all participants have arrived at the barrier. Multiple callers can safely call this on the same barrier — the last caller cleans up the ETS table.
Blocks until the latch has been released, polling every interval ms.
Fails after timeout ms if the latch was never released.
Creates a barrier that must be arrived at count times before it opens.
Creates a new latch. A latch is a one-shot signal: one caller releases it
via release/1, another waits via await_latch/2.
Signals the latch so that await_latch/2 unblocks.
Safe to call from any process.
Runs a list of zero-arity functions as async tasks in a controlled, sequential order using latches.
Functions
@spec arrive(reference()) :: :ok
Signals that a participant has arrived at the barrier. Thread-safe: uses ETS update_counter for atomic increment.
Asserts that condition_fn returns truthy within timeout milliseconds,
polling every interval milliseconds.
Options:
:timeout— maximum wait in ms (default: 2000):interval— polling interval in ms (default: 10):message— failure message prefix (default: "condition never became true")
Waits until pid is alive, up to timeout ms.
Fails the test if the process is not alive by the deadline.
Useful when a pid is obtained before the process has fully started.
Waits until pid is no longer alive, up to timeout ms.
Fails the test if the process is still alive at the deadline.
Waits until all participants have arrived at the barrier. Multiple callers can safely call this on the same barrier — the last caller cleans up the ETS table.
Blocks until the latch has been released, polling every interval ms.
Fails after timeout ms if the latch was never released.
Stops the Agent after returning (the latch is single-use).
@spec barrier(pos_integer()) :: reference()
Creates a barrier that must be arrived at count times before it opens.
Returns a reference that identifies the barrier. Pass this reference to
arrive/1 and await_barrier/2.
@spec latch() :: pid()
Creates a new latch. A latch is a one-shot signal: one caller releases it
via release/1, another waits via await_latch/2.
Implemented as an Agent holding a boolean flag.
@spec release(pid()) :: :ok
Signals the latch so that await_latch/2 unblocks.
Safe to call from any process.
Runs a list of zero-arity functions as async tasks in a controlled, sequential order using latches.
Each function is started as a task, but blocked on a per-task latch. Latches are released one at a time with a 1 ms gap, allowing each task to make progress before the next one is unblocked.
Returns the list of results in the same order as fns.