use crate::{always, sometimes};
pub const MAX_OPS: usize = 31;
pub const MAX_AMOUNT: u32 = 99;
const OP_BYTES: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Deposit(u32),
Withdraw(u32),
}
pub fn apply_ops(ops: &[Op]) {
let mut balance: i64 = 0;
for op in ops {
match *op {
Op::Deposit(amount) => balance += i64::from(amount),
Op::Withdraw(amount) => balance -= i64::from(amount).min(balance),
}
always!(balance >= 0, "balance went negative: {balance}");
sometimes!(balance == 0, "balance-returned-to-zero");
}
}
#[must_use]
pub fn ops_from_bytes(bytes: &[u8]) -> Vec<Op> {
let (ops, _partial) = bytes.as_chunks::<OP_BYTES>();
ops.iter()
.take(MAX_OPS)
.map(|&[tag, low, high]| {
let amount = 1 + u32::from(u16::from_le_bytes([low, high])) % MAX_AMOUNT;
if tag % 2 == 0 {
Op::Deposit(amount)
} else {
Op::Withdraw(amount)
}
})
.collect()
}
#[must_use]
pub fn ops_to_bytes(ops: &[Op]) -> Vec<u8> {
ops.iter()
.flat_map(|op| {
let (tag, amount) = match *op {
Op::Deposit(amount) => (0_u8, amount),
Op::Withdraw(amount) => (1_u8, amount),
};
let raw = u16::try_from(amount.saturating_sub(1)).unwrap_or(u16::MAX);
let [low, high] = raw.to_le_bytes();
[tag, low, high]
})
.collect()
}
pub fn run_fuzz_input(bytes: &[u8]) {
apply_ops(&ops_from_bytes(bytes));
}
#[cfg(feature = "sim-testing")]
pub fn ops_strategy() -> impl proptest::strategy::Strategy<Value = Vec<Op>> {
proptest::collection::vec(proptest::arbitrary::any::<Op>(), 1..=MAX_OPS)
}
#[cfg(feature = "sim-testing")]
impl proptest::arbitrary::Arbitrary for Op {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with((): ()) -> Self::Strategy {
use proptest::strategy::Strategy;
proptest::prop_oneof![
(1..=MAX_AMOUNT).prop_map(Op::Deposit),
(1..=MAX_AMOUNT).prop_map(Op::Withdraw),
]
.boxed()
}
}
#[cfg(test)]
mod tests {
use super::{MAX_AMOUNT, MAX_OPS, Op, apply_ops, ops_from_bytes, ops_to_bytes, run_fuzz_input};
#[test]
fn bytes_decode_to_both_ops_in_range() {
let ops = ops_from_bytes(&[0, 0, 0, 1, 0xff, 0xff, 2, 98, 0]);
assert_eq!(
ops,
vec![
Op::Deposit(1),
Op::Withdraw(1 + u32::from(u16::MAX) % MAX_AMOUNT),
Op::Deposit(MAX_AMOUNT),
]
);
}
#[test]
fn decoding_caps_the_length_and_drops_a_partial_op() {
assert_eq!(ops_from_bytes(&[0; 3 * 40]).len(), MAX_OPS);
assert_eq!(ops_from_bytes(&[0, 1]).len(), 0);
}
#[test]
fn a_sweep_failure_replays_as_a_fuzz_input() {
let ops = vec![Op::Deposit(1), Op::Withdraw(MAX_AMOUNT), Op::Deposit(42)];
assert_eq!(ops_from_bytes(&ops_to_bytes(&ops)), ops);
run_fuzz_input(&ops_to_bytes(&ops));
}
#[test]
fn the_invariant_holds_for_any_input() {
apply_ops(&[Op::Withdraw(5), Op::Deposit(3), Op::Withdraw(99)]);
for len in 0..64_u8 {
let bytes: Vec<u8> = (0..len).map(|i| i.wrapping_mul(37)).collect();
run_fuzz_input(&bytes);
}
}
}