rspec/rspec-expectations — pull request #567 by myronmarston (2014-06-08 05:49:38+00:00)
Title: Compound block expectations

This is a spike for #552. Feedback wanted.

The basic solution here is pretty straightforward:

* For one matcher (let's call it the "outer" matcher), rather than passing it the actual block, pass it a new block that performs matching against the other matcher (call it the "inner" matcher) using the actual block.
* This causes the real block to be executed when the outer matcher calls our intermediate block, making things work properly.

This straightforward approach didn't work for some of our block matchers, though:

* `raise_error` and `throw_symbol` cannot be the outer matcher, because when an error is raised or a symbol is thrown it jumps out of the inner matcher's `matches?` (since it doesn't expect a raise or throw). They must be the inner matcher, regardless of what order the matchers were chained in. To make this work, I added a new method to the matcher protocol, `block_can_be_wrapped_for_compound_expression?`. We use this to figure out if a particular matcher must always be used as the inner matcher.
* The `yield_xyz` matchers pass an argument (the yield probe) to the `expect` block, which gets a little hairy when the yield matcher is the outer matcher. The solution I came up with is to wrap it in an extra proc that receives an forwards the args on to the inner proc.

There are also some open questions:

* I really hate the `block_can_be_wrapped_for_compound_expression?` method name but couldn't come up with a better name for it. Any ideas? Also, when the method is not defined, it defaults to `true` (as the other block matchers allow the block wrapping), but it feels inverted to me to have the optional method be treated as `true` if not defined. We should find a wording that would allow us to default it to `false` but I can't think of a good method name for that. Alternately, we could assume all block matchers can't be wrapped and opt-in to the ones that can (`change`, `yield_xyz`, `output`).
* How should it behave when two matchers are used that both pass args to the block? My solution for the `yield` matcher will only work when there's not another one like this.

TODO:
- [ ] The coverage for the `or` case isn't as fleshed out as the `and` case -- flesh it out.
- [ ] Provide clear errors for the cases we won't support (such as two `raise_error` matchers).
- [ ] For whatever additions to the protocol we make, document them and add support to the custom matcher DSL for them.
- [ ] Get the build green :).