Use VirtualTimeGenStateMachine instead of :gen_statem to test timeouts and
timers without waiting for them.
This module wraps OTP's :gen_statem and provides send_after/3, which
schedules against either real time (production) or a virtual clock (testing).
The callback contract is :gen_statem's: init/1, handle_event/4 or state
functions, terminate/3, and so on.
Example
defmodule MyStateMachine do
use VirtualTimeGenStateMachine, callback_mode: :handle_event_function
def start_link(opts) do
VirtualTimeGenStateMachine.start_link(__MODULE__, :off, opts)
end
@impl true
def init(_) do
{:ok, :off, %{count: 0}}
end
@impl true
def handle_event(:cast, :flip, :off, data) do
schedule_timer(100)
{:next_state, :on, %{data | count: data.count + 1}}
end
@impl true
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
@impl true
def handle_event(:info, :timeout, _state, data) do
{:keep_state, %{data | timeout_fired: true}}
end
defp schedule_timer(delay) do
VirtualTimeGenStateMachine.send_after(self(), :timeout, delay)
end
endTesting with a local virtual clock
test "state machine with timers" do
{:ok, clock} = VirtualClock.start_link()
{:ok, server} = MyStateMachine.start_link(virtual_clock: clock)
# Trigger state transition
VirtualTimeGenStateMachine.cast(server, :flip)
# Advance virtual time - timer fires instantly
VirtualClock.advance(clock, 100)
# Check that timeout fired
assert get_timeout_fired(server) == true
end
Summary
Functions
Makes a synchronous call to a state machine.
Cancels a timer created with send_after/3. Uses the appropriate backend based on the current process configuration.
Sends an asynchronous cast to a state machine.
Gets the current time backend.
Sends a message to a process after a delay in milliseconds. Uses the appropriate backend based on the current process configuration.
Sends a message immediately in virtual time.
Sets the virtual clock for the current process. All child processes will inherit this setting.
Sets the virtual clock for the current process without emitting warnings.
Sleeps for the specified duration in milliseconds. Uses the appropriate backend based on the current process configuration.
Starts a GenStateMachine without linking.
Starts a GenStateMachine with virtual time support.
Stops a state machine.
Uses real time (default behavior).
Uses real time without emitting warnings.
Functions
Makes a synchronous call to a state machine.
Cancels a timer created with send_after/3. Uses the appropriate backend based on the current process configuration.
Sends an asynchronous cast to a state machine.
Gets the current time backend.
Sends a message to a process after a delay in milliseconds. Uses the appropriate backend based on the current process configuration.
Sends a message immediately in virtual time.
With virtual time, this schedules the message for the current virtual time, ensuring it gets processed in the next event cycle. With real time, this sends the message immediately.
This is useful for triggering immediate responses or state changes within the virtual time simulation.
Examples
# Send immediate message to self
VirtualTimeGenStateMachine.send_immediately(self(), :process_now)
# Send immediate message to another process
VirtualTimeGenStateMachine.send_immediately(other_pid, {:urgent, data})
Sets the virtual clock for the current process. All child processes will inherit this setting.
Sets the virtual clock for the current process without emitting warnings.
Use this when you intentionally want global virtual clock behavior and understand the implications. The explanation message should describe why global clock is needed.
Example
iex> {:ok, clock} = VirtualClock.start_link()
iex> VirtualTimeGenStateMachine.set_virtual_clock(clock, :i_know_what_i_am_doing, "coordinated simulation")
VirtualTimeBackend
Sleeps for the specified duration in milliseconds. Uses the appropriate backend based on the current process configuration.
Starts a GenStateMachine without linking.
Starts a GenStateMachine with virtual time support.
Returns {:ok, pid, backend} where backend is the time backend to use. Store the backend in your process state for optimal performance.
Stops a state machine.
Uses real time (default behavior).
Uses real time without emitting warnings.
Use this when you intentionally want global real time behavior and understand the implications. The explanation message should describe why global real time is needed.
Example
iex> VirtualTimeGenStateMachine.use_real_time(:i_know_what_i_am_doing, "production mode")
RealTimeBackend