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

Error on expects.with with empty hash argument #657

Closed
josesei opened this issue Jul 18, 2024 · 10 comments · Fixed by #660
Closed

Error on expects.with with empty hash argument #657

josesei opened this issue Jul 18, 2024 · 10 comments · Fixed by #660
Assignees

Comments

@josesei
Copy link

josesei commented Jul 18, 2024

How to reproduce:

minitest (5.24.1)
mocha (2.4.2)
rails (6.1.7.8)

Ruby 3.3.0

require "test_helper"

class MyKlass
  def self.method_accepting_hash(h)
    "whatever"
  end
end

class MochaErrorTest < ActiveSupport::TestCase
  hash_with_value1 = {"key" => "value"}
  hash_with_value2 = {"key2" => "value2"}
  empty_hash = {}

  test "mocha success, hash with values" do
    MyKlass.expects(:method_accepting_hash).once.with(hash_with_value1).returns("whatever")
    MyKlass.expects(:method_accepting_hash).once.with(hash_with_value2).returns("whatever")
    MyKlass.method_accepting_hash(hash_with_value1)
    MyKlass.method_accepting_hash(hash_with_value2)
  end
  
  test "mocha success, empty hashes" do
    MyKlass.expects(:method_accepting_hash).once.with(empty_hash).returns("whatever")
    MyKlass.expects(:method_accepting_hash).once.with(empty_hash).returns("whatever")
    MyKlass.method_accepting_hash(empty_hash)
    MyKlass.method_accepting_hash(empty_hash)
  end

  test "mocha success first empty hash, then hash with values" do
    MyKlass.expects(:method_accepting_hash).once.with(empty_hash).returns("whatever")
    MyKlass.expects(:method_accepting_hash).once.with(hash_with_value1).returns("whatever")
    MyKlass.method_accepting_hash(empty_hash)
    MyKlass.method_accepting_hash(hash_with_value1)
  end
  
  test "mocha error first hash with values, then empty hash, weird error message?" do
    MyKlass.expects(:method_accepting_hash).once.with(hash_with_value1).returns("whatever")
    MyKlass.expects(:method_accepting_hash).once.with(empty_hash).returns("whatever")
    MyKlass.method_accepting_hash(hash_with_value1)
    MyKlass.method_accepting_hash(empty_hash)
  end
end

Failure:

Minitest::Assertion: unexpected invocation: MyKlass.method_accepting_hash({})
unsatisfied expectations:

  • expected exactly once, invoked twice: MyKlass.method_accepting_hash({})
  • expected exactly once, invoked never: MyKlass.method_accepting_hash({"key" => "value"})

test/support/mocha_error_test.rb:31:in `block in class:MochaErrorTest'

@josesei
Copy link
Author

josesei commented Jul 18, 2024

workaround is to use block matcher

@josesei
Copy link
Author

josesei commented Jul 18, 2024

@floehopper just in case

module ParameterMatchers
    # @private
    class PositionalOrKeywordHash < Base
      def matches?(available_parameters)
        parameter, is_last_parameter = extract_parameter(available_parameters)

        # return false unless HasEntries.new(@value).matches?([parameter]) <---- commenting this line makes the tests pass

Possibly you need that for something else, but I'd love to get that fixed

@floehopper
Copy link
Member

floehopper commented Jul 18, 2024

@josesei Thanks for reporting this. I had independently come to the same conclusion as you as to where the problem lies. I've managed to write a couple of failing unit tests in #660 which reproduce the issue. That line of code is needed to handle nested matchers with keyword arguments (see test below), but clearly the implentation isn't quite right. My head is pretty scrambled right now, but I'll try to get it fixed as soon as I can.

def test_should_match_keyword_args_with_matchers_using_keyword_args
matcher = Hash.ruby2_keywords_hash({ key_1: is_a(String), key_2: is_a(Integer) }).to_matcher(top_level: true) # rubocop:disable Style/BracesAroundHashParameters
assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 'foo', key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end

@josesei
Copy link
Author

josesei commented Jul 18, 2024

sounds good, thx!

@floehopper
Copy link
Member

floehopper commented Jul 19, 2024

@josesei Quick question - were these tests working with earlier versions of Mocha (e.g. v2.2.0)?

Actually - ignore that!

@floehopper
Copy link
Member

@josesei I've got some tidying up to do and some double-checking I haven't broken anything else, but I think the changes in the fix-regression-in-matching-hash-parameter branch should fix your problem. If you get the chance, I'd really appreciate it if you could see if it does fix the problem.

@josesei
Copy link
Author

josesei commented Jul 19, 2024

yep, it does work now! appreciate the effort!

it worked for me for ruby 2.6 and rails 5, mocha version mocha (1.13.0)

I guess it has to do with the keyword params compat thingy, but I'm sure you have it

floehopper added a commit that referenced this issue Jul 22, 2024
In this commit [1] which fixed #647 and which was released in v2.3.0, a
top-level `Hash` argument passed into `Expectation#with` was implicitly
converted into a `HasEntries` matcher in order to handle "nested"
matchers used within the keys & values of the `Hash`.

This _new_ behaviour makes particular sense for keyword arguments where
the requirement to wrap such a `Hash` in a call to `#has_entries` was a
bit surprising as described in #647.

However, that change inadvertently broke basic `Hash` parameter matching
as described in #657, because the `HasEntries` matcher only needs to
match each of the entries specified; it will still match if there are
*extra* entries in the `Hash` it is matching against.

In order to fix this without breaking the "nested" matching behaviour
added in [1], I've added an optional `exact` keyword argument to the
`HasEntries` constructor and set this to `true` when it's called from
`PositionalOrKeywordHash#matches?`. I haven't bothered to document the
new `exact` argument, because currently it's only used internally.

I did consider introducing a new matcher more like
`RSpec::Matchers#match` matcher [2], but I decided that wouldn't provide
as elegant a solution to #647 as the change in this commit.

I can imagine introducing something like `#has_exact_entries` to the
public API, but it would probably make sense to add something similar
for exact matching of array items at the same time, so I'm going to
leave that for now.

I've added a few acceptance tests in this commit which would have caught
the problem that I inadvertently introduced in [1] and a few that cover
the "nested" matching behaviour that was introduced in that commit
(previously I had only added a unit test).

[1]: f94e250
[2]: https://www.rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers#match-instance_method
@floehopper
Copy link
Member

@josesei Thanks for testing. The problem was something I accidentally introduced in v2.3.0 (specifically f94e250) as a fix for #647 where a Hash parameter passed into Expectation#with was implicitly wrapped in a call to #has_entries. The latter does not require an exact match - only the specified entries have to match. The fix is to ensure that all entries match and that there are no extra entries. I hope to release the fix later today.

@josesei
Copy link
Author

josesei commented Jul 22, 2024

Makes sense, thanks for the insights and all your work!

@floehopper
Copy link
Member

This fix has been released in v2.4.3.

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

Successfully merging a pull request may close this issue.

2 participants