use crate::replay::{AccountAssert, CmpOp, Expect, Mutation, StateCheck};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cmp {
pub(crate) op: CmpOp,
pub(crate) value: i128,
}
macro_rules! cmp_ctor {
($name:ident, $op:ident, $doc:literal) => {
#[doc = $doc]
pub fn $name(value: impl Into<i128>) -> Cmp {
Cmp {
op: CmpOp::$op,
value: value.into(),
}
}
};
}
impl Cmp {
cmp_ctor!(eq, Eq, "`actual == value`");
cmp_ctor!(ne, Ne, "`actual != value`");
cmp_ctor!(lt, Lt, "`actual < value`");
cmp_ctor!(le, Le, "`actual <= value`");
cmp_ctor!(gt, Gt, "`actual > value`");
cmp_ctor!(ge, Ge, "`actual >= value`");
}
#[derive(Debug, Clone)]
pub struct Check(pub(crate) CheckKind);
#[derive(Debug, Clone)]
pub(crate) enum CheckKind {
Outcome(Expect),
LogContains(String),
ComputeUnits(Cmp),
MatchesOnchain,
Account(Vec<AccountAssert>),
}
impl Check {
pub fn success() -> Check {
Check(CheckKind::Outcome(Expect::Success))
}
pub fn revert() -> Check {
Check(CheckKind::Outcome(Expect::Revert))
}
pub fn revert_contains(text: impl Into<String>) -> Check {
Check(CheckKind::Outcome(Expect::RevertContains(text.into())))
}
pub fn any_outcome() -> Check {
Check(CheckKind::Outcome(Expect::Any))
}
pub fn log_contains(text: impl Into<String>) -> Check {
Check(CheckKind::LogContains(text.into()))
}
pub fn matches_onchain() -> Check {
Check(CheckKind::MatchesOnchain)
}
pub fn compute_units(cmp: Cmp) -> Check {
Check(CheckKind::ComputeUnits(cmp))
}
pub fn account(address: impl Into<String>) -> AccountCheck {
AccountCheck {
address: address.into(),
asserts: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct AccountCheck {
address: String,
asserts: Vec<AccountAssert>,
}
impl AccountCheck {
fn push(mut self, check: StateCheck) -> Self {
self.asserts.push(AccountAssert {
address: self.address.clone(),
check,
});
self
}
pub fn lamports(self, c: Cmp) -> Self {
self.push(StateCheck::Lamports {
op: c.op,
value: c.value,
})
}
pub fn lamports_delta(self, c: Cmp) -> Self {
self.push(StateCheck::LamportsDelta {
op: c.op,
value: c.value,
})
}
pub fn token_amount(self, c: Cmp) -> Self {
self.push(StateCheck::U64At {
offset: 64,
op: c.op,
value: c.value,
})
}
pub fn token_delta(self, c: Cmp) -> Self {
self.push(StateCheck::TokenDelta {
op: c.op,
value: c.value,
})
}
pub fn u64_at(self, offset: usize, c: Cmp) -> Self {
self.push(StateCheck::U64At {
offset,
op: c.op,
value: c.value,
})
}
pub fn field(self, name: impl Into<String>, c: Cmp) -> Self {
self.push(StateCheck::Field {
name: name.into(),
op: c.op,
value: c.value,
})
}
pub fn field_delta(self, name: impl Into<String>, c: Cmp) -> Self {
self.push(StateCheck::FieldDelta {
name: name.into(),
op: c.op,
value: c.value,
})
}
pub fn build(self) -> Check {
Check(CheckKind::Account(self.asserts))
}
}
#[derive(Debug, Clone)]
pub struct Scenario {
pub name: String,
pub mutations: Vec<Mutation>,
pub checks: Vec<Check>,
}
impl Scenario {
pub fn new(name: impl Into<String>) -> Scenario {
Scenario {
name: name.into(),
mutations: Vec::new(),
checks: Vec::new(),
}
}
pub fn mutate(mut self, m: Mutation) -> Scenario {
self.mutations.push(m);
self
}
pub fn check(mut self, c: Check) -> Scenario {
self.checks.push(c);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_expands_to_one_assert_per_constraint() {
let Check(CheckKind::Account(asserts)) = Check::account("X")
.lamports(Cmp::ge(1))
.field("count", Cmp::eq(100))
.build()
else {
panic!("expected account checks")
};
assert_eq!(asserts.len(), 2);
assert_eq!(asserts[0].address, "X");
}
#[test]
fn scenario_builder_accumulates() {
let s = Scenario::new("drain")
.mutate(Mutation::lamports("V", 0))
.check(Check::revert())
.check(Check::account("U").token_delta(Cmp::eq(0)).build());
assert_eq!(s.name, "drain");
assert_eq!(s.mutations.len(), 1);
assert_eq!(s.checks.len(), 2);
}
}