Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] State machine support #84

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

[WIP] State machine support #84

wants to merge 1 commit into from

Conversation

AltSysrq
Copy link
Collaborator

Something I've been working on here and there for a while, originally suggested in #28.

As noted in the first commit, this absolutely needs more tests and is probably still a bit deficient on documentation. I wanted to get this up now though to solicit opinions on the model/ergonomics now that the implementation is basically functional.

I'd recommend starting by reading the (still incomplete) tutorial and then the syntax docs above it.

@Centril I'd appreciate your thoughts when you have time.

Still needs more documentation and definitely more tests.
Copy link
Collaborator

@Centril Centril left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work :) Here are some initial thoughts.

@@ -92,7 +92,7 @@ impl<T : fmt::Debug> fmt::Display for TestError<T> {
TestError::Abort(ref why) =>
write!(f, "Test aborted: {}", why),
TestError::Fail(ref why, ref what) =>
write!(f, "Test failed: {}; minimal failing input: {:?}",
write!(f, "Test failed: {}; minimal failing input: {:#?}",
Copy link
Collaborator

@Centril Centril Aug 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm; this is a non-trivial change in proptest wrt. UX;
While it will it will help for complex structures (because they would have overflowed the screen horizontally and so the terminal would have line-split...) I think it will also be less great for simple structs and tuples.

#[macro_use]
extern crate proptest;

use std::cmp;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason to do cmp::Ord? The trait should be well know so Ord seems simpler.

// We define a strategy for our starting state. Here, we always start with
// an empty heap.
fn initial_heap_strategy<T : cmp::Ord + fmt::Debug + Clone>
() -> impl Strategy<Value = MyHeapState<T>>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably stick to a consistent style wrt. where or direct bounds?
Also, running rustfmt after merging the PR would be good (on the whole of proptest).

fn push(&mut state, value: T) {
// Add it to the heap...
state.heap.push(value.clone());
// ... and also the Vec where we track what the heap contains.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/track what/track of what/

fn pop(&mut state) {
let was_empty = state.heap.is_empty();

match state.heap.pop() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps use if let here to reduce rightward drift?

/// the value.
pub fn new(mutation_strategy: impl Strategy<Value = Box<dyn Mutator<S>>> + 'static,
invariants: Arc<dyn Fn (&mut S) -> TestCaseResult>) -> Self {
StateMachine {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self { .. }

pub fn run(mut self) -> Result<S, TestCaseError> {
if self.trace {
println!("initial state: {:?}", self.state);
println!("check initial invariants");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought for later: It might be nice to change from using println to use some more principled logging facility that users can control. I think the quickcheck crate does this. But this is out of scope for this PR.

/// Mutate `state`.
///
/// This effectively consumes `self`, but it is passed as a mutable
/// reference so that this trait is object-safe and so that the mutator can
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test that asserts object safety.

function: Option<F>,
arguments: Option<A>,
formatted_arguments: Option<Cow<'static, str>>,
name: &'static str,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields are in common from the previous struct... Reuse?

let args = self.arguments.take().expect("Mutator applied more than once");
if self.formatted_arguments.is_none() {
self.formatted_arguments = Some(Cow::Owned(format!("{:?}", args)));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit is identical from DirectMutator; refactor?

@Centril
Copy link
Collaborator

Centril commented Aug 22, 2018

Additional thoughts:

  • The base of this is quite nice.

  • I wonder if sugar should be added for preconditions of mutators (they can easily be added as ifs in each mutator but sugar could be nice...). This would model the fact that in state machines, after visiting some state nodes, a state machine may enter a phase which causes it to never enter those previous state nodes.

  • Sugar for post conditions could also be nice. You could add optional require and ensure clauses to each mutator to do this and the previous point.

  • Is "mutation" the right word? Other words to consider "command", "action". Tho perhaps "mutation" is the rustic word :)

  • The major question for me is whether and how this can be used for tests involving concurrent situations such as async or involving mutexes and so on... In particular, can we do the things in http://scalacheck.org/files/scaladays2014/#27 and the following slides?

    In particular, there's the question of symbolic vs. dynamic execution (see https://medium.com/@jlouis666/breaking-erlang-maps-2-362730a91400 for a discussion). In the blog post, they note that they generate the command sequence (and I assume that would include delegated bits too) before invoking any of the commands.

    From reading the code in this PR I'm not 100% sure what is done wrt. symbolic / dynamic execution, but I think that the command structure, including delegates are generated before any mutations actually occur (which is good). Could you say a few words about this? I think it would be good to include in the documentation or at least the code a bit about the high level logic of how the execution model works possibly including some diagrams / pictures or ascii art as in the medium blog post.

    I think it would be good to have some test cases for concurrent situations.

@Eh2406
Copy link

Eh2406 commented Jun 14, 2019

I was working on a data structure in Cargos internals and remembered this work. How is it going? What are the next steps?

@AltSysrq
Copy link
Collaborator Author

To be honest, I haven't really done anything here since I made the PR. IIRC I ran into some fundamental flaw in the way this is designed and put it on the backburner, but I've forgotten what that was now.

@zmitchell
Copy link

I'd be interested in continuing work on this if someone is able to mentor me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants