Skip to main content

Machine

Struct Machine 

Source
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>

Source

pub fn new(model: M) -> Self

Wrap model with the defaults: DEFAULT_STEP_COUNT steps and a minimum and maximum concurrency of 1.

Source

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>

Source

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>

Source

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.

Source

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.

Source

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);
}

Trait Implementations§

Source§

impl<M: Clone> Clone for Machine<M>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<M: Debug> Debug for Machine<M>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<M> Freeze for Machine<M>
where M: Freeze,

§

impl<M> RefUnwindSafe for Machine<M>
where M: RefUnwindSafe,

§

impl<M> Send for Machine<M>
where M: Send,

§

impl<M> Sync for Machine<M>
where M: Sync,

§

impl<M> Unpin for Machine<M>
where M: Unpin,

§

impl<M> UnsafeUnpin for Machine<M>
where M: UnsafeUnpin,

§

impl<M> UnwindSafe for Machine<M>
where M: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.