Skip to main content

breakwater/
lib.rs

1//! Breakwater core library.
2//!
3//! Stateful property testing for Soroban contracts. A suite declares *actors*
4//! (who may call), *handlers* (what may be called) and *invariants* (what must
5//! always hold). Breakwater generates randomized call sequences against an
6//! in-process [`soroban_sdk::Env`], checks every invariant after every step,
7//! shrinks any failing sequence to a minimal reproduction and exports it.
8//!
9//! # Example
10//!
11//! A counter contract whose count must never exceed 2. The suite calls
12//! `increment` in random sequences, finds the violation and shrinks it to the
13//! three increments that cause it.
14//!
15//! ```
16//! use breakwater::proptest::prelude::*;
17//! use breakwater::{
18//!     Actor, CallContext, CallOutcome, Handler, HandlerError, Invariant, Suite, TxLog, Violation,
19//! };
20//! use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
21//!
22//! const COUNT: Symbol = symbol_short!("COUNT");
23//!
24//! #[contract]
25//! pub struct Counter;
26//!
27//! #[contractimpl]
28//! impl Counter {
29//!     pub fn increment(env: Env) -> u32 {
30//!         let n: u32 = env.storage().instance().get(&COUNT).unwrap_or(0) + 1;
31//!         env.storage().instance().set(&COUNT, &n);
32//!         n
33//!     }
34//!
35//!     pub fn get(env: Env) -> u32 {
36//!         env.storage().instance().get(&COUNT).unwrap_or(0)
37//!     }
38//! }
39//!
40//! // A handler describes one callable entry point and the arguments to feed it.
41//! struct Increment {
42//!     contract: String,
43//!     actors: Vec<Actor>,
44//! }
45//!
46//! impl Handler for Increment {
47//!     type Args = ();
48//!
49//!     fn name(&self) -> &'static str {
50//!         "increment"
51//!     }
52//!
53//!     fn actors(&self) -> &[Actor] {
54//!         &self.actors
55//!     }
56//!
57//!     fn strategy(&self) -> BoxedStrategy<()> {
58//!         Just(()).boxed()
59//!     }
60//!
61//!     fn call(
62//!         &self,
63//!         env: &Env,
64//!         _ctx: &CallContext,
65//!         _actor: &Actor,
66//!         _args: &(),
67//!     ) -> Result<CallOutcome, HandlerError> {
68//!         let id = Address::from_str(env, &self.contract);
69//!         match CounterClient::new(env, &id).try_increment() {
70//!             Ok(Ok(count)) => Ok(CallOutcome::returning(count)),
71//!             Ok(Err(e)) => Err(HandlerError::Other(format!("conversion error: {e:?}"))),
72//!             Err(Ok(e)) => Ok(CallOutcome::ContractError(e.get_code())),
73//!             Err(Err(e)) => Err(HandlerError::Host(format!("{e:?}"))),
74//!         }
75//!     }
76//! }
77//!
78//! // An invariant is checked after every step of every sequence.
79//! struct CountAtMostTwo {
80//!     contract: String,
81//! }
82//!
83//! impl Invariant for CountAtMostTwo {
84//!     fn name(&self) -> &'static str {
85//!         "count_at_most_two"
86//!     }
87//!
88//!     fn check(&self, env: &Env, _log: &TxLog) -> Result<(), Violation> {
89//!         let id = Address::from_str(env, &self.contract);
90//!         let count = CounterClient::new(env, &id).get();
91//!         if count > 2 {
92//!             return Err(Violation::new("count exceeded 2").with_detail(count));
93//!         }
94//!         Ok(())
95//!     }
96//! }
97//!
98//! fn main() {
99//!     let env = Env::default();
100//!     let contract = env.register(Counter, ()).to_string().to_string();
101//!     let owner = Actor::generate(&env, "owner");
102//!     let restore_id = contract.clone();
103//!
104//!     let report = Suite::new("counter", env)
105//!         .seed(42)
106//!         .steps(1..=8)
107//!         .cases(16)
108//!         .handler(Increment { contract: contract.clone(), actors: vec![owner] })
109//!         .invariant(CountAtMostTwo { contract })
110//!         // Re-registers the contract whenever the runner restores a snapshot.
111//!         .on_restore(move |env| {
112//!             env.register_at(&Address::from_str(env, &restore_id), Counter, ());
113//!         })
114//!         .run()
115//!         .expect("the suite runs");
116//!
117//!     let failure = report.failure().expect("the third increment breaks the invariant");
118//!     assert_eq!(failure.violation.source, "count_at_most_two");
119//!     assert_eq!(failure.log.len(), 3, "shrunk to three increments");
120//! }
121//! ```
122#![forbid(unsafe_code)]
123#![warn(missing_docs)]
124
125mod actor;
126pub mod auth;
127mod config;
128mod error;
129mod export;
130mod handler;
131mod invariant;
132mod ledger;
133mod log;
134mod replay;
135mod runner;
136mod step;
137mod suite;
138
139pub use actor::Actor;
140pub use config::{parse_steps, AuthMode, SuiteConfig};
141pub use error::SuiteError;
142pub use export::Exporter;
143pub use handler::{CallContext, CallOutcome, Handler, HandlerError, StepArgs};
144pub use invariant::{Invariant, Violation, ViolationKind};
145pub use ledger::{LedgerAdvance, LEDGERS_PER_DAY, SECONDS_PER_LEDGER};
146pub use log::{Gap, StepOutcome, StepRecord, TxLog};
147pub use replay::{ReplayError, ReplayStep};
148pub use step::Step;
149pub use suite::{Failure, Outcome, RunReport, Suite};
150
151/// Re-export so suites can name proptest strategies without a direct dependency.
152pub use proptest;