AuroraMeter. Test
(Aurora Meter v1.0.0)
View Source
Helpers for testing an application that uses Aurora Meter.
defmodule MyApp.MeterCase do
use ExUnit.CaseTemplate
using do
quote do
use AuroraMeter.Test, reset: true
end
end
enduse AuroraMeter.Test imports every function below. With reset: true the
ETS tables are cleared before each test (see reset!/0, which requires
async: false); with sandbox: true an Ecto.Adapters.SQL.Sandbox owner is
started on the configured repo for tests that do not already have one.
Set large :flush_interval / :broadcast_interval values in test config so
the timers never fire mid-test, and drive them with flush!/0 and
broadcast!/0.
Cross-node behaviour can be exercised on one node: simulate_node/3 applies
deltas as if another node had gossiped them, and simulate_flush/2 applies
totals as if another node had flushed.
For the credit ledger, fund!/3 and drain!/1 put a tenant at a known
balance without inventing references, and credit_balance/1 reads it back.
To make a time-dependent behaviour deterministic, freeze the clock with
with_clock/2 and move it with travel/1 and travel/2. The fixed clock is
a single named agent, so it is global to the node and every test that uses it
must be async: false.
Summary
Functions
Broadcasts touched counters to subscribed LiveViews (and gossips deltas) now.
Starts an Ecto.Adapters.SQL.Sandbox owner on the configured repo (shared
unless the test is async) and stops it on exit. Use only when your own case
template does not already do this.
The tenant's credit balance snapshot; see AuroraMeter.Credits.balance/1.
Debits everything tenant has available (nothing when the available balance
is zero or negative), returning the amount drained.
Flushes dirty counters to the database now; returns the number of keys persisted.
Grants amount micro-dollars to tenant as an :adjustment with a unique
reference (so it never collides with an earlier grant), returning the entry.
Any AuroraMeter.Credits.grant/3 option can be overridden in opts.
Clears every Aurora Meter ETS table: counters, dirty and touched sets, and
the subscription cache. Unflushed usage is discarded; the database is not
touched. Because the tables are global, only use this in async: false
tests (or give each test a unique tenant instead, see unique_tenant/1).
Applies totals ([{tenant, feature, total}]) as if node origin had
flushed and announced them: this node re-bases each warm counter on the total
plus its own unflushed increments.
Applies deltas ([{tenant, feature, delta}]) as if node origin had
gossiped them: this node's view of each counter moves by the delta, exactly
as it would in a cluster. period_start defaults to the tenant's current
period. Only warm counters are affected, as in production; read the counter
first if you need it seeded.
Subscribes tenant to plan with the assignment starting at since, and
drops the cached row.
Moves the frozen clock to instant. Raises outside a with_clock/2 block.
Moves the frozen clock by amount of unit (any unit DateTime.add/3
accepts). Raises outside a with_clock/2 block, so it cannot silently no-op.
A process-unique tenant key, so tests can share the global ETS tables safely.
Runs fun with the library's clock frozen at instant.
Functions
@spec broadcast!() :: :ok
Broadcasts touched counters to subscribed LiveViews (and gossips deltas) now.
@spec checkout(map()) :: :ok
Starts an Ecto.Adapters.SQL.Sandbox owner on the configured repo (shared
unless the test is async) and stops it on exit. Use only when your own case
template does not already do this.
@spec credit_balance(term()) :: AuroraMeter.Credits.balance()
The tenant's credit balance snapshot; see AuroraMeter.Credits.balance/1.
@spec drain!(term()) :: non_neg_integer()
Debits everything tenant has available (nothing when the available balance
is zero or negative), returning the amount drained.
@spec flush!() :: non_neg_integer()
Flushes dirty counters to the database now; returns the number of keys persisted.
@spec fund!(term(), pos_integer(), keyword()) :: AuroraMeter.Credits.txn()
Grants amount micro-dollars to tenant as an :adjustment with a unique
reference (so it never collides with an earlier grant), returning the entry.
Any AuroraMeter.Credits.grant/3 option can be overridden in opts.
fund!(org, Money.from_cents(1_000))
fund!(org, 500_000, category: :promotional, expires_at: tomorrow)
@spec reset!() :: :ok
Clears every Aurora Meter ETS table: counters, dirty and touched sets, and
the subscription cache. Unflushed usage is discarded; the database is not
touched. Because the tables are global, only use this in async: false
tests (or give each test a unique tenant instead, see unique_tenant/1).
@spec simulate_flush(node(), [{term(), atom(), integer()}], DateTime.t() | nil) :: :ok
Applies totals ([{tenant, feature, total}]) as if node origin had
flushed and announced them: this node re-bases each warm counter on the total
plus its own unflushed increments.
@spec simulate_node(node(), [{term(), atom(), integer()}], DateTime.t() | nil) :: :ok
Applies deltas ([{tenant, feature, delta}]) as if node origin had
gossiped them: this node's view of each counter moves by the delta, exactly
as it would in a cluster. period_start defaults to the tenant's current
period. Only warm counters are affected, as in production; read the counter
first if you need it seeded.
@spec subscribe_since!(term(), atom(), DateTime.t(), keyword()) :: AuroraMeter.Schema.Subscription.t()
Subscribes tenant to plan with the assignment starting at since, and
drops the cached row.
AuroraMeter.subscribe/3 starts the assignment at the database's clock, which
is right for a real subscription and wrong for a test whose usage is dated in
the past: every such event would be attributed before the contract existed and
stamped attribution: :plan_unresolved (build unit 07c). This writes the row
with an explicit plan_effective_at instead, which is a supported field of
AuroraMeter.Storage.put_subscription/1, so a test about something other than
attribution can have the tenant a real host would have.
Options: :version (default the effective one) and :status
(default "active").
subscribe_since!("org_1", :pro, ~U[2026-01-01 00:00:00Z])
@spec travel(DateTime.t()) :: :ok
Moves the frozen clock to instant. Raises outside a with_clock/2 block.
@spec travel(integer(), AuroraMeter.Clock.Fixed.unit()) :: :ok
Moves the frozen clock by amount of unit (any unit DateTime.add/3
accepts). Raises outside a with_clock/2 block, so it cannot silently no-op.
A process-unique tenant key, so tests can share the global ETS tables safely.
@spec with_clock(DateTime.t(), (-> result)) :: result when result: var
Runs fun with the library's clock frozen at instant.
Installs AuroraMeter.Clock.Fixed under the clock: key, starts it at
instant, runs fun, then stops the agent and restores the previous
configuration. Restoration and the stop both run on a raise as well as on a
normal return, so one failing test cannot leave the next one frozen.
Inside the block, move the clock with travel/1 and travel/2.
with_clock(~U[2026-01-31 23:59:59.999999Z], fn ->
assert AuroraMeter.period(org).start == ~U[2026-01-01 00:00:00Z]
travel(1, :microsecond)
assert AuroraMeter.period(org).start == ~U[2026-02-01 00:00:00Z]
end)The fixed clock is global to the node: use it only in async: false tests.
Because a frozen clock stamps every row with the same inserted_at, a test
that writes several credit ledger rows and then pages AuroraMeter.Credits.history/2
must travel/2 between the writes; see docs/testing.md.