Skip to content
This repository

Provide a clear failure message when using `{}.should =~ {}`. #193

Merged
merged 1 commit into from over 1 year ago

2 participants

Myron Marston Andy Lindeman
Myron Marston

The match_array matcher (delegated to by =~ for enumerables) is
not meant to be used for hashes, but the should =~ syntax doesn't
make that obvious. Previously, you would get a failure like:

 Failure/Error: actual.should =~ actual
   expected: {:foo=>"bar"}
        got: {:foo=>"bar"} (using =~)

...which is pretty confusing. Our match_array matcher already
includes handling for invalid arguments (such as hashes) to return
a clear failure message, but it wasn't being used for an expression
like {}.should =~ {} because it was only registered as an operator
matcher for Array. This changes it so that it is registered as an
operator matcher for any Enumerable. This improves the failure message
for enumerable types like hash and set, but on its own, it could cause
breakage for things like 1.8 strings that are Enumerable but also
define a reasonable =~. The fix here changes the operator matcher
delegation logic so that it only delegates to the registered matcher
if the object has the generic Kernel implementation of the operator.
If it has a more specific implementation, we assume the user actually
wants to match using the given operator itself.

Fixes #191.

Can I get a code review? /cc @alindeman @phiggins

Myron Marston myronmarston Provide a clear failure message when using `{}.should =~ {}`.
The match_array matcher (delegated to by `=~` for enumerables) is
not meant to be used for hashes, but the `should =~` syntax doesn't
make that obvious. Previously, you would get a failure like:

     Failure/Error: actual.should =~ actual
       expected: {:foo=>"bar"}
            got: {:foo=>"bar"} (using =~)

...which is pretty confusing. Our `match_array` matcher already
includes handling for invalid arguments (such as hashes) to return
a clear failure message, but it wasn't being used for an expression
like `{}.should =~ {}` because it was only registered as an operator
matcher for `Array`. This changes it so that it is registered as an
operator matcher for any Enumerable. This improves the failure message
for enumerable types like hash and set, but on its own, it could cause
breakage for things like 1.8 strings that are Enumerable but also
define a reasonable `=~`.  The fix here changes the operator matcher
delegation logic so that it only delegates to the registered matcher
if the object has the generic Kernel implementation of the operator.
If it has a more specific implementation, we assume the user actually
wants to match using the given operator itself.

Fixes #191.
c7a91af
Andy Lindeman alindeman commented on the diff
lib/rspec/matchers/operator_matcher.rb
@@ -62,6 +62,22 @@ def description
62 62
63 63 private
64 64
  65 + if Method.method_defined?(:owner) # 1.8.6 lacks Method#owner :-(
3
Andy Lindeman Owner

Other places we have checked RUBY_VERSION >= '1.8.7' to make this code easier to find and delete ifever/whenever we drop support for 1.8.6. Thoughts?

Myron Marston Owner

If I was going to check the RUBY_VERSION, I'd want to check if it equaled 1.8.6 -- the point in my mind is to make it easy to ack/grep for 1.8.6. and find lots of places where we can remove code.

I actually considered checking the version in that manner, but decided to use feature detection plus a comment that mentions 1.8.6...that still gives us the search-ability, while still performing the check in a more semantic manner. Also, if anyone on 1.8.6 loads backports, it defines Method#owner and they could potentially use this first implementation of uses_generic_implementation_of?.

Andy Lindeman Owner

That's good rationale. Makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Andy Lindeman alindeman commented on the diff
lib/rspec/matchers/operator_matcher.rb
@@ -62,6 +62,22 @@ def description
62 62
63 63 private
64 64
  65 + if Method.method_defined?(:owner) # 1.8.6 lacks Method#owner :-(
  66 + def uses_generic_implementation_of?(op)
  67 + @actual.method(op).owner == ::Kernel
  68 + end
  69 + else
  70 + def uses_generic_implementation_of?(op)
  71 + # This is a bit of a hack, but:
  72 + #
  73 + # {}.method(:=~).to_s # => "#<Method: Hash(Kernel)#=~>"
  74 + #
  75 + # In the absence of Method#owner, this is the best we
  76 + # can do to see if the method comes from Kernel.
  77 + @actual.method(op).to_s.include?('(Kernel)')
1
Andy Lindeman Owner

Nice :) Gotta do what you gotta do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Andy Lindeman alindeman commented on the diff
spec/rspec/matchers/match_array_spec.rb
@@ -135,4 +146,8 @@ def ==(other)
135 146 it "fails with a string and the expected error message is given" do
136 147 expect { "I like turtles".should match_array([1,2,3]) }.to fail_with(/expected an array/)
137 148 end
  149 +
  150 + it 'fails with a clear message when given a hash using the `should =~` syntax' do
  151 + expect { {}.should =~ {} }.to fail_with(/expected an array/)
  152 + end
3
Andy Lindeman Owner

Even though I know it will pass, does it make sense to test a String instance on 1.8.7 .. since that is a bit of a weird case? I'm thinking regression prevention if the code is ever refactored or reworked a bit. I'm not sure it's a good idea, but I did have the thought.

Myron Marston Owner

I wanted to make sure my intuition that this was necessary for String on 1.8.7 was correct...so I developed this on 1.8.7, and changed the operator registration from Array to Enumerable as the first step. It broke a bunch of tests, and demonstrated this was necessary. For example, this spec failed:

https://github.com/rspec/rspec-expectations/blob/v2.12.0/spec/rspec/matchers/operator_matcher_spec.rb#L107-L111

Andy Lindeman Owner

Also I see that the string case was already tested. I had skimmed over the non-added part. This looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston myronmarston merged commit cf52fe6 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Dec 06, 2012
Myron Marston myronmarston Provide a clear failure message when using `{}.should =~ {}`.
The match_array matcher (delegated to by `=~` for enumerables) is
not meant to be used for hashes, but the `should =~` syntax doesn't
make that obvious. Previously, you would get a failure like:

     Failure/Error: actual.should =~ actual
       expected: {:foo=>"bar"}
            got: {:foo=>"bar"} (using =~)

...which is pretty confusing. Our `match_array` matcher already
includes handling for invalid arguments (such as hashes) to return
a clear failure message, but it wasn't being used for an expression
like `{}.should =~ {}` because it was only registered as an operator
matcher for `Array`. This changes it so that it is registered as an
operator matcher for any Enumerable. This improves the failure message
for enumerable types like hash and set, but on its own, it could cause
breakage for things like 1.8 strings that are Enumerable but also
define a reasonable `=~`.  The fix here changes the operator matcher
delegation logic so that it only delegates to the registered matcher
if the object has the generic Kernel implementation of the operator.
If it has a more specific implementation, we assume the user actually
wants to match using the given operator itself.

Fixes #191.
c7a91af
This page is out of date. Refresh to see the latest.
2  lib/rspec/matchers.rb
@@ -684,6 +684,6 @@ def match_array(array)
684 684 BuiltIn::MatchArray.new(array)
685 685 end
686 686
687   - OperatorMatcher.register(Array, '=~', BuiltIn::MatchArray)
  687 + OperatorMatcher.register(Enumerable, '=~', BuiltIn::MatchArray)
688 688 end
689 689 end
18 lib/rspec/matchers/operator_matcher.rb
@@ -31,7 +31,7 @@ def initialize(actual)
31 31
32 32 def self.use_custom_matcher_or_delegate(operator)
33 33 define_method(operator) do |expected|
34   - if matcher = OperatorMatcher.get(@actual.class, operator)
  34 + if uses_generic_implementation_of?(operator) && matcher = OperatorMatcher.get(@actual.class, operator)
35 35 @actual.send(::RSpec::Matchers.last_should, matcher.new(expected))
36 36 else
37 37 eval_match(@actual, operator, expected)
@@ -62,6 +62,22 @@ def description
62 62
63 63 private
64 64
  65 + if Method.method_defined?(:owner) # 1.8.6 lacks Method#owner :-(
  66 + def uses_generic_implementation_of?(op)
  67 + @actual.method(op).owner == ::Kernel
  68 + end
  69 + else
  70 + def uses_generic_implementation_of?(op)
  71 + # This is a bit of a hack, but:
  72 + #
  73 + # {}.method(:=~).to_s # => "#<Method: Hash(Kernel)#=~>"
  74 + #
  75 + # In the absence of Method#owner, this is the best we
  76 + # can do to see if the method comes from Kernel.
  77 + @actual.method(op).to_s.include?('(Kernel)')
  78 + end
  79 + end
  80 +
65 81 def eval_match(actual, operator, expected)
66 82 ::RSpec::Matchers.last_matcher = self
67 83 @operator, @expected = operator, expected
15 spec/rspec/matchers/match_array_spec.rb
@@ -113,6 +113,17 @@ def ==(other)
113 113 MESSAGE
114 114 end
115 115
  116 + context "when the array defines a `=~` method" do
  117 + it 'delegates to that method rather than using the match_array matcher' do
  118 + array = []
  119 + def array.=~(other)
  120 + other == :foo
  121 + end
  122 +
  123 + array.should =~ :foo
  124 + expect { array.should =~ :bar }.to fail_with(/expected: :bar/)
  125 + end
  126 + end
116 127 end
117 128
118 129 describe "should_not =~ [:with, :multiple, :args]" do
@@ -135,4 +146,8 @@ def ==(other)
135 146 it "fails with a string and the expected error message is given" do
136 147 expect { "I like turtles".should match_array([1,2,3]) }.to fail_with(/expected an array/)
137 148 end
  149 +
  150 + it 'fails with a clear message when given a hash using the `should =~` syntax' do
  151 + expect { {}.should =~ {} }.to fail_with(/expected an array/)
  152 + end
138 153 end

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.