use std::sync::atomic::{self, AtomicUsize};
use std::sync::Arc;
use proptest::bits::{BitSetLike, VarBitSet};
use proptest::collection::SizeRange;
use proptest::num::sample_uniform_incl;
use proptest::std_facade::fmt::{Debug, Formatter, Result};
use proptest::std_facade::Vec;
use proptest::strategy::BoxedStrategy;
use proptest::strategy::{NewTree, Strategy, ValueTree};
use proptest::test_runner::TestRunner;
pub trait ReferenceStateMachine: 'static {
type State: Clone + Debug;
type Transition: Clone + Debug;
fn init_state() -> BoxedStrategy<Self::State>;
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition>;
fn apply(state: Self::State, transition: &Self::Transition) -> Self::State;
fn preconditions(
state: &Self::State,
transition: &Self::Transition,
) -> bool {
let _ = (state, transition);
true
}
fn sequential_strategy(
size: impl Into<SizeRange>,
) -> Sequential<
Self::State,
Self::Transition,
BoxedStrategy<Self::State>,
BoxedStrategy<Self::Transition>,
> {
Sequential::new(
size.into(),
Self::init_state,
Self::preconditions,
Self::transitions,
Self::apply,
)
}
}
pub struct Sequential<State, Transition, StateStrategy, TransitionStrategy> {
size: SizeRange,
init_state: Arc<dyn Fn() -> StateStrategy + Send + Sync>,
preconditions: Arc<dyn Fn(&State, &Transition) -> bool + Send + Sync>,
transitions: Arc<dyn Fn(&State) -> TransitionStrategy + Send + Sync>,
next: Arc<dyn Fn(State, &Transition) -> State + Send + Sync>,
}
impl<State, Transition, StateStrategy, TransitionStrategy>
Sequential<State, Transition, StateStrategy, TransitionStrategy>
where
State: 'static,
Transition: 'static,
StateStrategy: 'static,
TransitionStrategy: 'static,
{
pub fn new(
size: SizeRange,
init_state: impl Fn() -> StateStrategy + 'static + Send + Sync,
preconditions: impl Fn(&State, &Transition) -> bool + 'static + Send + Sync,
transitions: impl Fn(&State) -> TransitionStrategy + 'static + Send + Sync,
next: impl Fn(State, &Transition) -> State + 'static + Send + Sync,
) -> Self {
Self {
size,
init_state: Arc::new(init_state),
preconditions: Arc::new(preconditions),
transitions: Arc::new(transitions),
next: Arc::new(next),
}
}
}
impl<State, Transition, StateStrategy, TransitionStrategy> Debug
for Sequential<State, Transition, StateStrategy, TransitionStrategy>
{
fn fmt(&self, f: &mut Formatter) -> Result {
f.debug_struct("Sequential")
.field("size", &self.size)
.finish()
}
}
impl<
State: Clone + Debug,
Transition: Clone + Debug,
StateStrategy: Strategy<Value = State>,
TransitionStrategy: Strategy<Value = Transition>,
> Strategy
for Sequential<State, Transition, StateStrategy, TransitionStrategy>
{
type Tree = SequentialValueTree<
State,
Transition,
StateStrategy::Tree,
TransitionStrategy::Tree,
>;
type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let initial_state = (self.init_state)().new_tree(runner)?;
let last_valid_initial_state = initial_state.current();
let (min_size, end) = self.size.start_end_incl();
let max_size = sample_uniform_incl(runner, min_size, end);
let mut transitions = Vec::with_capacity(max_size);
let mut acceptable_transitions = Vec::with_capacity(max_size);
let included_transitions = VarBitSet::saturated(max_size);
let shrinkable_transitions = VarBitSet::saturated(max_size);
let mut state = initial_state.current();
while transitions.len() < max_size {
let transition_tree =
(self.transitions)(&state).new_tree(runner)?;
let transition = transition_tree.current();
if (self.preconditions)(&state, &transition) {
transitions.push(transition_tree);
state = (self.next)(state, &transition);
acceptable_transitions
.push((TransitionState::Accepted, transition));
} else {
runner.reject_local("Pre-conditions were not satisfied")?;
}
}
let max_ix = max_size - 1;
Ok(SequentialValueTree {
initial_state,
is_initial_state_shrinkable: true,
last_valid_initial_state,
preconditions: self.preconditions.clone(),
next: self.next.clone(),
transitions,
acceptable_transitions,
included_transitions,
shrinkable_transitions,
max_ix,
shrink: Shrink::DeleteTransition(max_ix),
last_shrink: None,
seen_transitions_counter: Some(Default::default()),
})
}
}
#[derive(Clone, Copy, Debug)]
enum Shrink {
InitialState,
DeleteTransition(usize),
Transition(usize),
}
use Shrink::*;
#[derive(Clone, Copy, Debug)]
enum TransitionState {
Accepted,
SimplifyRejected,
ComplicateRejected,
}
use TransitionState::*;
pub struct SequentialValueTree<
State,
Transition,
StateValueTree,
TransitionValueTree,
> {
initial_state: StateValueTree,
is_initial_state_shrinkable: bool,
last_valid_initial_state: State,
preconditions: Arc<dyn Fn(&State, &Transition) -> bool>,
next: Arc<dyn Fn(State, &Transition) -> State>,
transitions: Vec<TransitionValueTree>,
acceptable_transitions: Vec<(TransitionState, Transition)>,
included_transitions: VarBitSet,
shrinkable_transitions: VarBitSet,
max_ix: usize,
shrink: Shrink,
last_shrink: Option<Shrink>,
seen_transitions_counter: Option<Arc<AtomicUsize>>,
}
impl<
State: Clone + Debug,
Transition: Clone + Debug,
StateValueTree: ValueTree<Value = State>,
TransitionValueTree: ValueTree<Value = Transition>,
>
SequentialValueTree<State, Transition, StateValueTree, TransitionValueTree>
{
fn try_simplify(&mut self) -> bool {
if let Some(seen_transitions_counter) =
self.seen_transitions_counter.as_ref()
{
let seen_count =
seen_transitions_counter.load(atomic::Ordering::SeqCst);
let included_count = self.included_transitions.count();
if seen_count < included_count {
let mut kept_count = 0;
for ix in 0..self.transitions.len() {
if self.included_transitions.test(ix) {
if kept_count < seen_count {
kept_count += 1;
} else {
self.included_transitions.clear(ix);
self.shrinkable_transitions.clear(ix);
}
}
}
if kept_count == 0 {
self.shrink = InitialState;
} else if kept_count == 1 {
self.shrink = Transition(0);
} else {
self.shrink = DeleteTransition(
kept_count.checked_sub(2).unwrap_or_default(),
);
}
}
self.seen_transitions_counter = None;
}
if let DeleteTransition(ix) = self.shrink {
self.included_transitions.clear(ix);
self.last_shrink = Some(self.shrink);
self.shrink = if ix == 0 {
Transition(0)
} else {
DeleteTransition(ix - 1)
};
if !self
.check_acceptable(None, self.last_valid_initial_state.clone())
{
self.included_transitions.set(ix);
self.last_shrink = None;
return self.try_simplify();
}
self.shrinkable_transitions.clear(ix);
return true;
}
while let Transition(ix) = self.shrink {
if self.shrinkable_transitions.count() == 0 {
self.shrink = Shrink::InitialState;
break;
}
if !self.included_transitions.test(ix) {
self.shrink = self.next_shrink_transition(ix);
continue;
}
if let Some((SimplifyRejected, _trans)) =
self.acceptable_transitions.get(ix)
{
self.shrink = self.next_shrink_transition(ix);
} else if self.transitions[ix].simplify() {
self.last_shrink = Some(self.shrink);
if self.check_acceptable(
Some(ix),
self.last_valid_initial_state.clone(),
) {
self.acceptable_transitions[ix] =
(Accepted, self.transitions[ix].current());
return true;
} else {
let (state, _trans) =
self.acceptable_transitions.get_mut(ix).unwrap();
*state = SimplifyRejected;
self.shrinkable_transitions.clear(ix);
self.shrink = self.next_shrink_transition(ix);
return self.simplify();
}
} else {
self.shrinkable_transitions.clear(ix);
self.shrink = self.next_shrink_transition(ix);
}
}
if let InitialState = self.shrink {
if self.initial_state.simplify() {
if self.check_acceptable(None, self.initial_state.current()) {
self.last_valid_initial_state =
self.initial_state.current();
self.last_shrink = Some(self.shrink);
return true;
} else {
self.last_shrink = None;
}
}
self.is_initial_state_shrinkable = false;
return false;
}
panic!("Unexpected shrink state");
}
fn try_to_find_acceptable_transition(&mut self, ix: usize) -> bool {
let mut ix_to_check = ix;
loop {
if self.included_transitions.test(ix_to_check)
&& self.check_acceptable(
Some(ix_to_check),
self.last_valid_initial_state.clone(),
)
{
self.acceptable_transitions[ix_to_check] =
(Accepted, self.transitions[ix_to_check].current());
return true;
}
if ix_to_check == self.max_ix {
ix_to_check = 0;
} else {
ix_to_check += 1;
}
if ix_to_check == ix {
return false;
}
}
}
fn check_acceptable(&self, ix: Option<usize>, mut state: State) -> bool {
let transitions = self.get_included_acceptable_transitions(ix);
for transition in transitions.iter() {
let is_acceptable = (self.preconditions)(&state, transition);
if is_acceptable {
state = (self.next)(state, transition);
} else {
return false;
}
}
true
}
fn get_included_acceptable_transitions(
&self,
ix: Option<usize>,
) -> Vec<Transition> {
self.acceptable_transitions
.iter()
.enumerate()
.filter(|&(this_ix, _)| self.included_transitions.test(this_ix))
.map(|(this_ix, (_, transition))| match ix {
Some(ix) if this_ix == ix => self.transitions[ix].current(),
_ => transition.clone(),
})
.collect()
}
fn can_simplify(&self) -> bool {
self.is_initial_state_shrinkable ||
!self
.acceptable_transitions
.iter()
.enumerate()
.filter(|&(ix, _)| self.included_transitions.test(ix))
.all(|(_, (state, _transition))| {
matches!(state, SimplifyRejected | ComplicateRejected)
})
}
fn next_shrink_transition(&self, current_ix: usize) -> Shrink {
if current_ix == self.max_ix {
Transition(0)
} else {
Transition(current_ix + 1)
}
}
}
impl<
State: Clone + Debug,
Transition: Clone + Debug,
StateValueTree: ValueTree<Value = State>,
TransitionValueTree: ValueTree<Value = Transition>,
> ValueTree
for SequentialValueTree<
State,
Transition,
StateValueTree,
TransitionValueTree,
>
{
type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
fn current(&self) -> Self::Value {
if let Some(seen_transitions_counter) = &self.seen_transitions_counter {
if seen_transitions_counter.load(atomic::Ordering::SeqCst) > 0 {
panic!("Unexpected non-zero `seen_transitions_counter`");
}
}
(
self.last_valid_initial_state.clone(),
self.get_included_acceptable_transitions(None),
self.seen_transitions_counter.clone(),
)
}
fn simplify(&mut self) -> bool {
let was_simplified = if self.can_simplify() {
self.try_simplify()
} else if let Some(Transition(ix)) = self.last_shrink {
self.try_to_find_acceptable_transition(ix)
} else {
false
};
self.seen_transitions_counter = Default::default();
was_simplified
}
fn complicate(&mut self) -> bool {
self.seen_transitions_counter = Default::default();
match &self.last_shrink {
None => false,
Some(DeleteTransition(ix)) => {
self.included_transitions.set(*ix);
self.shrinkable_transitions.set(*ix);
self.last_shrink = None;
true
}
Some(Transition(ix)) => {
let ix = *ix;
if self.transitions[ix].complicate() {
if self.check_acceptable(
Some(ix),
self.last_valid_initial_state.clone(),
) {
self.acceptable_transitions[ix] =
(Accepted, self.transitions[ix].current());
return true;
} else {
let (state, _trans) =
self.acceptable_transitions.get_mut(ix).unwrap();
*state = ComplicateRejected;
}
}
self.last_shrink = None;
false
}
Some(InitialState) => {
if self.initial_state.complicate()
&& self.check_acceptable(None, self.initial_state.current())
{
self.last_valid_initial_state =
self.initial_state.current();
return true;
}
self.last_shrink = None;
false
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use proptest::collection::hash_set;
use proptest::prelude::*;
use heap_state_machine::*;
use std::collections::HashSet;
const SIMPLIFICATIONS: usize = 32;
const TRANSITIONS: usize = 32;
#[test]
fn number_of_sequential_value_tree_simplifications() {
let mut value_tree = deterministic_sequential_value_tree();
value_tree
.seen_transitions_counter
.as_mut()
.unwrap()
.store(TRANSITIONS, atomic::Ordering::SeqCst);
let mut i = 0;
loop {
let simplified = value_tree.simplify();
if simplified {
i += 1;
} else {
break;
}
}
assert_eq!(i, SIMPLIFICATIONS);
}
proptest! {
#[test]
fn test_state_machine_sequential_value_tree(
complicate_ixs in hash_set(0..SIMPLIFICATIONS, 0..SIMPLIFICATIONS)
) {
test_state_machine_sequential_value_tree_aux(complicate_ixs)
}
}
fn test_state_machine_sequential_value_tree_aux(
complicate_ixs: HashSet<usize>,
) {
println!("Complicate indices: {complicate_ixs:?}");
let mut value_tree = deterministic_sequential_value_tree();
let check_preconditions = |value_tree: &TestValueTree| {
let (mut state, transitions, _seen_counter) = value_tree.current();
let len = transitions.len();
println!("Transitions {}", len);
for (ix, transition) in transitions.into_iter().enumerate() {
println!("Transition {}/{len} {transition:?}", ix + 1);
assert!(
<HeapStateMachine as ReferenceStateMachine>::preconditions(
&state,
&transition
)
);
state = <HeapStateMachine as ReferenceStateMachine>::apply(
state,
&transition,
);
}
};
let mut ix = 0_usize;
loop {
let simplified = value_tree.simplify();
check_preconditions(&value_tree);
if !simplified {
break;
}
ix += 1;
if complicate_ixs.contains(&ix) {
loop {
let complicated = value_tree.complicate();
check_preconditions(&value_tree);
if !complicated {
break;
}
}
}
}
}
proptest! {
#[test]
fn test_value_tree_initial_simplification(
len in 10usize..100,
) {
test_value_tree_initial_simplification_aux(len)
}
}
fn test_value_tree_initial_simplification_aux(len: usize) {
let sequential =
<HeapStateMachine as ReferenceStateMachine>::sequential_strategy(
..len,
);
let mut runner = TestRunner::deterministic();
let mut value_tree = sequential.new_tree(&mut runner).unwrap();
let (_, transitions, mut seen_counter) = value_tree.current();
let num_seen = transitions.len() / 2;
let seen_counter = seen_counter.as_mut().unwrap();
seen_counter.store(num_seen, atomic::Ordering::SeqCst);
let mut seen_before_complication =
transitions.into_iter().take(num_seen).collect::<Vec<_>>();
assert!(value_tree.simplify());
let (_, transitions, _seen_counter) = value_tree.current();
let seen_after_first_complication =
transitions.into_iter().collect::<Vec<_>>();
if seen_before_complication.len() > 1 {
let last = seen_before_complication.pop().unwrap();
seen_before_complication.pop();
seen_before_complication.push(last);
assert_eq!(
seen_before_complication, seen_after_first_complication,
"only seen transitions should be present after first simplification"
);
} else {
assert!(
!seen_after_first_complication.is_empty(),
"When only 1 transition was seen, at least 1 should remain after simplification"
);
assert!(
matches!(value_tree.shrink, Transition(0)),
"When only 1 transition was seen, shrink should be set to Transition(0)"
);
}
}
#[test]
fn test_call_to_current_with_non_zero_seen_counter() {
let result = std::panic::catch_unwind(|| {
let value_tree = deterministic_sequential_value_tree();
let (_, _transitions1, mut seen_counter) = value_tree.current();
{
let seen_counter = seen_counter.as_mut().unwrap();
seen_counter.store(1, atomic::Ordering::SeqCst);
}
drop(seen_counter);
let _transitions2 = value_tree.current();
})
.expect_err("should panic");
let s = "Unexpected non-zero `seen_transitions_counter`";
assert_eq!(result.downcast_ref::<&str>(), Some(&s));
}
mod heap_state_machine {
use std::vec::Vec;
use crate::{ReferenceStateMachine, SequentialValueTree};
use proptest::prelude::*;
use proptest::test_runner::TestRunner;
use super::TRANSITIONS;
pub struct HeapStateMachine;
pub type TestValueTree = SequentialValueTree<
TestState,
TestTransition,
<BoxedStrategy<TestState> as Strategy>::Tree,
<BoxedStrategy<TestTransition> as Strategy>::Tree,
>;
pub type TestState = Vec<i32>;
#[derive(Clone, Debug, PartialEq)]
pub enum TestTransition {
PopNonEmpty,
PopEmpty,
Push(i32),
}
pub fn deterministic_sequential_value_tree() -> TestValueTree {
let sequential =
<HeapStateMachine as ReferenceStateMachine>::sequential_strategy(
TRANSITIONS,
);
let mut runner = TestRunner::deterministic();
sequential.new_tree(&mut runner).unwrap()
}
impl ReferenceStateMachine for HeapStateMachine {
type State = TestState;
type Transition = TestTransition;
fn init_state() -> BoxedStrategy<Self::State> {
Just(vec![]).boxed()
}
fn transitions(
state: &Self::State,
) -> BoxedStrategy<Self::Transition> {
if state.is_empty() {
prop_oneof![
1 => Just(TestTransition::PopEmpty),
2 => (any::<i32>()).prop_map(TestTransition::Push),
]
.boxed()
} else {
prop_oneof![
1 => Just(TestTransition::PopNonEmpty),
2 => (any::<i32>()).prop_map(TestTransition::Push),
]
.boxed()
}
}
fn apply(
mut state: Self::State,
transition: &Self::Transition,
) -> Self::State {
match transition {
TestTransition::PopEmpty => {
state.pop();
}
TestTransition::PopNonEmpty => {
state.pop();
}
TestTransition::Push(value) => state.push(*value),
}
state
}
fn preconditions(
state: &Self::State,
transition: &Self::Transition,
) -> bool {
match transition {
TestTransition::PopEmpty => state.is_empty(),
TestTransition::PopNonEmpty => !state.is_empty(),
TestTransition::Push(_) => true,
}
}
}
}
mod find_simplest_failure {
use proptest::prelude::*;
use proptest::strategy::BoxedStrategy;
use proptest::test_runner::TestRng;
use proptest::{
collection,
strategy::Strategy,
test_runner::{Config, TestError, TestRunner},
};
use crate::{ReferenceStateMachine, StateMachineTest};
const MIN_TRANSITION: u32 = 10;
const MAX_TRANSITION: u32 = 20;
const MIN_LIMIT: u32 = 2;
const MAX_LIMIT: u32 = 50;
#[derive(Debug, Default, Clone)]
struct FailIfLessThan(u32);
impl ReferenceStateMachine for FailIfLessThan {
type State = Self;
type Transition = u32;
fn init_state() -> BoxedStrategy<Self> {
(MIN_LIMIT..MAX_LIMIT).prop_map(FailIfLessThan).boxed()
}
fn transitions(_: &Self::State) -> BoxedStrategy<u32> {
(MIN_TRANSITION..MAX_TRANSITION).boxed()
}
fn apply(state: Self::State, _: &Self::Transition) -> Self::State {
state
}
}
struct FailIfLessThanTest;
impl StateMachineTest for FailIfLessThanTest {
type SystemUnderTest = ();
type Reference = FailIfLessThan;
fn init_test(ref_state: &FailIfLessThan) {
println!();
println!("starting {ref_state:?}");
}
fn apply(
(): Self::SystemUnderTest,
ref_state: &FailIfLessThan,
transition: u32,
) -> Self::SystemUnderTest {
let FailIfLessThan(limit) = ref_state;
println!("{transition} < {}?", limit);
if transition < ref_state.0 {
panic!("{transition} < {}", limit);
}
}
}
proptest! {
#[test]
fn test_returns_simplest_failure(
seed in collection::vec(any::<u8>(), 32).no_shrink()) {
let mut runner = TestRunner::new_with_rng(
Config::default(), TestRng::from_seed(Default::default(), &seed));
let result = runner.run(
&FailIfLessThan::sequential_strategy(10..50_usize),
|(ref_state, transitions, seen_counter)| {
Ok(FailIfLessThanTest::test_sequential(
Default::default(),
ref_state,
transitions,
seen_counter,
))
},
);
if let Err(TestError::Fail(
_,
(FailIfLessThan(limit), transitions, _seen_counter),
)) = result
{
assert_eq!(transitions.len(), 1, "The minimal failing case should be ");
assert_eq!(limit, MIN_TRANSITION + 1);
assert!(transitions.into_iter().next().unwrap() < limit);
} else {
prop_assume!(false,
"If the state machine doesn't fail as intended, we need a case that fails.");
}
}
}
}
#[test]
fn test_zero_seen_transitions_optimization() {
let mut value_tree = deterministic_sequential_value_tree();
value_tree
.seen_transitions_counter
.as_mut()
.unwrap()
.store(0, atomic::Ordering::SeqCst);
let simplified = value_tree.simplify();
assert_eq!(value_tree.included_transitions.count(), 0,
"All transitions should be removed when none were seen");
assert!(matches!(value_tree.shrink, InitialState),
"Shrink should be set to InitialState when kept_count == 0");
assert!(!simplified,
"Simplification should return false since initial state (Just(vec![])) is not shrinkable");
let (_, transitions, _) = value_tree.current();
assert!(transitions.is_empty(),
"No transitions should remain when none were seen");
}
}