Support composable matchers #393

Merged
merged 57 commits into from Jan 2, 2014

Projects

None yet

3 participants

@myronmarston
Member

This is still a bit WIP but it's much further along than the last WIP PR and I wanted to open the PR and start the code review process. I applied the things discussed with @xaviershay and @JonRowe from #388.

Still TODO:

  • The rbx build is failing for some reason. Figure out why.
  • Finish defining all the matcher aliases.
  • Figure out how to easily document the aliases in the YARD docs so it makes it clear they are aliases.
  • Update the README with examples showing how to compose matchers
  • Update the YARD doc comments above the RSpec::Matchers module that discusses the matcher protocol so that it mentions how the composing works (e.g. the use of ===).
  • Add a cuke demonstrating this.
  • Given that many matchers do not support passing matchers as args (e.g. because the semantics would not make sense), should we explicitly document which matchers support it? (Note that all matchers will support being passed as an arg to another matcher -- it's just that some matchers aren't designed to receive a matcher as an arg).
  • Consider updating the match matcher to be composable.
  • Figure out a better algorithm for the match_array matcher to get the pending spec to pass. Edit: I think a stable matching algorithm may work here.
  • Ensure diff output, if used, looks good with matchers (example: failing match(nested_structure))

The last item in particular is stumping me -- I need help on that one! See the pending spec I left in match_array_spec.rb for what I'm talking about.

Even though I have a lot of TODOs left, feedback is welcome whenever anyone wants to give it. I'll be sure to comment here when I everything I'm planning to do is complete.

/cc @xaviershay @JonRowe @soulcutter @samphippen @alindeman

Closes #280.

@xaviershay
Member

For the match_array problem, one option would be to brute force permutation and pick the "best" match.

https://coderpad.io/078048

Sorry don't have enough time over the next week to flesh out the idea.

@myronmarston
Member

Thanks, @xaviershay. I pushed a branch with that implementation (brute-force-match-array) -- see 78bdd38. Unfortunately, it's ridiculously slow, because it's an O(N!) algorithm...which is is pretty much the worse time complexity you can get. Check out this growth (from def7863):

    Top 6 slowest examples (61.67 seconds, 100.0% of total time):
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 10 items
        56.41 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 9 items
        4.66 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 8 items
        0.53014 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 7 items
        0.05838 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 6 items
        0.00974 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 5 items
        0.00212 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269

Compared to what we had before:

    Top 6 slowest examples (0.00324 seconds, 100.0% of total time):
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 10 items
        0.00067 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 9 items
        0.00062 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 8 items
        0.00056 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 6 items
        0.00054 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 7 items
        0.00047 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269
      Composing `match_array` with other matchers expect(...).to match_array([matcher, matcher]) works with arrays of 5 items
        0.00038 seconds ./spec/rspec/matchers/built_in/match_array_spec.rb:269

A minute to compare two arrays of 10 elements each is clearly not going to fly. I think I'm going to go back and see if I can get the stable marriage algorithm to work, unless you have any better ideas.

On a side note, even what we had before is slower than I would like: 6 ms to evaluate one matcher expression is going to be the primary bottle neck in a true unit test (most of my unit tests average ~2ms or so). I think that's simply the nature of having to do all the comparisons for match_array to be as flexible as it is, so I'm not sure we can do anything about it, but we should probably add note to the docs, telling people that it's slow compared to expect(array.sort).to eq(%w[ a b c d e ]), so in fast unit tests we recommend using that instead of people can.

@myronmarston
Member

Actually, I realized I just read that wrong: I said it was taking 6 ms to evaluate the matcher expression but it looks like it's actually .6 ms...which isn't so bad for the awesome flexibility match_array gives you.

@myronmarston
Member

And, FWIW, here's an implementation of the stable marriage algorithm in ruby:

http://rosettacode.org/wiki/Stable_marriage_problem#Ruby

@xaviershay
Member

I expected slow, but not that slow.

@myronmarston
Member

I expected slow, but not that slow.

Yep, it was definitely slower than I expected. Then again, factorial is the fastest growing function I can think of...

@myronmarston
Member

So I've made a bit of progress on the matching algorithm. I eventually decided the stable marriage thing was a dead end (I couldn't find a way to model our problem in terms of the requirements of the stable marriage algorithm), but I've come up with an alternate approach that I think will perform quite well:

  1. Compare every expected item against every actual item to get, for a each item, a list of matches from the other list.
  2. Put all all expected items in a remaining_expected_items list, and all actual items in a remaining_actual_items list. This represents the items that still have to be dealt with: they must either be paired, or put in the final extra_items list or put in the final missing_items list.
  3. Resolve any definitive results:
    • Any expected items for which there are no matches get moved from remaining_expected_items to extra_items.
    • Any actual items for which there are no matches get moved from remaining_actual_items to missing_items.
    • Any expected/actual item pairs that are reciprocally matched only to each other get removed from remaining_expected_items and remaining_actual_items.
  4. From here, do a brute force trial & error/backtracking approach:
    • Take any expected item. Choose one of its actual item matches, and try pairing them. Remove them from remaining_expected_items and remaining_actual_items, and update the matches lists for each remaining item to no longer include these paired items (since we are eliminating them as options for any other items to pair with).
    • Resolve any definitive results that come out of this trial, as explained in #3.
    • Iterate.
  5. As we do this, we keep track of the best solution seen so far. This will give us the extra_items and missing_items used in the failure message. If we get a solution that pairs everything up, we can abort early and consider the matcher to pass.

I feel pretty good about this algorithm, for a few reasons:

  • It's easy to reason about: I don't have a formal proof of its correctness, but it's pretty easy to see that as long as it's implemented correctly, it should always work properly.
  • Besides giving us a pass/fail status, this also gives us the extra_items and missing_items needed for the failure message.
  • It performs quite well (even though it boils down to a brute force approach in the end!). In the common case of every expected item matching against 1 actual item, the brute force part won't even come into play. It'll perform the same as it does now (which is basically O(N^2)). The number of brute force iterations required is a function of how many expected items match against multiple actual items (rather than being a function of array length, as the first attempt was).

I've got some of the plumbing for this solution already in place in a local branch. The part I'm struggling a bit with is the brute force iteration: we need to have a way to try fixing a pair to see where that goes, w/o destroying what we have so far, so that we can later pop the brute force stack and try a different pairing. I think that doing this easily basically comes down to having good abstractions, but I'm having a hard time naming things because it's already so abstract. (Plus, I've had a lot of distractions every time I've tried to work on this).

@xaviershay -- what do you think of that solution? Would you want to pair on it some time?

@xaviershay
Member

Algorithm sounds good. I'm not going to be able to pair on it before the new year, have quite a bit of travel and family stuff over the next week.

@myronmarston
Member

OK, I've implemented the algorithm discussed above. It's working, and it's much faster than the first brute force approach that @xaviershay and I tried. However, it still has some perf issues -- not based on how big the arrays are but based on how many duplicates/multi-matches there are. Also, I'm not super happy with the algorithm implementation -- it could definitely use some refactoring.

myronmarston added some commits Dec 5, 2013
@myronmarston myronmarston Rename spec file. 55c8686
@myronmarston myronmarston Refactor to use the new FuzzyMatcher from rspec-support. 622c4c4
@myronmarston myronmarston Make `change` composable with other matchers. ce44330
@myronmarston myronmarston Refactor `include` matcher a bit: pass around fewer args.
We can leverage the instance state of the matcher
object.
ec07bb1
@myronmarston myronmarston Update base matcher to better leverage `description`.
Include it in `failure_message` and
`failure_message_when_negated` so the matchers
don't have to override all 3 if they are just
customization the description part of it.
b030ec5
@myronmarston myronmarston Improve hash formatting used in `include` matcher.
{:a => 1, :b => 2}

...reads much better than:

{:a=>1, :b=>2}
aae1357
@myronmarston myronmarston Make `include` matcher fully composable. 0d739bb
@myronmarston myronmarston Make `raise_error` matcher fully composable. 1d64bc0
@myronmarston myronmarston Refactor start_with and end_with matchers.
- Leverage instance variable state.
- Leverage the description/failure messages
  provided by BaseMatcher.
- Remove ternaries.
1cb0e96
@myronmarston myronmarston Remove old rcov artifacts. cabb200
@myronmarston myronmarston Make `match_array` fully composable. 1e13557
@myronmarston myronmarston Make `start_with` and `end_with` fully composable. bbac8aa
@myronmarston myronmarston Make `throw_symbol` fully composable. 3602666
@myronmarston myronmarston Make `yield_with_args` fully compose with other matchers. 2c19cb4
@myronmarston myronmarston Make `yield_successive_args` matcher composable with other matchers. 74154e5
@myronmarston myronmarston Provide a default description override for alias matchers. 95a9048
@myronmarston myronmarston Setup infrastructure to specify matcher aliases. fef04e8
@myronmarston myronmarston Add some more matcher aliases. 2671393
@myronmarston myronmarston Add aliases for `be_xyz` predicate matchers. c22c052
@myronmarston myronmarston Add aliases for `be operator value`. e9d22d1
@myronmarston myronmarston Fix expressions like `be > 5` so they don't claim to be equal to anyt…
…hing.

This was being caused by subclassing `Be`, which defined `==`.
1b378e6
@myronmarston myronmarston More matcher aliases. 9cf0fea
@myronmarston myronmarston Move match_array definition to keep it alphabetical. 37a2205
@myronmarston myronmarston Add aliases for `has_` matcher. f0f1751
@myronmarston myronmarston No need to hide `Cover` constant on 1.8.
Range#cover? in 1.9 but other objects may implement
`#cover?` and there's not any benefit from hiding it.
56275ef
@myronmarston myronmarston Make `match` matcher fully composable. 4f1d59d
@myronmarston myronmarston Work around RBX bug by making a spec pending. 96caa7e
@myronmarston myronmarston Add a cuke for the new composable matchers feature. 8c0d2f7
@myronmarston myronmarston Add `contain_exactly` as an alternate version of `match_array`.
Fixes #398.
6bde36e
@myronmarston myronmarston be_within: for non-numeric, fail normally rather than erroring.
This allows it to be passed to `contain_exactly` for
a list that contains mixed types:

expect(["foo", 1.05]).to contain_exactly(
  a_value_within(0.1).of(1.0),
  a_string_starting_with("f")
)
7838ca3
@myronmarston myronmarston Add README sections on compound & composable matchers. 94cfe2c
@myronmarston myronmarston Update changelog.
[ci skip]
3063afb
@myronmarston myronmarston Add YARD docs for `Composable` module.
[ci skip]
2659a70
@myronmarston myronmarston Implement more complete `contain_exactly` algorithm. 30d3bab
@myronmarston myronmarston We don't need to loop over all expecteds; the recursion will hit all …
…cases.

On 1.9.3 on my machine, this reduced the time of the
added example from 41 seconds to 20 ms - 2000x faster.
fec663d
@myronmarston myronmarston Fail match rather than error from `start_with` and `end_with`
…when the given object can't be indexed or is not ordered. We
want to be able to pass an alias like `a_collection_ending_with`
to a collection matcher (like `include`) which may contain
multiple types of objects, some of which can be indexed and are
ordered, and some not. We don't want an early `ArgumentError`
for these cases -- instead, it should just be treated as
not matching.
c3f9f9b
@myronmarston myronmarston Make composed matchers display in diffs well. d6cada7
@myronmarston myronmarston `contain_exactly` should work with sets and other collections, too. ec93540
@myronmarston myronmarston Add docs for how to make your own composable matchers. 4afc668
@myronmarston myronmarston Change privateness when generating YARD docs.
We have YARD configured to hide private methods
from produced docs.  However, for the Composable
mixing, we have some private methods (as they are
only ever intended to be called w/ no receiver)
that we need to document so that users know how
they should be used when they mix them into their
own custom matchers.
0b55992
@myronmarston myronmarston Support composing compound matcher expressions. 4526db0
@myronmarston myronmarston Update yard. 49a4413
@myronmarston myronmarston Make `alias_matcher` doc like `alias_method`. 9ea9aa0
@myronmarston
Member

OK, this is at a point where I've done everything I planned to do, and I'm really just looking for code review feedback before I merge, specifically:

  • The contain_exactly matching algorithm: it works but I'm not super happy with it. I think there are some opportunities for perf improvements (which may require skyping with someone to talk through), and I think the terminology/method names/etc used for it is not very good.
  • The matcher aliases: too many? too few? Are there better/more consistent ways to phrase any of them? I tried to provide aliases both for passing the matcher as an argument to another matcher, and for when passing a compound matcher expression as an argument, so for example, end_with has both a_string_ending_with and ending_with, so it can be used in an expression like expect(s).to include( a_string_ending_with("z"), a_string_starting_with("a").and( ending_with("f"))
  • One side concern of the predicate matchers is that many of them are phrased in a way to suggest they check the type of an object, when they don't. For example, include is aliased as an_array_including, a_collection_including and a_string_including. In an expression like expect { }.to yield_with_args(a_string_including("a")), it suggests that it would fail if the the yielded arg is not a string, but it doesn't check that...it's just an alias of include, so it would also pass with an array like ["a", "b", "c"]. I think it's good to allow the user to use whatever alias they find most appropriate/expressive, but I wonder if users will expect it to check the type since the alias has a type in the name. Thoughts?
  • The method_missing predicate matchers: is the flexibility provided by that potentially confusing or overly magical?
  • rspec-mocks contains argument matchers like hash_including, array_including, etc that now have analogs in rspec-expectations (e.g. an_array_including -- but I didn't make an alias named a_hash_including -- should I have?). Should we have rspec-expectations overwrite the rspec-mocks argument matchers?
@xaviershay xaviershay commented on the diff Jan 1, 2014
features/built_in_matchers/contain_exactly.feature
@@ -0,0 +1,46 @@
+Feature: contain_exactly matcher
+
+ The `contain_exactly` matcher provides a way to test arrays against each other
+ in a way that disregards differences in the ordering between the actual
+ and expected array. For example:
+
+ ```ruby
+ expect([1, 2, 3]).to contain_exactly(2, 3, 1) # pass
+ expect([:a, :c, :b]).to contain_exactly(:a, :c ) # fail
+ ```
+
+ This matcher is also available as `match_array`, which expects the
+ expected array to be given as a single array argument rather than
+ as individual splatted elements. The above could also be written as:
@xaviershay
xaviershay Jan 1, 2014 Member

how do you choose between using contain_exactly or match_array? Why are there two methods that appear to do the same thing? Do we want to give equal prominence to both like we are here?

@myronmarston
myronmarston Jan 1, 2014 Member

For background, see the conversation in #398. I'm definitely open to doing it differently if you have a suggestion.

@xaviershay
xaviershay Jan 1, 2014 Member

"This matcher is also available as match_array, which gives you the option to not splat arrays. The above could also be written as:"

@myronmarston
myronmarston Jan 1, 2014 Member

match_array requires a single array arg -- there's no option to splat or not.

@xaviershay xaviershay and 1 other commented on an outdated diff Jan 1, 2014
features/composing_matchers.feature
@@ -0,0 +1,233 @@
+Feature: Composing Matchers
+
+ RSpec's matchers are designed to be composable so that you can
+ combine them to express the exact details of what you expect
+ but nothing more. This can help you avoid writing over-specified
+ brittle specs, by using a matcher in place of an exact value to
+ specify only the essential aspects of what you expect.
+
+ For RSpec 3, we have updated all matchers to make them accept
@xaviershay
xaviershay Jan 1, 2014 Member

"For RSpec 3" seems a weird phrase to have in documentation. (Would make sense in a blog post though.)

Simplify to something like: "The following matchers accept matchers as arguments:"

@myronmarston
myronmarston Jan 1, 2014 Member

Agreed, good suggestion.

@xaviershay xaviershay commented on the diff Jan 1, 2014
features/composing_matchers.feature
+ * `throw_symbol(:sym, matcher)`
+ * `yield_with_args(matcher, matcher)`
+ * `yield_successive_args(matcher, matcher)`
+
+ Note that many built-in matchers do not accept matcher arguments
+ because they have precise semantics that do not allow for a matcher
+ argument. For example, `equal(some_object)` is designed to pass only
+ if the actual and expected arguments are references to the same object.
+ It would not make sense to support a matcher argument here.
+
+ All of RSpec's built-in matchers have one or more aliases that allow
+ you to use a noun-phrase rather than verb form since they read better
+ as composed arguments. They also provide customized failure output so
+ that the failure message reads better as well.
+
+ A full list of these aliases is out of scope here, but here are some
@xaviershay
xaviershay Jan 1, 2014 Member

Can we add a link here to the full list, or details for how to find it?

@myronmarston
myronmarston Jan 1, 2014 Member

I think once the new RSpec site is up it'll be easier to include links to this kind of stuff. For now I think I may just say "see the API docs for the RSpec::Matchers module" -- does that sound sufficient?

@xaviershay xaviershay and 1 other commented on an outdated diff Jan 1, 2014
lib/rspec/matchers.rb
+ #
+ # ### Making custom matchers composable
+ #
+ # RSpec's built-in matchers are designed to be composed, in expressions like:
+ #
+ # expect(["barn", 2.45]).to contain_exactly(
+ # a_value_within(0.1).of(2.5),
+ # a_string_starting_with("bar")
+ # )
+ #
+ # Custom matchers can easily participate in composed matcher expressions like these.
+ # Include {RSpec::Matchers::Composable} in your custom matcher to make it support
+ # being composed (matchers defined using the DSL have this included automatically).
+ # Within your matcher's `matches?` method (or the `match` block, if using the DSL),
+ # use `values_match?(expected, actual)` rather than `expected == actual` or
+ # `actual == expected`. Under the covers, `values_match?` is able to match arbitrary
@xaviershay
xaviershay Jan 1, 2014 Member

"or" clause seems redundant and doesn't make the sentence any clearer.

@myronmarston
myronmarston Jan 1, 2014 Member

Agreed, good suggestion.

@xaviershay xaviershay commented on the diff Jan 1, 2014
lib/rspec/matchers.rb
def be_falsey
BuiltIn::BeFalsey.new
end
-
- alias_method :be_falsy, :be_falsey
+ alias_matcher :be_falsy, :be_falsey
+ alias_matcher :a_falsey_value, :be_falsey
+ alias_matcher :a_falsy_value, :be_falsey
@xaviershay
xaviershay Jan 1, 2014 Member

makes me a little sad to have both spellings given neither are real words anyways.

@myronmarston
myronmarston Jan 1, 2014 Member

We discussed this in #283. As you said, it's not a real word, and given that, there's no canonical spelling. I think it's best for RSpec not to take a stand on which spelling is correct, and just provide both.

@xaviershay xaviershay and 1 other commented on an outdated diff Jan 1, 2014
lib/rspec/matchers/built_in/be.rb
@@ -193,9 +194,11 @@ def parse_expected(expected)
expected
end
+ # http://rubular.com/r/KkwGL6s6yZ
+ REGEX = /^(?:(be_(?:an?_)?)(.*))|(an?_\w+_(?:that|who|which)_is_(?:an?_)?)(.*)/
@xaviershay
xaviershay Jan 1, 2014 Member

gnarly

I have a small worry that all these nouns/verbs are becoming such a large API that they are to reason about / keep in your head.

@myronmarston
myronmarston Jan 1, 2014 Member

Yeah, I worry a bit about that too. Having the consistency of aliases for all matchers seems desirable, but I do worry about the semi-insanity of this regex to support that. In practice, I'm thinking that the predicate matchers may not be used that often in composable fashion, so maybe it's best not to provide aliases for them? Users can always call RSpec::Matchers.alias_matcher to define their own aliases. Thoughts?

@xaviershay xaviershay and 1 other commented on an outdated diff Jan 1, 2014
lib/rspec/matchers/built_in/contain_exactly.rb
+ actual_matches = Array.new(actual.count) { [] }
+
+ expected.each_with_index do |e, ei|
+ actual.each_with_index do |a, ai|
+ if values_match?(e, a)
+ expected_matches[ei] << ai
+ actual_matches[ai] << ei
+ end
+ end
+ end
+
+ PairingsMaximizer.new(expected_matches, actual_matches)
+ end
+ end
+
+ class PairingsMaximizer
@xaviershay
xaviershay Jan 1, 2014 Member

I would include some of the history and justification for this algorithm in a class comment.

@myronmarston
myronmarston Jan 1, 2014 Member

Yep, I kinda wanted to settle on the terminology and implementation before commenting it -- but I'll plan to circle round on this once we've settled on that.

@xaviershay xaviershay commented on the diff Jan 1, 2014
lib/rspec/matchers/composable.rb
@@ -39,6 +41,98 @@ def or(matcher)
def ===(value)
matches?(value)
end
+
+ private unless defined?(::YARD)
@xaviershay
xaviershay Jan 1, 2014 Member

haven't seen this before ... because YARD doesn't include private methods? Does it include protected?

@myronmarston
myronmarston Jan 1, 2014 Member
➜  rspec-mocks git:(master) yard doc --help | grep private
        --private                    Show private methods. (default hides private)
        --no-private                 Hide objects with @private tag

Private methods are hidden in the docs by default. This makes a lot of sense: private methods are generally not part of your public API.

Here I wanted these methods to be private (as they are only ever intended to be called from within a matcher on self) but since this is a mixin intended for users to use in their custom matchers, I wanted these methods doc'd. This definitely felt like a hack.

Do you think it's better to just make the methods public? I'm on the fence.

@xaviershay
xaviershay Jan 1, 2014 Member

Making them ruby public is fine - we've docced them as api private

@myronmarston
myronmarston Jan 1, 2014 Member

Actually, they are doc'd as api public. The problem here is that there are two notions of visibility:

  • Ruby's notion: private vs protected vs public. This affects whether you can call a method with an explicit receiver or not.
  • API notion: private vs public: Things docs as api private are subject to change in minor or patch releases with no deprecation warnings or notices. They are "off limits" for end users to use directly if they don't want their code breaking when they upgrade. API public means we are committed to not breaking it in a minor or patch release as per SemVer.

Here we've got methods we'd like to be ruby private but API public.

@xaviershay
Member

it suggests that it would fail if the the yielded arg is not a string, but it doesn't check that

I'm not too worried about this. In my experience most people don't think about explicitly checking types of their objects, and would expect this to pass with a string-like object. (Though it is a bit weird in ruby, string is an array-like object.)

Should we have rspec-expectations overwrite the rspec-mocks argument matchers?

Is there existing code in either library that soft depends on the other like this? If yes, we should keep that direction the same. If no, does pushing some part of the code down into rspec-support make sense?

@myronmarston
Member

Is there existing code in either library that soft depends on the other like this?

rspec-mocks defines a couple matchers (e.g. have_received), but it's a very soft dependency: it simply implements the same protocol, and it can work on its own w/o rspec-expectations.

If no, does pushing some part of the code down into rspec-support make sense?

Maybe -- I'll think more about that.

@xaviershay
Member

I'm ok with a soft-dependency, just as long as we don't have one both ways :)

myronmarston added some commits Jan 1, 2014
@myronmarston myronmarston Improve docs based on feedback from @xaviershay. dd5cdd4
@myronmarston myronmarston Pare down the number of matcher aliases.
- The `RSpec::Matchers` API was getting very large with
  all the aliases, which makes it harder to keep all in
  your head.
- It's better to start with fewer and then add more
  in future releases as there is demand. Once they are
  there, we can't remove them from a future minor/patch
  release because of server.
- `RSpec::Matchers.alias_matcher` is provided so users can
  define new aliases.
23ee1e3
@myronmarston myronmarston Remove method_missing support for dynamic predicate matcher aliases.
It was going to be very hard to document how this worked,
and what phrasing forms are supported. Instead, we simply allow
users to define their own aliases using `alias_matcher`.
ce06693
@myronmarston myronmarston Improve docs and readability of PairingsMaximizer. 47c543d
@myronmarston myronmarston Fix comparison operator.
We want to keep the solution with the fewest unmatched
items, not the most unmatched items.
8d33cfd
@myronmarston
Member

@xaviershay -- I think the perf is still unsatisfactory -- see 09734a7. I'm going to try to see if I can optimize it a bit.

myronmarston added some commits Jan 2, 2014
@myronmarston myronmarston Add match_array/contain_exactly benchmarks.
There are still perf problems :(.
09734a7
@myronmarston myronmarston Refactor: extract a solution struct. 4419b12
@myronmarston myronmarston Refactor: shrink the subproblems.
When recursing during the brute force depth-first
search, pass along only the indeterminate indexes,
so that it runs on a smaller subproblem. For this to
work, we had to switch to using a hash, so that it
can handle index gaps.

This provides mixed results: it makes some things faster,
some things slower. I think it's an improvement overall.
e63799a
@myronmarston myronmarston Use correct spelling of "indeterminate". 96bfc0f
@myronmarston myronmarston Update pairings maximizer spec based on current API. 3ffd27e
@myronmarston myronmarston Add rubyprof script so I can see troubleshoot perf issues. 1167771
@myronmarston myronmarston Use our own `values_match?` logic for better perf.
It's surprising how much difference this makes!
f8a5b03
@myronmarston myronmarston Try a fast sorted matching algorithm first.
This is much, much faster than the full matching
algorithm, and in common cases (e.g. arrays of numbers
or strings) works just fine.  See the benchmarks
for how much of a difference this makes!
ae12b4a
@myronmarston
Member

@xaviershay -- I found some fairly easy optimizations to make that make the common cases blazing fast, and make the slower cases about 2x faster. It's still slower than I would like it for those slower cases, but I'm at a point where I think I'm OK with it for now and can move on to something else. Let me know what you think of my optimizations -- I may have missed something or there may be further low-hanging fruit that these optimizations suggest.

@xaviershay
Member

nice

@xaviershay
Member

LGTM

@myronmarston myronmarston merged commit 7736337 into master Jan 2, 2014
@myronmarston myronmarston deleted the support-composable-matchers branch Jan 2, 2014
@xaviershay
Member

So we can ship 3 now right? :P

@myronmarston
Member

So we can ship 3 now right? :P

Depends on your definition of "now" :).

@JonRowe
Member
JonRowe commented Jan 2, 2014

I'm going to work on the formatter stuff more this weekend, when thats done then we can ship RSpec 3 :P

@myronmarston
Member

I'm going to work on the formatter stuff more this weekend, when thats done then we can ship RSpec 3 :P

Well, we can ship beta2. Then I plan to do my core/mocks/expectations code audit and see what else (if anything) we want before RC and final.

@JonRowe
Member
JonRowe commented Jan 2, 2014

Aye, mostly was just reminding peeps more stuff existed ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment