Skip to content

Commit 418c912

Browse files
committed
Merge branch 'hs/simplify-unseen' (#388)
* hs/simplify-unseen: changelog: add #388 panic on misuse add fallback to avoid panic delete unseen transitions in state machine test
2 parents 2efefa8 + c7d383f commit 418c912

File tree

3 files changed

+192
-27
lines changed

3 files changed

+192
-27
lines changed

proptest-state-machine/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Unreleased
22

3+
### New Features
4+
5+
- Remove unseen transitions on a first step of shrinking.
6+
([\#388](https://github.com/proptest-rs/proptest/pull/388))
7+
38
## 0.2.0
49

510
### Other Notes

proptest-state-machine/src/strategy.rs

+170-22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
//! Strategies used for abstract state machine testing.
1111
12+
use std::sync::atomic::{self, AtomicUsize};
13+
use std::sync::Arc;
14+
1215
use proptest::bits::{BitSetLike, VarBitSet};
1316
use proptest::collection::SizeRange;
1417
use proptest::num::sample_uniform_incl;
@@ -118,14 +121,19 @@ pub trait ReferenceStateMachine {
118121
/// The shrinking strategy is to iteratively apply `Shrink::InitialState`,
119122
/// `Shrink::DeleteTransition` and `Shrink::Transition`.
120123
///
121-
/// 1. We start by trying to delete transitions from the back of the list, until
122-
/// we can do so no further (reached the beginning of the list).
123-
/// We start from the back, because it's less likely to affect the state
124-
/// machine's pre-conditions, if any.
125-
/// 2. Then, we again iteratively attempt to shrink the individual transitions,
124+
/// 1. We start by trying to delete transitions from the back of the list that
125+
/// were never seen by the test, if any. Note that because proptest expects
126+
/// deterministic results in for reproducible issues, unlike the following
127+
/// steps this step will not be undone on `complicate`. If there were any
128+
/// unseen transitions, then the next step will start at trying to delete
129+
/// the transition before the last one seen as we know that the last
130+
/// transition cannot be deleted as it's the one that has failed.
131+
/// 2. Then, we keep trying to delete transitions from the back of the list, until
132+
/// we can do so no further (reached the beginning of the list)..
133+
/// 3. Then, we again iteratively attempt to shrink the individual transitions,
126134
/// but this time starting from the front of the list - i.e. from the first
127135
/// transition to be applied.
128-
/// 3. Finally, we try to shrink the initial state until it's not possible to
136+
/// 4. Finally, we try to shrink the initial state until it's not possible to
129137
/// shrink it any further.
130138
///
131139
/// For `complicate`, we attempt to undo the last shrink operation, if there was
@@ -162,7 +170,7 @@ impl<
162170
StateStrategy::Tree,
163171
TransitionStrategy::Tree,
164172
>;
165-
type Value = (State, Vec<Transition>);
173+
type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
166174

167175
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
168176
// Generate the initial state value tree
@@ -214,6 +222,7 @@ impl<
214222
// which is less likely to invalidate pre-conditions
215223
shrink: Shrink::DeleteTransition(max_ix),
216224
last_shrink: None,
225+
seen_transitions_counter: Some(Default::default()),
217226
})
218227
}
219228
}
@@ -276,6 +285,13 @@ pub struct SequentialValueTree<
276285
shrink: Shrink,
277286
/// The last applied shrink operation, if any
278287
last_shrink: Option<Shrink>,
288+
/// The number of transitions that were seen by the test runner.
289+
/// On a test run this is shared with `StateMachineTest::test_sequential`
290+
/// which increments the inner counter value on every transition. If the
291+
/// test fails, the counter is used to remove any unseen transitions before
292+
/// shrinking and this field is set to `None` as it's no longer needed for
293+
/// shrinking.
294+
seen_transitions_counter: Option<Arc<AtomicUsize>>,
279295
}
280296

281297
impl<
@@ -289,6 +305,46 @@ impl<
289305
/// Try to apply the next `self.shrink`. Returns `true` if a shrink has been
290306
/// applied.
291307
fn try_simplify(&mut self) -> bool {
308+
if let Some(seen_transitions_counter) =
309+
self.seen_transitions_counter.as_ref()
310+
{
311+
let seen_count =
312+
seen_transitions_counter.load(atomic::Ordering::SeqCst);
313+
314+
let included_count = self.included_transitions.count();
315+
316+
if seen_count < included_count {
317+
// the test runner did not see all the transitions so we can
318+
// delete the transitions that were not seen because they were
319+
// not executed
320+
321+
let mut kept_count = 0;
322+
for ix in 0..self.transitions.len() {
323+
if self.included_transitions.test(ix) {
324+
// transition at ix was part of test
325+
326+
if kept_count < seen_count {
327+
// transition at xi was seen by the test or we are
328+
// still below minimum size for the test
329+
kept_count += 1;
330+
} else {
331+
// transition at ix was never seen
332+
self.included_transitions.clear(ix);
333+
self.shrinkable_transitions.clear(ix);
334+
}
335+
}
336+
}
337+
// Set the next shrink to the transition before the last seen
338+
// transition (subtract 2 from `kept_count`)
339+
self.shrink = DeleteTransition(
340+
kept_count.checked_sub(2).unwrap_or_default(),
341+
);
342+
}
343+
344+
// Remove the seen transitions counter for shrinking runs
345+
self.seen_transitions_counter = None;
346+
}
347+
292348
if let DeleteTransition(ix) = self.shrink {
293349
// Delete the index from the included transitions
294350
self.included_transitions.clear(ix);
@@ -442,7 +498,7 @@ impl<
442498
/// yet been rejected.
443499
fn can_simplify(&self) -> bool {
444500
self.is_initial_state_shrinkable ||
445-
// If there are some transitions whose shrinking has not yet been
501+
// If there are some transitions whose shrinking has not yet been
446502
// rejected, we can try to shrink them further
447503
!self
448504
.acceptable_transitions
@@ -483,39 +539,54 @@ impl<
483539
TransitionValueTree,
484540
>
485541
{
486-
type Value = (State, Vec<Transition>);
542+
type Value = (State, Vec<Transition>, Option<Arc<AtomicUsize>>);
487543

488544
fn current(&self) -> Self::Value {
545+
if let Some(seen_transitions_counter) = &self.seen_transitions_counter {
546+
if seen_transitions_counter.load(atomic::Ordering::SeqCst) > 0 {
547+
panic!("Unexpected non-zero `seen_transitions_counter`");
548+
}
549+
}
550+
489551
(
490552
self.last_valid_initial_state.clone(),
491553
// The current included acceptable transitions
492554
self.get_included_acceptable_transitions(None),
555+
self.seen_transitions_counter.clone(),
493556
)
494557
}
495558

496559
fn simplify(&mut self) -> bool {
497-
if self.can_simplify() {
560+
let was_simplified = if self.can_simplify() {
498561
self.try_simplify()
562+
} else if let Some(Transition(ix)) = self.last_shrink {
563+
self.try_to_find_acceptable_transition(ix)
499564
} else {
500-
if let Some(Transition(ix)) = self.last_shrink {
501-
return self.try_to_find_acceptable_transition(ix);
502-
}
503565
false
504-
}
566+
};
567+
568+
// reset seen transactions counter for next run
569+
self.seen_transitions_counter = Default::default();
570+
571+
was_simplified
505572
}
506573

507574
fn complicate(&mut self) -> bool {
508-
match self.last_shrink {
575+
// reset seen transactions counter for next run
576+
self.seen_transitions_counter = Default::default();
577+
578+
match &self.last_shrink {
509579
None => false,
510580
Some(DeleteTransition(ix)) => {
511581
// Undo the last item we deleted. Can't complicate any further,
512582
// so unset prev_shrink.
513-
self.included_transitions.set(ix);
514-
self.shrinkable_transitions.set(ix);
583+
self.included_transitions.set(*ix);
584+
self.shrinkable_transitions.set(*ix);
515585
self.last_shrink = None;
516586
true
517587
}
518588
Some(Transition(ix)) => {
589+
let ix = *ix;
519590
if self.transitions[ix].complicate() {
520591
if self.check_acceptable(Some(ix)) {
521592
self.acceptable_transitions[ix] =
@@ -574,6 +645,11 @@ mod test {
574645
#[test]
575646
fn number_of_sequential_value_tree_simplifications() {
576647
let mut value_tree = deterministic_sequential_value_tree();
648+
value_tree
649+
.seen_transitions_counter
650+
.as_mut()
651+
.unwrap()
652+
.store(TRANSITIONS, atomic::Ordering::SeqCst);
577653

578654
let mut i = 0;
579655
loop {
@@ -614,7 +690,7 @@ mod test {
614690
let mut value_tree = deterministic_sequential_value_tree();
615691

616692
let check_preconditions = |value_tree: &TestValueTree| {
617-
let (mut state, transitions) = value_tree.current();
693+
let (mut state, transitions, _seen_counter) = value_tree.current();
618694
let len = transitions.len();
619695
println!("Transitions {}", len);
620696
for (ix, transition) in transitions.into_iter().enumerate() {
@@ -660,6 +736,77 @@ mod test {
660736
}
661737
}
662738

739+
proptest! {
740+
/// Test the initial simplifications of the `SequentialValueTree` produced
741+
/// by `deterministic_sequential_value_tree`.
742+
///
743+
/// We want to make sure that we initially remove the transitions that
744+
/// where not seen.
745+
#[test]
746+
fn test_value_tree_initial_simplification(
747+
len in 10usize..100,
748+
) {
749+
test_value_tree_initial_simplification_aux(len)
750+
}
751+
}
752+
753+
fn test_value_tree_initial_simplification_aux(len: usize) {
754+
let sequential =
755+
<HeapStateMachine as ReferenceStateMachine>::sequential_strategy(
756+
..len,
757+
);
758+
759+
let mut runner = TestRunner::deterministic();
760+
let mut value_tree = sequential.new_tree(&mut runner).unwrap();
761+
762+
let (_, transitions, mut seen_counter) = value_tree.current();
763+
764+
let num_seen = transitions.len() / 2;
765+
let seen_counter = seen_counter.as_mut().unwrap();
766+
seen_counter.store(num_seen, atomic::Ordering::SeqCst);
767+
768+
let mut seen_before_complication =
769+
transitions.into_iter().take(num_seen).collect::<Vec<_>>();
770+
771+
assert!(value_tree.simplify());
772+
773+
let (_, transitions, _seen_counter) = value_tree.current();
774+
775+
let seen_after_first_complication =
776+
transitions.into_iter().collect::<Vec<_>>();
777+
778+
// After the unseen transitions are removed, the shrink to delete the
779+
// transition before the last seen one is applied
780+
let last = seen_before_complication.pop().unwrap();
781+
seen_before_complication.pop();
782+
seen_before_complication.push(last);
783+
784+
assert_eq!(
785+
seen_before_complication, seen_after_first_complication,
786+
"only seen transitions should be present after first simplification"
787+
);
788+
}
789+
790+
#[test]
791+
fn test_call_to_current_with_non_zero_seen_counter() {
792+
let result = std::panic::catch_unwind(|| {
793+
let value_tree = deterministic_sequential_value_tree();
794+
795+
let (_, _transitions1, mut seen_counter) = value_tree.current();
796+
{
797+
let seen_counter = seen_counter.as_mut().unwrap();
798+
seen_counter.store(1, atomic::Ordering::SeqCst);
799+
}
800+
drop(seen_counter);
801+
802+
let _transitions2 = value_tree.current();
803+
})
804+
.expect_err("should panic");
805+
806+
let s = "Unexpected non-zero `seen_transitions_counter`";
807+
assert_eq!(result.downcast_ref::<&str>(), Some(&s));
808+
}
809+
663810
/// The following is a definition of an reference state machine used for the
664811
/// tests.
665812
mod heap_state_machine {
@@ -682,7 +829,7 @@ mod test {
682829

683830
pub type TestState = Vec<i32>;
684831

685-
#[derive(Clone, Debug)]
832+
#[derive(Clone, Debug, PartialEq)]
686833
pub enum TestTransition {
687834
PopNonEmpty,
688835
PopEmpty,
@@ -839,22 +986,23 @@ mod test {
839986
Config::default(), TestRng::from_seed(Default::default(), &seed));
840987
let result = runner.run(
841988
&FailIfLessThan::sequential_strategy(10..50_usize),
842-
|(ref_state, transitions)| {
989+
|(ref_state, transitions, seen_counter)| {
843990
Ok(FailIfLessThanTest::test_sequential(
844991
Default::default(),
845992
ref_state,
846993
transitions,
994+
seen_counter,
847995
))
848996
},
849997
);
850998
if let Err(TestError::Fail(
851999
_,
852-
(FailIfLessThan(limit), transitions),
1000+
(FailIfLessThan(limit), transitions, _seen_counter),
8531001
)) = result
8541002
{
8551003
assert_eq!(transitions.len(), 1, "The minimal failing case should be ");
8561004
assert_eq!(limit, MIN_TRANSITION + 1);
857-
assert!(transitions[0] < limit);
1005+
assert!(transitions.into_iter().next().unwrap() < limit);
8581006
} else {
8591007
prop_assume!(false,
8601008
"If the state machine doesn't fail as intended, we need a case that fails.");

proptest-state-machine/src/test_runner.rs

+17-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
//! Test declaration helpers and runners for abstract state machine testing.
1111
12+
use std::sync::atomic::{self, AtomicUsize};
13+
use std::sync::Arc;
14+
1215
use crate::strategy::ReferenceStateMachine;
13-
use proptest::std_facade::Vec;
1416
use proptest::test_runner::Config;
1517

1618
/// State machine test that relies on a reference state machine model
@@ -72,6 +74,7 @@ pub trait StateMachineTest {
7274
transitions: Vec<
7375
<Self::Reference as ReferenceStateMachine>::Transition,
7476
>,
77+
mut seen_counter: Option<Arc<AtomicUsize>>,
7578
) {
7679
#[cfg(feature = "std")]
7780
use proptest::test_runner::INFO_LOG;
@@ -91,6 +94,15 @@ pub trait StateMachineTest {
9194
Self::check_invariants(&concrete_state, &ref_state);
9295

9396
for (ix, transition) in transitions.into_iter().enumerate() {
97+
// The counter is `Some` only before shrinking. When it's `Some` it
98+
// must be incremented before every transition that's being applied
99+
// to inform the strategy that the transition has been applied for
100+
// the first step of its shrinking process which removes any unseen
101+
// transitions.
102+
if let Some(seen_counter) = seen_counter.as_mut() {
103+
seen_counter.fetch_add(1, atomic::Ordering::SeqCst);
104+
}
105+
94106
#[cfg(feature = "std")]
95107
if config.verbose >= INFO_LOG {
96108
eprintln!();
@@ -170,11 +182,11 @@ macro_rules! prop_state_machine {
170182
#![proptest_config($config)]
171183
$(#[$meta])*
172184
fn $test_name(
173-
(initial_state, transitions) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
185+
(initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
174186
) {
175187

176188
let config = $config.__sugar_to_owned();
177-
<$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(config, initial_state, transitions)
189+
<$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(config, initial_state, transitions, seen_counter)
178190
}
179191
}
180192
)*
@@ -189,10 +201,10 @@ macro_rules! prop_state_machine {
189201
::proptest::proptest! {
190202
$(#[$meta])*
191203
fn $test_name(
192-
(initial_state, transitions) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
204+
(initial_state, transitions, seen_counter) in <<$test $(< $( $ty_param ),+ >)? as $crate::StateMachineTest>::Reference as $crate::ReferenceStateMachine>::sequential_strategy($size)
193205
) {
194206
<$test $(::< $( $ty_param ),+ >)? as $crate::StateMachineTest>::test_sequential(
195-
::proptest::test_runner::Config::default(), initial_state, transitions)
207+
::proptest::test_runner::Config::default(), initial_state, transitions, seen_counter)
196208
}
197209
}
198210
)*

0 commit comments

Comments
 (0)