Skip to content

Commit c1bf635

Browse files
authored
Merge pull request #418 from timstobal/recomplicate-initial
Fix re-complication of initial state
2 parents a16f6c3 + 8f1b35e commit c1bf635

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

proptest-state-machine/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- Removed the limit of number of transitions that can be deleted in shrinking that depended on the number the of transitions given to `prop_state_machine!` or `ReferenceStateMachine::sequential_strategy`.
1010
- Fixed state-machine macro's inability to handle missing config
1111
- Fixed logging of state machine transitions to be enabled when verbose config is >= 1. The "std" feature is added to proptest-state-machine as a default feature that allows to switch the logging off in non-std env.
12+
- Fixed an issue where after simplification of the initial state causes the test to succeed, the initial state would not be re-complicated - causing the test to report a succeeding input as the simplest failing input.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 2e5fb0b2e70b08bd8a567bf4d9ae66ee2478d832d6bdfb9670f5b98203e78178 # shrinks to seed = [120, 233, 214, 148, 193, 171, 178, 64, 157, 62, 78, 165, 215, 79, 177, 175, 171, 202, 51, 93, 79, 238, 39, 104, 174, 79, 152, 255, 45, 174, 27, 168]
8+
cc 39927f23e5f67ac32c5219c226c49d87e3e3c995a73cd969d72fbcdf52ac895b # shrinks to seed = [0, 10, 244, 19, 249, 125, 161, 150, 61, 56, 77, 245, 12, 228, 187, 180, 148, 61, 17, 32, 189, 118, 70, 47, 147, 210, 94, 127, 210, 23, 128, 75]

proptest-state-machine/src/strategy.rs

+111-1
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ impl<
357357
// Store the valid initial state
358358
self.last_valid_initial_state =
359359
self.initial_state.current();
360+
self.last_shrink = Some(self.shrink);
360361
return true;
361362
} else {
362363
// If the shrink is not acceptable, clear it out
@@ -533,7 +534,6 @@ impl<
533534
false
534535
}
535536
Some(InitialState) => {
536-
self.last_shrink = None;
537537
if self.initial_state.complicate()
538538
&& self.check_acceptable(None)
539539
{
@@ -752,4 +752,114 @@ mod test {
752752
}
753753
}
754754
}
755+
756+
/// A tests that verifies that the strategy finds a simplest failing case, and
757+
/// that this simplest failing case is ultimately reported by the test runner,
758+
/// as opposed to reporting input that actually passes the test.
759+
///
760+
/// This module defines a state machine test that is designed to fail.
761+
/// The reference state machine consists of a lower bound the acceptable value
762+
/// of a transition. And the test fails if an unacceptably low transition
763+
/// value is observed, given the reference state's limit.
764+
///
765+
/// This intentionally-failing state machine test is then run inside a proptest
766+
/// to verify that it reports a simplest failing input when it fails.
767+
mod find_simplest_failure {
768+
use proptest::prelude::*;
769+
use proptest::strategy::BoxedStrategy;
770+
use proptest::test_runner::TestRng;
771+
use proptest::{
772+
collection,
773+
strategy::Strategy,
774+
test_runner::{Config, TestError, TestRunner},
775+
};
776+
777+
use crate::{ReferenceStateMachine, StateMachineTest};
778+
779+
const MIN_TRANSITION: u32 = 10;
780+
const MAX_TRANSITION: u32 = 20;
781+
782+
const MIN_LIMIT: u32 = 2;
783+
const MAX_LIMIT: u32 = 50;
784+
785+
#[derive(Debug, Default, Clone)]
786+
struct FailIfLessThan(u32);
787+
impl ReferenceStateMachine for FailIfLessThan {
788+
type State = Self;
789+
type Transition = u32;
790+
791+
fn init_state() -> BoxedStrategy<Self> {
792+
(MIN_LIMIT..MAX_LIMIT).prop_map(FailIfLessThan).boxed()
793+
}
794+
795+
fn transitions(_: &Self::State) -> BoxedStrategy<u32> {
796+
(MIN_TRANSITION..MAX_TRANSITION).boxed()
797+
}
798+
799+
fn apply(state: Self::State, _: &Self::Transition) -> Self::State {
800+
state
801+
}
802+
}
803+
804+
/// Defines a test that is intended to fail, so that we can inspect the
805+
/// failing input.
806+
struct FailIfLessThanTest;
807+
impl StateMachineTest for FailIfLessThanTest {
808+
type SystemUnderTest = ();
809+
type Reference = FailIfLessThan;
810+
811+
fn init_test(ref_state: &FailIfLessThan) {
812+
println!();
813+
println!("starting {ref_state:?}");
814+
}
815+
816+
fn apply(
817+
(): Self::SystemUnderTest,
818+
ref_state: &FailIfLessThan,
819+
transition: u32,
820+
) -> Self::SystemUnderTest {
821+
// Fail on any transition that is less than the ref state's limit.
822+
let FailIfLessThan(limit) = ref_state;
823+
println!("{transition} < {}?", limit);
824+
if transition < ref_state.0 {
825+
panic!("{transition} < {}", limit);
826+
}
827+
}
828+
}
829+
830+
proptest! {
831+
#[test]
832+
fn test_returns_simplest_failure(
833+
seed in collection::vec(any::<u8>(), 32).no_shrink()) {
834+
835+
// We need to explicitly run create a runner so that we can
836+
// inspect the output, and determine if it does return an input that
837+
// should fail, and is minimal.
838+
let mut runner = TestRunner::new_with_rng(
839+
Config::default(), TestRng::from_seed(Default::default(), &seed));
840+
let result = runner.run(
841+
&FailIfLessThan::sequential_strategy(10..50_usize),
842+
|(ref_state, transitions)| {
843+
Ok(FailIfLessThanTest::test_sequential(
844+
Default::default(),
845+
ref_state,
846+
transitions,
847+
))
848+
},
849+
);
850+
if let Err(TestError::Fail(
851+
_,
852+
(FailIfLessThan(limit), transitions),
853+
)) = result
854+
{
855+
assert_eq!(transitions.len(), 1, "The minimal failing case should be ");
856+
assert_eq!(limit, MIN_TRANSITION + 1);
857+
assert!(transitions[0] < limit);
858+
} else {
859+
prop_assume!(false,
860+
"If the state machine doesn't fail as intended, we need a case that fails.");
861+
}
862+
}
863+
}
864+
}
755865
}

0 commit comments

Comments
 (0)