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;