pub struct Machine<M> { /* private fields */ }Expand description
A state machine model paired with the configuration for running it: the
step count and, for concurrent machines, the concurrency bounds. Wrap the
model with machine() (or Machine::new), adjust the configuration
with the builder methods, and finish with Machine::run for a
StateMachine or Machine::run_concurrent for a
ConcurrentStateMachine.
The concurrency bounds default to 1 and exist only for concurrent models:
Machine::min_concurrency and Machine::max_concurrency are defined
when M: ConcurrentStateMachine, so a sequential machine cannot be given
bounds it would ignore.
§Example
use hegel::TestCase;
use hegel::stateful::machine;
struct Counter {
value: u32,
}
#[hegel::state_machine]
impl Counter {
#[rule]
fn increment(&mut self, _: TestCase) {
self.value += 1;
}
}
#[hegel::test]
fn test_counter(tc: TestCase) {
machine(Counter { value: 0 }).steps(200).run(tc);
}Implementations§
Source§impl<M> Machine<M>
impl<M> Machine<M>
Sourcepub fn new(model: M) -> Self
pub fn new(model: M) -> Self
Wrap model with the defaults: DEFAULT_STEP_COUNT steps and a
minimum and maximum concurrency of 1.
Sourcepub fn steps(self, step_count: i64) -> Self
pub fn steps(self, step_count: i64) -> Self
Set the target number of steps per test case: every case runs at
least one step and at most step_count. For a sequential machine a
step is one completed rule (rules that stop on a violated assumption
do not count); for a concurrent machine it is one round. Each sampled
invariant runs after any given step with probability
1 / step_count. Most test cases run the full step_count; the
shrinker is free to shorten a failing one. The count must be at least
1, or the run fails with a usage error.
Source§impl<M: StateMachine> Machine<M>
impl<M: StateMachine> Machine<M>
Sourcepub fn run(self, tc: TestCase)
pub fn run(self, tc: TestCase)
Execute a stateful test by repeatedly applying random rules and checking invariants.
A sequential machine is the special case of the engine’s concurrent
state-machine protocol with a single group and concurrency 1: the engine
hands out exactly one rule per round, so the join points where sampled
invariant checks may run fall after each rule. Invariants run in full on
the machine’s initial and final state; in between, each invariant runs at
a join point only when the engine’s sampling draw (probability
1 / step_count) says to, keeping an invariant’s expected cost
per test case constant as the step count grows — except always-run
invariants (#[invariant(always_run)]), which run at every join point.
One consequence of the
join-point timing: a sampled check can land after a rule that stopped on
a violated assumption (rules are expected to reject before mutating the
model, and nothing restores model state on rejection anyway).
Each test case runs at least one rule and at most this machine’s step
count (Self::steps).
Source§impl<M: ConcurrentStateMachine + Sync> Machine<M>
impl<M: ConcurrentStateMachine + Sync> Machine<M>
Sourcepub fn min_concurrency(self, min_concurrency: i64) -> Self
pub fn min_concurrency(self, min_concurrency: i64) -> Self
Set the minimum number of worker threads Self::run_concurrent
uses (default 1). The level is drawn per test case in
[min_concurrency, max_concurrency], weighted toward the maximum
(concurrency bugs need concurrency); set both bounds equal for a
fixed level. The bounds must satisfy
1 <= min_concurrency <= max_concurrency, or the run fails with a
usage error. Only concurrent models have concurrency bounds: this
method exists when M: ConcurrentStateMachine, so a sequential
machine cannot be given bounds it would ignore.
Sourcepub fn max_concurrency(self, max_concurrency: i64) -> Self
pub fn max_concurrency(self, max_concurrency: i64) -> Self
Set the maximum number of worker threads Self::run_concurrent
uses (default 1); see Self::min_concurrency.
Sourcepub fn run_concurrent(self, tc: TestCase)
pub fn run_concurrent(self, tc: TestCase)
Execute a concurrent stateful test. Execution proceeds in rounds. For
each round, the engine picks a random concurrency group; every worker
thread then runs a short (possibly empty) random sequence of rules from
that group — and only that group — concurrently with the other workers. Rules in the same
group may overlap each other, and rules in different groups never
overlap. Once every worker has finished its rules for the round, the
main thread runs the invariants the engine’s sampling draws select
(probability 1 / step_count each, and always-run invariants
unconditionally); every invariant runs in full on the machine’s initial
and final state. Each test case runs at most this machine’s step count
(Self::steps) rounds.
The number of worker threads is drawn per test case, when the state
machine is created, within this machine’s concurrency bounds
(Self::min_concurrency and Self::max_concurrency, both 1 by
default) and weighted toward the maximum (concurrency bugs need
concurrency); set equal bounds for a fixed level.
§Nondeterminism
Concurrency bugs are usually nondeterministic — thread scheduling is outside Hegel’s control — but concurrency alone does not mark the run nondeterministic: a properly serialized machine can fail deterministically, and the engine watches observed behavior (verdict flips, replay misses) exactly as for any other test. Once a divergence is observed, the run switches into nondeterministic handling: failures are confirmed by repeated replay before they are shrunk or persisted; each report carries a caveat quoting that replay evidence, plus a reproduce blob when confirmed. An unconfirmed failure still fails the run, caveated.
§Abandoned rules and lock poisoning
A rule abandoned mid-execution by a rejected assumption or a failed draw
can poison any lock std::sync::Mutex it was holding. To avoid this,
perform all draws upfront within each rule.
§Example
use std::sync::Mutex;
use hegel::TestCase;
use hegel::stateful::machine;
struct CounterTest {
counter: Mutex<i64>,
}
#[hegel::concurrent_state_machine]
impl CounterTest {
#[rule(group = "write")]
fn increment(&self, _: TestCase) {
*self.counter.lock().unwrap_or_else(|e| e.into_inner()) += 1;
}
#[rule(group = "read")]
fn read(&self, _: TestCase) {
let _ = *self.counter.lock().unwrap_or_else(|e| e.into_inner());
}
#[invariant]
fn non_negative(&self, _: TestCase) {
assert!(*self.counter.lock().unwrap_or_else(|e| e.into_inner()) >= 0);
}
}
#[hegel::test]
fn test_counter(tc: TestCase) {
let m = CounterTest { counter: Mutex::new(0) };
machine(m).max_concurrency(3).run_concurrent(tc);
}