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

Runner for event streams #44

Merged
merged 20 commits into from
Jul 27, 2021
Merged

Runner for event streams #44

merged 20 commits into from
Jul 27, 2021

Conversation

andrey-kuprianov
Copy link
Contributor

This PR replaces the previous Runner which was based on Steps, with a new Runner that is based on Event streams.

The intention is to create a unified interface (event streams) that can be used both for unit-testing the implementation, as well as for model-based testing.

@andrey-kuprianov
Copy link
Contributor Author

@vitorenesduarte , @romac , would be grateful to hear your comments on this.

@vitorenesduarte , this PR removes the previous TestRunner completely and I am not sure about that. What do you think, does it still make sense to keep it?

@romac
Copy link
Member

romac commented Apr 26, 2021

Very nice, I like the little EventStream DSL!

There's one thing, where I am not quite sure about using Default to initialize the system. What if you want to initialize the same System multiple times with different parameters? With Default you have to introduce a new type for each system and implement Default for it, whereas if one could just supply a system you could handle any variations of initial system (maybe even dynamically) without introducing new types.

@andrey-kuprianov
Copy link
Contributor Author

Glad you like the DSL, Romain!

In fact, originally it was that the system has to be supplied as one of the parameters; I've changed it to Default in f00acfe, because this made the usage of Runner shorter/simpler. I could revert this back; or, alternatively, provide an additional method, e.g. run_on_system.

@@ -288,12 +288,14 @@ impl<'a, System> Runner<'a, System> {
&mut self,
system: &mut System,
stream: &mut dyn Iterator<Item = Event>,
Copy link
Member

Choose a reason for hiding this comment

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

This should also work without the need for a trait object (dyn Trait):

Suggested change
stream: &mut dyn Iterator<Item = Event>,
stream: impl Iterator<Item = Event>,

@romac
Copy link
Member

romac commented Apr 27, 2021

I could revert this back; or, alternatively, provide an additional method, e.g. run_on_system.

The way you addressed this in 66211b3 looks good to me :)

@romac
Copy link
Member

romac commented Apr 27, 2021

If you think it's useful, you can always add a run_with_default method which requires the Default bound and uses the default value of the system.

},

/// A error that occurs when a test fails.
#[error("Test failed: {message}\n {location}")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, should we use testand system in this error message?

/// A `modelator` [enum@Error].
#[error("Error while running modelator: {0}")]
Modelator(Error),

/// A error that occurs when a test fails.
#[error("Test failed on step {step_index}/{step_count}:\nsteps: {steps:#?}")]
#[error("Unhandled test: {test}")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use the test field in this error message?

@@ -45,11 +45,11 @@ pub enum Event {
/// Process the abstract action, modifying the system state.
Action(Box<dyn Any>),
/// Expect the provided outcome of the last action.
Outcome(String),
Expect(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if these should be called TestSteps instead of Events. It wasn't immediately obvious to me what an Event was.

/// Check the assertion about the abstract system state.
Check(Box<dyn Any>),
/// Expect exactly the provided abstract system state.
Expect(Box<dyn Any>),
Equal(Box<dyn Any>),
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, this Event::Equal is the reason why we need StateHandler::read right?

I'm thinking that implementing StateHandler::read can be a lot of effort (and some users may not even need it). Not only users have to provide a way to go from an abstract state to a concrete one (the bare minimum), now they also need to provide the opposite transformation. Moreover, now users have to implement two traits, StateHandler and ActionHandler.

Overall, I would be in favor of simplifying things (probably because I don't understand why we had to complicate them in the first place): I would have a single trait that only has two methods, init and handle. This would already provide basic functionality. Then, we should also support more complicated setups for which this is not enough, but not at the cost of complicating the rest.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just realized that what advocating for is very similar to the initial approach we had in the TestRunner:

pub trait TestRunner<S> {
/// Executes the first step against the runner.
fn initial_step(&mut self, step: S) -> bool;
/// Executes each next step against the runner.
fn next_step(&mut self, step: S) -> bool;
}

@andrey-kuprianov
Copy link
Contributor Author

andrey-kuprianov commented Apr 28, 2021

Ha-ha, I am actually already in the process of bringing back the TestRunner :)

I also came to the conclusion that it's better to keep them both -- the runner for events, and the more simple variant as you've had it originally.

Now, why we need a more complex one, not all but a couple of reasons:

  • in the simple TestRunner the users have to process the states and actions themselves. In particular the actions is a headache.
  • on the contrary, in the more complex variant, the users need to implement state and action handlers, which they can easily reuse for unit testing. So we provide a nice way to construct unit tests, and the users get MBT for free on top. The alternative is that they need to write a fairly complicated logic themselves each time specifically for MBT.

Just to make it a bit more example-based: compare the hand-written state-checking logic here, which is also incomplete, with the transparent transformations from concrete to abstract states for ICS02 and ICS03.

I hope the difference speaks for itself.

@andrey-kuprianov
Copy link
Contributor Author

Another reason, which I forgot to mention, and which is purely software engineering one: the TestRunner core functionality is too "thin": it does too little to justify it's existence. And exactly because it doesn't do enough of the useful things, this means it pushes them to the users.

@andrey-kuprianov
Copy link
Contributor Author

This is really annoying: the errors when running cargo test for this PR stem not from the errors in the code, but from the interactions between different tests, and also between Apalache and TLC via mc.log. We can't progress without fixing #43 and #46

Copy link
Member

@rnbguy rnbguy left a comment

Choose a reason for hiding this comment

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

The commits look good. I added some small comments. But IMHO some internal codes need some refactoring maybe. Not just because of Rust idioms, but to simplify the API. But that should be a future milestone. I really like the Runner API.

outcome: String,
}

impl<'a, System> Default for Runner<'a, System> {
impl<System: Debug> Default for EventRunner<System> {
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove these redundant Default impls and ::new() fns. Instead derive Default maybe?

/// assert!(run_tla_steps(tla_tests_file, tla_config_file, &options, &mut system).is_ok());
/// }
/// ```
pub fn run_tla_steps<P, System, Step>(
Copy link
Member

Choose a reason for hiding this comment

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

Can we make run_tla_steps and run_tla_events convenient struct methods. The function bodies are very small and very direct anyway. Maybe something like this,

Setup::default()
  .set_options(&Options)
  .config_file(P)
  .tests_file(P)
  .run_tla_steps_on(&mut System)

and something similar for run_tla_events.

/// Self::is_even(step.b)
/// // We define StateHandlers that are able to initialize your SUT from
/// // these abstract states, as well as to read them at any point in time.
/// impl StateHandler<A> for NumberSystem {
Copy link
Member

Choose a reason for hiding this comment

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

Should we use the power of AsRef or Deref instead?

}
}

impl ActionHandler<Action> for NumberSystem {
Copy link
Member

Choose a reason for hiding this comment

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

Why Action is generic type and Outcome is associated type? Action does not have trait bounds. We can make Action as an associated type too, no?


#[test]
fn event_runner() {
let events = EventStream::new()
Copy link
Member

Choose a reason for hiding this comment

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

haha this is the best example !! so simple and short 👍

@rnbguy
Copy link
Member

rnbguy commented Jul 26, 2021

By the way, from what I understood, run_tla_steps and run_tla_events seem to have the same power. Is there anything fundamentally different about them?

@rnbguy
Copy link
Member

rnbguy commented Jul 27, 2021

Also, I saw some 'static and DeserializeOwned bounds here and there. Are these lifetimes sanitized? If it is not done yet, I can take a look later. It would remove unnecessary lifetime bounds.

@andrey-kuprianov
Copy link
Contributor Author

By the way, from what I understood, run_tla_steps and run_tla_events seem to have the same power. Is there anything fundamentally different about them?

Well, fundamentally everything is the same;) It's the question of convenience. The first one is about StepRunner (I now think that StateRunner is a better name), and such API is probably better for small specs, where you only want an access to the state of a TLA+ model "as is". The second one is about EventRunner, where some parts of TLA+ state have predefined meaning, like action or actionOutcome (and this should be documented for sure!)

@andrey-kuprianov
Copy link
Contributor Author

As all suggestions seem to be about refactoring and API improvements, I have pulled them in a separate issue #50; to be addressed in the next weeks.

@rnbguy thanks for the review!

@andrey-kuprianov andrey-kuprianov merged commit 748ea8e into main Jul 27, 2021
@danwt danwt deleted the andrey/new-runner branch January 18, 2022 14:14
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.

4 participants