Repository: freerange/mocha (Ruby)

Issue:
Regression after upgrading to `2.4.3`
After upgrading to `2.4.3`, a previously-passing test started failing with a mocha exception. It seems to have to do with mocking two expectations on the same method with two different signatures:

Repro code:
```ruby
class C
   def self.foo(bar: 0); end
end

# test:
C.expects(:foo).with(bar: 42)
C.expects(:foo)
      
C.foo(bar: 42)
C.foo
```

Exception:
```
NoMethodError: undefined method `length' for nil
```

Stack trace:
```
mocha-2.4.3/lib/mocha/parameter_matchers/has_entries.rb:41:in `matches?'
mocha-2.4.3/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb:18:in `matches?'
mocha-2.4.3/lib/mocha/parameters_matcher.rb:21:in `block in parameters_match?'
mocha-2.4.3/lib/mocha/parameters_matcher.rb:21:in `all?'
mocha-2.4.3/lib/mocha/parameters_matcher.rb:21:in `parameters_match?'
mocha-2.4.3/lib/mocha/parameters_matcher.rb:16:in `match?'
mocha-2.4.3/lib/mocha/expectation.rb:647:in `match?'
```


Discussion:
Thanks for reporting - I'll take a look in the morning 
@vlad-pisanov I think I've reproduced the problem, although I had to change your code slightly:

```ruby
require 'bundler/setup'
require 'minitest/autorun'
require 'mocha/minitest'

class C
  def self.foo(bar: 0); end
end

class FooTest < Minitest::Test
  def test_foo
    C.expects(:foo).with(bar: 42)
    C.expects(:foo)

    C.foo
    C.foo(bar: 42)
  end
end
```

i.e. I had to reverse the order of the method invocations.

Does that make sense?
I get the same `NoMethodError: undefined method 'length' for nil` exception with both
```ruby
C.foo
C.foo(bar: 42)
```
and
```ruby
C.foo(bar: 42)
C.foo
```
@vlad-pisanov Ah, yes - my mistake - I was working from a simplified version. Please ignore me!
@vlad-pisanov I think I've fixed the problem in #663 in the `fix-regression-when-method-expecting-hash-invoked-with-no-args` branch. Is there any chance you could check that the changes in that branch fix your problem?
This fix was released in [v2.4.4](https://rubygems.org/gems/mocha/versions/2.4.4).
The fix seems to catch `nil` parameters correctly, but still fails for us (in a Rails app), because the parameters end up being `ActionController::Parameter` and not `Hash`:

```
2024-07-23T21:03:12 [E|app|] NoMethodError: undefined method `length' for #<ActionController::Parameters:0x00000000278aec70>
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameter_matchers/has_entries.rb:42:in `matches?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb:18:in `matches?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameters_matcher.rb:21:in `block in parameters_match?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameters_matcher.rb:21:in `all?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameters_matcher.rb:21:in `parameters_match?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/parameters_matcher.rb:16:in `match?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/expectation.rb:647:in `match?'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/expectation_list.rb:59:in `block in matching_expectations'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/expectation_list.rb:59:in `select'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/expectation_list.rb:59:in `matching_expectations'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/expectation_list.rb:29:in `match_allowing_invocation'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/mock.rb:324:in `handle_method_call'
 | /home/evgeni/.rbenv/versions/2.7.6/lib/ruby/gems/2.7.0/gems/mocha-2.4.4/lib/mocha/stubbed_method.rb:47:in `block in define_new_method'
 | /home/evgeni/Devel/katello/katello/app/controllers/katello/api/v2/content_view_versions_controller.rb:142:in `incremental_update'
```
Something like this fixes it, but I find it rather ugly:

```patch
diff --git a/lib/mocha/parameter_matchers/has_entries.rb b/lib/mocha/parameter_matchers/has_entries.rb
index 51bb160..699a294 100644
--- a/lib/mocha/parameter_matchers/has_entries.rb
+++ b/lib/mocha/parameter_matchers/has_entries.rb
@@ -39,7 +39,7 @@ module Mocha
       def matches?(available_parameters)
         parameter = available_parameters.shift
         return false unless parameter
-        return false if @exact && @entries.length != parameter.length
+        return false if @exact && parameter.is_a?(Hash) && @entries.length != parameter.length
 
         has_entry_matchers = @entries.map { |key, value| HasEntry.new(key, value) }
         AllOf.new(*has_entry_matchers).matches?([parameter])
```
@evgeni Thanks for reporting - I'll take a look
Thanks!
I don't have a minimal reproducer (and don't want to force you to try to get to the bottom of our test suite), but the gist of it should be:
* Have a Rails ActionContoller that accepts parameters (that can be hashes!)
* These parameters get passed to a function (in our case we call `async_task(params[:add_content])`
* Have a test that mocks this function call: `@controller.expects(:async_task).with({'errata_ids' => [1,2,3]}`
* And then calls that controller:
```ruby
put :incremental_update, params: { :add_content => {:errata_ids => [1,2,3]} } 
assert_response :success
```
@evgeni No worries - I understand the problem and I have a solution in the works
@evgeni Would you be able to try out the fix in #664 i.e. in the `fix-regression-when-stubbed-method-expects-hash-but-receives-action-controller-parameters-object` branch?
Sure. Our tests are passing with that patch applied ontop of 2.4.4!
Fix released in [v2.4.5](https://rubygems.org/gems/mocha/versions/2.4.5).



Patch:
diff --git a/lib/mocha/parameter_matchers/has_entries.rb b/lib/mocha/parameter_matchers/has_entries.rb
index 2b90f34b8..51bb160b3 100644
--- a/lib/mocha/parameter_matchers/has_entries.rb
+++ b/lib/mocha/parameter_matchers/has_entries.rb
@@ -38,6 +38,7 @@ def initialize(entries, exact: false)
       # @private
       def matches?(available_parameters)
         parameter = available_parameters.shift
+        return false unless parameter
         return false if @exact && @entries.length != parameter.length
 
         has_entry_matchers = @entries.map { |key, value| HasEntry.new(key, value) }


Test patch:
diff --git a/test/acceptance/parameter_matcher_test.rb b/test/acceptance/parameter_matcher_test.rb
index 49cb58226..89b936d61 100644
--- a/test/acceptance/parameter_matcher_test.rb
+++ b/test/acceptance/parameter_matcher_test.rb
@@ -29,6 +29,15 @@ def test_should_not_match_hash_parameter_which_is_not_exactly_the_same
     assert_failed(test_result)
   end
 
+  def test_should_not_match_hash_parameter_when_method_invoked_with_no_parameters
+    test_result = run_as_test do
+      mock = mock()
+      mock.expects(:method).with(key_1: 'value_1')
+      mock.method
+    end
+    assert_failed(test_result)
+  end
+
   def test_should_match_hash_parameter_with_specified_key
     test_result = run_as_test do
       mock = mock()
diff --git a/test/unit/parameter_matchers/has_entries_test.rb b/test/unit/parameter_matchers/has_entries_test.rb
index 4f58347c0..d165e5c1f 100644
--- a/test/unit/parameter_matchers/has_entries_test.rb
+++ b/test/unit/parameter_matchers/has_entries_test.rb
@@ -17,6 +17,16 @@ def test_should_not_match_hash_not_including_specified_entries
     assert !matcher.matches?([{ key_1: 'value_1', key_2: 'value_2' }])
   end
 
+  def test_should_not_match_no_arguments_when_exact_match_not_required
+    matcher = has_entries(key_1: 'value_1')
+    assert !matcher.matches?([nil])
+  end
+
+  def test_should_not_match_no_arguments_when_exact_match_required
+    matcher = HasEntries.new({ key_1: 'value_1' }, exact: true)
+    assert !matcher.matches?([nil])
+  end
+
   def test_should_describe_matcher
     matcher = has_entries(key_1: 'value_1', key_2: 'value_2')
     description = matcher.mocha_inspect