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.