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

Implement test environments and before/after closures #30

Merged
merged 48 commits into from
Sep 1, 2017
Merged

Conversation

regexident
Copy link
Collaborator

@regexident regexident commented Aug 18, 2017

Hi @mackwic,

consider this the first in a series of PRs aiming to improve rspec, as layed out in tracking issue #29.

It implements a shared context/example environment with mutability isolation, which aims to enable sophisticated test scenarios as well as hopefully allowing us to safely implement concurrent test execution without having to worry about environment corruption, thanks to the power of Rust's borrow checker. (as such it aims to fix issue #7)

Further more it adds support for specifying multiple before_all (aka before), after_all (aka after) closures (with guarantees of preserved execution order) to be executed prior/posterior to execution of a context's context or example members. (as such it aims to fix issue #20)

Last but not least it adds support for specifying multiple before_each, after_each closures (with guarantees of preserved execution order) to be executed prior/posterior to execution of each of a context's context or example members. (as such it aims to fix issue #8)

Please note that during the Runner's complete overhaul I removed its unit tests at some point and haven't yet found the time to add them back. So consider this a WIP, I guess?

Anyway, I'd love to hear your feedback.

Ps: I also upped the Rust version on TravisCI from 1.9 to 1.18, for the sake of being able to use pub(crate) and other neat things. I hope that's fine with you?

Pps: Also expect the next PRs to be more finely grained. 😉

Cargo.toml Outdated
name = "rspec"
readme = "README.md"
repository = "https://github.com/mackwic/rspec"
version = "1.0.0-beta.3"
Copy link
Member

Choose a reason for hiding this comment

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

this would be a .4

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Followed by hopefully quite a bunch of betas and a final RC, yeah.

Cargo.toml Outdated

# [dependencies.expectest]
# optional = true
# version = "0.8.0"
Copy link
Member

Choose a reason for hiding this comment

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

remove dead code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, this would have to be bumped to 0.9.1 and the other git branch one removed.

Cargo.toml Outdated
branch = "consistency"

# [dev-dependencies]
# expectest = "0.8.0"
Copy link
Member

Choose a reason for hiding this comment

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

dead code to remove

Cargo.toml Outdated

[dev-dependencies.expectest]
git = "https://github.com/regexident/expectest"
branch = "consistency"
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't need that

Cargo.toml Outdated
[dependencies.expectest]
optional = true
git = "https://github.com/regexident/expectest"
branch = "consistency"
Copy link
Member

Choose a reason for hiding this comment

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

I believe your PR has been merged in the v0.9.1 ? (congrats btw)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. expectest + rspec are a beautiful match, imho. 😉


impl Failure {
pub fn new<S>(message: Option<S>) -> Self
where String: From<S>
Copy link
Member

Choose a reason for hiding this comment

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

Where String implements From<S> ? Why not S : ToString ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure. To be honest I didn't have ToString on my screen at all, when I wrote this. 😇

Copy link
Member

Choose a reason for hiding this comment

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

To be frank, it's not the same. There is no blanket impl String : From<T: ToString> so it you depends on this bound, it will break something.

Where do you use this ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is used for Failure creation from either a str (literal) or a String (format!(…)).

}

// #[derive(Clone, PartialEq, Eq, Debug)]
// pub struct ExampleReport(Result<(), String>);
Copy link
Member

Choose a reason for hiding this comment

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

dead code to remove

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup.

if result.is_err() && !self.name_stack.is_empty() {
let failure_name = self.name_stack.join(" | ");
self.failures.push(failure_name);
fn exit_suite(&mut self, report: &ContextReport) {
Copy link
Member

Choose a reason for hiding this comment

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

too much logic here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have a second PR almost ready however which completely parallelizes the entire test suite execution (configurable) via rayon. It completely messes up the console log for obvious reasons. With this in mind I'd suggest to just ignore this one for now when deciding when and how to merge this PR, as I plan to rework this part (and add unit tests) in the very next PR to follow. But for the sake of clarity I'd rather not any further major refactors to this PR.

The next PR also turns the entire suite/context/example hierarchy immutable and makes the runner execution immutable (as in …(&self, …), plus some Arc<Mutex<…>> on the EventHandlers) as well. This whole immutability thing is what enables the fearless concurrency cough to begin with.

Copy link
Member

Choose a reason for hiding this comment

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

As I said, I am OK with a complete big PR. Sometimes refactoring are large and it's OK.

Why don't you push it all here, and we work the rest together ?
At least please place block comments that signal where the moving code is, but I suspect a change in type might break your next PR...

src/runner.rs Outdated
pub fn run(mut self) -> ContextReport {
let suite = mem::replace(&mut self.suite, None).expect("Expected context");
panic::set_hook(Box::new(|_panic_info| {
// silently swallows panics
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 fail the test, no ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean the // silently swallows panics? Oh that just prevents the panic from getting printed to the console.

It's this that does the actual fail catching.

Copy link
Member

@mackwic mackwic Aug 19, 2017

Choose a reason for hiding this comment

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

Oh ! Okay, so then maybe change the comment to: // XXX panics already catched at the test call site, don't output the trace in stdout or something equivalent ?

( I like the convention XXX == weird behavior if you are new to the code base)

src/runner.rs Outdated
result = test_res.or(result);
pub fn run_or_exit(self) {
if self.run().failed > 0 {
::std::process::exit(101);
Copy link
Member

Choose a reason for hiding this comment

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

why 101 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I basically checked what $ cargo test returns for a failing test (via $ echo $?). Turns out it's 101.

https://www.reddit.com/r/rust/comments/3qdvez/guarantees_about_exit_codes/cwf46cb/

Copy link
Member

@mackwic mackwic Aug 19, 2017

Choose a reason for hiding this comment

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

Excellent ! I suggest you put this link in comment just before the process::exit line. Something like that:

   pub fn run_or_exit(self) {
        if self.run().failed > 0 {
             // XXX Cargo test failure returns 101. Ref: https://www.reddit.com/r/rust/comments/3qdvez/guarantees_about_exit_codes/cwf46cb/
            ::std::process::exit(101);

@mackwic
Copy link
Member

mackwic commented Aug 18, 2017

There is tremendous work here, I can see it. I it is impressing. And I quite like the look of it !

this was a quick look at the code, I need to load it in my IDE and play with it a bit to suggest modifications. This weekend I'll make better remarks.

@regexident
Copy link
Collaborator Author

There is tremendous work here, I can see it. I it is impressing. And I quite like the look of it !

Awesome, thanks!

this was a quick look at the code, I need to load it in my IDE and play with it a bit to suggest modifications. This weekend I'll make better remarks.

Cool. As already mentioned in one of the comments I have another PR in the works (as a follow-up for this one) that turns the whole thing into a parallel executed test Runner that walks over a strictly immutable hierarchy of suites/contexts/examples with the only mutable thing being the environment that is being passed around safely and with strict mutability isolation per context.

For all this I need to completely overhaul the formatter. And while I'm doing that I also plan to add back the unit test that I had to remove from runner and formatter.

src/runner.rs Outdated
// silently swallows panics
}));
self.visit(&suite);
let _ = panic::take_hook();
Copy link
Member

Choose a reason for hiding this comment

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

this is not clear and missed it at first review. Add a comment // XXX unregister panic hook ?

@regexident
Copy link
Collaborator Author

I just pushed the WIP that makes the entire test execution immutable (with exception of the environment that's getting passed around and forked).

It further more adds support for using a single runner with multiple suites, and removes lots of debris from the past, such as unused explicit lifetimes and mut markers.

The formatter's output breaks when run in parallel mode with Configuration::default() (i.e. parallel = true). I hope to find time tomorrow to tackle this by overhauling the hole formatting thing.

Please feel free to point out any other parts that still need some work to get merged.

@regexident regexident mentioned this pull request Aug 20, 2017
}

unsafe impl<T> Send for ContextMember<T> where T: Send {}
unsafe impl<T> Sync for ContextMember<T> where T: Sync {}
Copy link
Member

Choose a reason for hiding this comment

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

these two needs XXX comment on why they are here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Necessary for parallelization. Good point, will document.

src/example.rs Outdated
}

unsafe impl<T> Send for Example<T> where T: Send {}
unsafe impl<T> Sync for Example<T> where T: Sync {}
Copy link
Member

Choose a reason for hiding this comment

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

same here, need an explanation in comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Necessary for parallelization. Good point, will document.

src/runner.rs Outdated
self.handlers.broadcast(&Event::FinishedRunner(result));
result
impl Runner {
pub fn run<T>(self, suite: (Suite<T>, T)) -> SuiteReport
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand the difference between run and run_with

Copy link
Member

Choose a reason for hiding this comment

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

(same for run_and_exit and its run_and_exit_with counterpart)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One takes an explicit configuration, one uses the implicit default.

However given that test initiation has become quite cumbersome with the latest changes it doesn't make much a change, I guess. Maybe just drop the default config one and remove the _with from the explicitly configured variant.

My plan is to at some point (i.e. once the overall architecture has settled) re-introduce some API akin to rdescribe(…), which would wrap all this:

let simple = rspec::formatter::Simple::new(io::stdout());
let formatter = Arc::new(Mutex::new(simple));
let configuration = Configuration::default(); // .parallel(false);
let runner = Runner::new(configuration, vec![formatter]);

#[derive(Clone, Debug)]
struct Environment {
    // …
}

let environment = Environment {
    // …
};

runner.run_or_exit(rspec::given("a BTreeSet", environment, |ctx| {
    // …
}));

into something more palatable, like this:

#[derive(Clone, Debug, Rspec)]
struct Environment {
    // …
}

rspec::run_or_exit(rspec::given("a BTreeSet", Environment {
    // …
}, |ctx| {
    // …
}));

@mackwic
Copy link
Member

mackwic commented Aug 20, 2017

Are you OK if I push a Rustfmt commit on this branch ?

For the next changes modifications, I'd like to make PRs on this PR.

@regexident
Copy link
Collaborator Author

Are you OK if I push a Rustfmt commit on this branch ?

Oh please, do. 🙂

For the next changes modifications, I'd like to make PRs on this PR.

Fine with me. I don't plan to do any major changes on this one, other than cleanups.
Unless you want me to push the reworked Formatter (to make it work with parallel test runs) here as well, before merging. Up to you. 🙂

I'm currently looking into the concurrent formatter thing.

@mackwic
Copy link
Member

mackwic commented Aug 20, 2017

Great !

@regexident
Copy link
Collaborator Author

So, anything in particular you'd like me to fix prior to merging?

@mackwic
Copy link
Member

mackwic commented Aug 20, 2017

Yes, there is a lot of work to be done in this PR before being ready to merge. Documentation, tests, examples are the obvious one. I also saw some bigger changes but I am not sure I understand how it could break the rest (like duplication). I am currently playing with it in my IDE to test changes.

When it's a little thing I can put a comment. But when it's a big change, as I already tested it in my IDE, I make a change proposal via external PRs (eg: #31 and #32 ), all dependants PRs needs to be merged before merging this one. It's like a comment on your diff, but I already tested the code.

The goal is to keep the debate contained in little changes and improve progressively. You can disagree on my proposal (it's fine !), or have better ideas on how to do it. When we agree, we merge, we move forward, and we have better code. 👌

Is this fine by you ?

@regexident
Copy link
Collaborator Author

Yes, there is a lot of work to be done in this PR before being ready to merge. Documentation, tests, examples are the obvious one. I also saw some bigger changes but I am not sure I understand how it could break the rest (like duplication). I am currently playing with it in my IDE to test changes.

Good point. I will add a couple more examples and extend the existing documentation.

When it's a little thing I can put a comment. But when it's a big change, as I already tested it in my IDE, I make a change proposal via external PRs (eg: #31 and #32 ), all dependants PRs needs to be merged before merging this one. It's like a comment on your diff, but I already tested the code.

Cool.

The goal is to keep the debate contained in little changes and improve progressively. You can disagree on my proposal (it's fine !), or have better ideas on how to do it. When we agree, we merge, we move forward, and we have better code. 👌

👍

Is this fine by you ?

Absolutely!

fn it_can_be_called() {
// arrange
let runner = Runner::default();
let block = Block::Example(Example::new(ExampleHeader::default(), |_| ExampleReport::Success));
Copy link
Member

Choose a reason for hiding this comment

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

seems like a bug to me (or at least a regression).
This wont compile:

let block = Block::Example(Example::new(ExampleHeader::default(), |_| ()));

It should !

Copy link
Member

@mackwic mackwic Aug 27, 2017

Choose a reason for hiding this comment

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

Same for false:

/Users/thomas/.cargo/bin/cargo test --color=always --package rspec --lib runner::tests::impl_visitor_block_for_runner -- --nocapture
   Compiling rspec v1.0.0-beta.4 (file:///Users/thomas/projects/rust/rspec)
error[E0308]: mismatched types
   --> src/runner/mod.rs:607:83
    |
607 |             let block = Block::Example(Example::new(ExampleHeader::default(), |_| false));
    |                                                                                   ^^^^^ expected enum `report::example::ExampleReport`, found bool
    |
    = note: expected type `report::example::ExampleReport`
               found type `bool`

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, this is correct. Example::new(…) takes a closure which handles the conversion from ()/bool/… to ExampleReport. One could use F: 'static + Fn(&T) -> impl Into<ExampleReport> instead of F: 'static + Fn(&T) -> ExampleReport in future, but that's not available yet. Also Example::new(…) is not intended for public use anyway. That's what ctx.then(…) is for.

Copy link
Member

Choose a reason for hiding this comment

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

okay I see

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

However if you see a way to make this cleaner I'm all for it. :)

@regexident
Copy link
Collaborator Author

regexident commented Aug 27, 2017

Some theoretical remarks:

Lemme take the opportunity to thank you for your great and detailed comments! They helped a lot! At some point it can become difficult to see the forest for the trees.

  • Runner is a pure dispatcher (and this is great !), it doesn't have state per se

Yeah, this was one of the main goals of my overall concept. Suite/Context/Example are pure, too.

  • So we only need to test the order of the dispatch and the eventual logic behind (like conditional dispatch)

Yes.

  • I believe your test do too many things at once. I find that simpler, little focused tests seems easier to write, maintain, and read IMO

True. Your tests look good! 👍

  • It's cool the API is generic, I like doing impl for testing needs and inject my little stubs :)

😎

I am impressed by the overall design. It's good, really.

I'm glad you like it!

I already have timing implemented (yet will have to rebase it and probably rewrite some parts of it now). And filtering/focussing shouldn't be too difficult to implement now either.

@regexident
Copy link
Collaborator Author

With 97cab2b Logger has now reached a state where I'm happy with both implementation and API use.

That being said I plan to add realtime progress for parallel execution via the indicatif crate with a later, separate PR.


What is your take on the progress of this PR? What's still missing? What should I fix?

@mackwic
Copy link
Member

mackwic commented Aug 31, 2017

LGTM ! I think this PR is now in an acceptable state :)

@regexident
Copy link
Collaborator Author

regexident commented Aug 31, 2017

Great! Feel free to rebase and merge it at your discretion then.

Thanks for the ride, @mackwic! 😎

@mackwic mackwic merged commit f543f9f into master Sep 1, 2017
@mackwic mackwic deleted the environment branch September 1, 2017 08:58
@mackwic
Copy link
Member

mackwic commented Sep 1, 2017

🎉

@regexident
Copy link
Collaborator Author

Cool, but sure you want this massive PR to be squashed into a single commit?

I think for a massive change of API/implementation like this it would be more than useful to have a more finely-grained log of changes, no? It would also give a better picture of the activity of this project, which with a single commit might give the false impression of still being in some kind of stagnation.

@mackwic
Copy link
Member

mackwic commented Sep 1, 2017

I don't want to optimize by the number of commits. It's true, some people look at it but it's a meaningless metric.

I want this PR to be squashed so that we can't checkout an intermediate state from master.

@regexident
Copy link
Collaborator Author

I don't want to optimize by the number of commits. It's true, some people look at it but it's a meaningless metric.

Fair point. I personally don't care either.

I want this PR to be squashed so that we can't checkout an intermediate state from master.

Ah I see, this makes sense.

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.

3 participants