Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Writing a suite

The suite below is crates/breakwater/tests/counter.rs, compiled and run by cargo test: a counter contract, one handler, one invariant. The framework finds the eleventh increment and shrinks the sequence to eleven steps.

#![allow(unused)]
fn main() {
use breakwater::proptest::prelude::*;
use breakwater::{Actor, CallContext, CallOutcome, Handler, HandlerError, Invariant, Suite, TxLog, Violation};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};

const COUNT: Symbol = symbol_short!("COUNT");
const LIMIT: u32 = 10;

#[contract]
pub struct Counter;

#[contractimpl]
impl Counter {
    pub fn increment(env: Env) -> u32 {
        let n: u32 = env.storage().instance().get(&COUNT).unwrap_or(0) + 1;
        env.storage().instance().set(&COUNT, &n);
        n
    }
    pub fn get(env: Env) -> u32 {
        env.storage().instance().get(&COUNT).unwrap_or(0)
    }
}

struct Increment { contract: String, actors: Vec<Actor> }   // addresses are kept as strkeys

impl Handler for Increment {
    type Args = ();
    fn name(&self) -> &'static str { "increment" }
    fn actors(&self) -> &[Actor] { &self.actors }
    fn strategy(&self) -> BoxedStrategy<()> { Just(()).boxed() }
    fn call(&self, env: &Env, _ctx: &CallContext, _actor: &Actor, _args: &()) -> Result<CallOutcome, HandlerError> {
        let id = Address::from_str(env, &self.contract);
        match CounterClient::new(env, &id).try_increment() {
            Ok(Ok(count)) => Ok(CallOutcome::returning(count)),
            Ok(Err(e)) => Err(HandlerError::Other(format!("conversion error: {e:?}"))),
            Err(Ok(e)) => Ok(CallOutcome::ContractError(e.get_code())),
            Err(Err(e)) => Err(HandlerError::Host(format!("{e:?}"))),
        }
    }
}

struct NeverAbove { contract: String }

impl Invariant for NeverAbove {
    fn name(&self) -> &'static str { "count_never_above_limit" }
    fn check(&self, env: &Env, _log: &TxLog) -> Result<(), Violation> {
        let id = Address::from_str(env, &self.contract);
        let count = CounterClient::new(env, &id).get();
        if count > LIMIT {
            return Err(Violation::new("count exceeded the limit").with_detail(count));
        }
        Ok(())
    }
}

#[test]
fn finds_the_eleventh_increment_and_shrinks_to_eleven_steps() {
    let env = Env::default();
    let contract = env.register(Counter, ()).to_string().to_string();
    let owner = Actor::generate(&env, "owner");
    let restore_id = contract.clone();

    let report = Suite::new("counter", env)
        .seed(42)
        .steps(1..=30)
        .cases(32)
        .handler(Increment { contract: contract.clone(), actors: vec![owner] })
        .invariant(NeverAbove { contract })
        // Native test contracts live in the host, not the ledger: re-register them
        // on every Env the suite restores from its snapshot.
        .on_restore(move |env| { env.register_at(&Address::from_str(env, &restore_id), Counter, ()); })
        .run()
        .expect("suite runs");

    let failure = report.failure().expect("the limit must be exceeded");
    assert_eq!(failure.violation.source, "count_never_above_limit");
    assert_eq!(failure.log.len(), LIMIT as usize + 1);   // shrunk to exactly eleven steps
}
}

A suite is an ordinary cargo test; the breakwater CLI drives the test named breakwater_suite in tests/suite.rs, passing seed, step range and auth mode through BREAKWATER_SEED, BREAKWATER_STEPS and BREAKWATER_AUTH, and reads the JSON report the suite writes to BREAKWATER_REPORT. The full API, including real-authorization mode and replay, is in docs/ARCHITECTURE.md; the two example suites under examples/*/tests/ are the reference implementations.