Skip to content

Fix errors method check in be_valid matcher #2096

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

Merged
merged 1 commit into from
Mar 12, 2019

Conversation

kkuchta
Copy link
Contributor

@kkuchta kkuchta commented Mar 1, 2019

Bug report + bug fix PR.

If the .be_valid? matcher fails, it checks for the presence of a
.errors method and uses it, if present, in the failure message. This
works great, unless the subject happens to include an errors method
that takes an argument, in which case we get an error (because we call
errors with 0 arguments).

Here's a simple repro case which'll result in wrong number of arguments (given 0, expected 1) for the .errors method.

  it 'foo' do
    some_obj = Object.new
    def some_obj.valid?() false; end
    def some_obj.errors(foo) 'bar'; end

    expect(some_obj).to be_valid
  end

I'd propose to fix this with an arity check (this PR). I've included a spec that fails without this fix (piggybacking on an existing spec that already tested the general case of an object with a valid? method but no errors method).

Copy link
Member

@JonRowe JonRowe left a comment

Choose a reason for hiding this comment

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

I'd like an extra spec here, and for the majority the boolean can be collapsed. You will need to see if theres a solution for the 1.8.7 failure too unless you're happy to wait for RSpec Rails 4.

@@ -15,7 +15,8 @@ def matches?(actual)
def failure_message
message = "expected #{actual.inspect} to be valid"

if actual.respond_to?(:errors)
if actual.respond_to?(:errors) &&
(!actual.methods.include?(:errors) || actual.method(:errors).arity < 1)
Copy link
Member

Choose a reason for hiding this comment

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

You don't need this duplicate methods check, if if actual.respond_to?(:errors) && actual.method(:errors).arity < 1 will suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the methods.include? check because, given how many ways there are to define functions in ruby, I figured there was probably some way or another to have an object not work with the original check.

When I actually tried to construct such an object, I found that it is possible, but it's pretty edge-casey:

class A
  def respond_to_missing?(name, _)
    return true if name.to_sym == :foo
    super
  end
end
a = A.new
a.respond_to?(:foo) # true
a.method(:foo).arity # -1
a.foo # NoMethodError: undefined method `foo' for #<A:0x00007fd60c168430>
a.methods.include?(:foo) # false

So with a class defined like that, we could still run into errors in this code without the .include? check. Although if you think that's too much of an edge-case to bother accounting for, I'm happy to remove it.

Copy link
Member

Choose a reason for hiding this comment

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

We don't support classes that don't behave according to ruby semantics, if someone uses the trick you mention it won't work with the matcher and thats ok, it'll just return false. If you add the correct method_missing definition to go with that snippet the method should be returned.

@kkuchta
Copy link
Contributor Author

kkuchta commented Mar 1, 2019

Thanks for the feedback! I'll break this case out into a separate spec and see if I can get it working in 1.8.7.

@kkuchta
Copy link
Contributor Author

kkuchta commented Mar 4, 2019

@JonRowe - I think I've addressed all your comments. Thoughts?

If the `.be_valid?` matcher fails, it checks for the presence of a
`.errors` method and uses it, if present, in the failure message.  This
works great, unless the subject happens to include an `errors` method
that takes an argument, in which case we get an error (because we call
errors with 0 arguments).

We can fix this with a simple arity check.
Copy link
Member

@benoittgt benoittgt left a comment

Choose a reason for hiding this comment

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

I reran the one failing build. LGTM and it works on old Ruby.

Thanks for the PR. 🎉

@JonRowe JonRowe merged commit 6ad4943 into rspec:master Mar 12, 2019
JonRowe added a commit that referenced this pull request Mar 12, 2019
JonRowe added a commit that referenced this pull request Mar 12, 2019
benoittgt pushed a commit to benoittgt/rspec-rails that referenced this pull request May 1, 2019
If the `.be_valid?` matcher fails, it checks for the presence of a `.errors` method and uses it, if present, in the failure message.  This works great, unless the subject happens to include an `errors` method that takes an argument, in which case we get an error (because we call
errors with 0 arguments), this checks the arity of that method before we call it.
benoittgt pushed a commit to benoittgt/rspec-rails that referenced this pull request May 1, 2019
benoittgt pushed a commit to benoittgt/rspec-rails that referenced this pull request May 1, 2019
JonRowe pushed a commit that referenced this pull request Oct 3, 2019
If the `.be_valid?` matcher fails, it checks for the presence of a `.errors` method and uses it, if present, in the failure message.  This works great, unless the subject happens to include an `errors` method that takes an argument, in which case we get an error (because we call
errors with 0 arguments), this checks the arity of that method before we call it.
JonRowe added a commit that referenced this pull request Oct 3, 2019
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