Repository: freerange/mocha (Ruby)

Issue:
Mocking a call with empty keyword arguments fails its parameter validation
Given the following code (a slight change from the example of `with`):
```ruby
object = mock()
object.expects(:expected_method).with(:param1, :param2)
kwargs = {}
object.expected_method(:param1, :param2, **kwargs)
```
This worked fine in Mocha 1.x, but this results in a pretty weird error message with Mocha 2.0.2:
```
unexpected invocation: #<Mock:0x1630>.expected_method(:param1, :param2, )
```
When we explicitly add an empty hash to the expectation, we can make the test pass with a warning that tells us to change the code:
```
Mocha deprecation warning at example.rb:4:in `test_kwargs': Expectation defined at example:2:in `test_kwargs' expected positional hash ({}), but received keyword arguments (). These will stop matching when strict keyword argument matching is enabled. See the documentation for Mocha::Configuration#strict_keyword_argument_matching=.
```
With `strict_keyword_argument_matching = true`, both options fail


Discussion: @herwinw

Thanks for reporting this. I've managed to reproduce the issue under Ruby v2.7 but not under Ruby 3.1. Is that what you're seeing too?

My initial reaction is surprise that this test passed with Mocha v1.x since as soon as you call `Expectation#with` an invocation needs to match all the expected arguments (and/or matchers) passed in to `Expectation#with`. In your example with Mocha v1.x it seems as if Mocha is *not* including the empty keyword argument hash in that matching process which seems inconsistent with how I would expect it to work.

Did you find that surprising too or was it the behaviour you expected? If it was, then can you explain why?
This was indeed Ruby 2.7, I forgot to mention that.

Mocha 1.x on Ruby 2.7 has a few other quirks, the following lines are equivalent:
```ruby
object.expects(:expected_method).with(:param1, :param2, foo: 1)
object.expects(:expected_method).with(:param1, :param2, { foo: 1 })
```
And for the caller too:
```ruby
object.expected_method(:param1, :param2, foo: 1)
object.expected_method(:param1, :param2, { foo: 1 })
```
This is the default Ruby 2.x behaviour when using a single splat argument to gather both positional and keyword arguments, so I can guess why and how this worked in Mocha 1.x.
> This was indeed Ruby 2.7, I forgot to mention that.

Thanks for confirming. And thanks for the extra info. I'm still interested in an answer to the question below if you have a chance:

> In your example with Mocha v1.x it seems as if Mocha is not including the empty keyword argument hash in that matching process which seems inconsistent with how I would expect it to work.
>
> Did you find that surprising too or was it the behaviour you expected? If it was, then can you explain why?

> Did you find that surprising too or was it the behaviour you expected? If it was, then can you explain why?

That is what I tried to explain with the extra info. If you only use the single splat operator for your argument capture in Ruby 2, the keyword arguments are converted to a hash that is added to the positional keyword arguments:
```
irb(main):001:0> ruby2_keywords def foo(*a) = a.to_s
=> nil
irb(main):002:0> foo(1, 2)
=> "[1, 2]"
irb(main):003:0> foo(1, 2, a: 1)
=> "[1, 2, {:a=>1}]"
irb(main):004:0> foo(a: 1)
=> "[{:a=>1}]"
```
So I guess I looked at it the other way around: I can guess the implementation based on the behaviour, so the behaviour does not surprise me
@herwinw

> That is what I tried to explain with the extra info.

Ah, I see what you mean now. However, rather than thinking about the implementation / the behaviour of Ruby and working backwards, I'm trying to think what would be least surprising to a user of Mocha.

Currently I think I'm leaning towards saying that the behaviour of Mocha v1.x in Ruby v2.7 is a bug, because it seems inconsistent / surprising to me. That would suggest releasing a patch version of v1.x to issue a deprecation warning and then another patch version of v1.x to "correct" the behaviour. It also assumes that there's a way to implement the deprecation warning and the correct behaviour - I haven't really thought much about that yet.

I think this would mean you would need to change the arguments or matchers in the call to `Expectation#with` in your test(s) to more explicitly expect the keyword arguments / final hash.

Does that make sense to you? Or am I missing something?

I guess that would make a backwards incompatible change, which should not happen in a minor release (according to semantic versioning).

Also: I'm not sure how well this change would work. The Gemspec for the latest 1.x release of Mocha says the minimal Ruby version is 1.9. Keyword arguments have kind of been a hacked on extension in Ruby for years, I expect this would break things.

My personal opinion: Don't change the behaviour of 1.x: it is too much effort and is very likely to break things. I'm not saying this is the best possible behaviour, but it's probably good enough given that nobody ever reported an issue for this behaviour.
The current 2.0.x release has issues when running with Ruby 2.7. I guess the best way of action is to either fix that, or bump the required/recommended Ruby version for the 2.x release to Ruby 3.0
@herwinw:

Sorry I haven't updated this issue in a while. I have been thinking about it, but haven't had much time to actually do anything!

A small improvement I could make fairly easily would be to change the way method invocations are represented - in particular so that empty keyword arguments are displayed more sensibly. This would at least mean the weird error message you originally reported would make a bit more sense!

Currently an invocation with keyword arguments vs a positional Hash are represented like this in error messages:

```ruby
foo.bar(:x => 1) # keyword arguments
foo.bar({:x => 1}) # positional Hash
```

i.e. the only distinction is the presence or absence of surrounding braces. Using the hash-rocket syntax is necessary to cope with the potential for keyword arguments with String keys.

However, keyword arguments can be empty, so how about: 

```ruby
foo.bar(**{:x => 1}) # keyword arguments
foo.bar({:x => 1}) # positional Hash
```

Then empty keyword arguments would be represented like this: 

```ruby
foo.bar(**{}) # keyword arguments
foo.bar({}) # positional Hash
```

The latter seems a bit clunky, but at least it's very explicit. I suppose I _could_ make empty keyword arguments an exception and just use the double-splat for that.

Thoughts?

I think only showing `**{` when it's empty would be clearer, the `foo.bar(**{:x => 1})` looks very complex, and it doesn't really add anything
> I think only showing `**{` when it's empty would be clearer, the `foo.bar(**{:x => 1})` looks very complex, and it doesn't really add anything

I tend to agree. 👍 

However, I'm also still not convinced the current approach of using the presence or absence of wrapping braces is a good way to distinguish a keyword `Hash` vs a standard `Hash`. And I think this might be even more true if we started using the Ruby v1.9 `Hash` format in `Mocha::HashMethods#mocha_inspect`. I'm going to give it some more thought.

#651 & #652 may be relevant here
Previously I had some concerns about the only distinction between a standard Hash and a keyword argument Hash being the wrapping braces. However, I'm now convinced that this is OK. Hence the changes in #719. Hopefully those changes can unblock this issue, but I'll need to find some time to look at it afresh.
@herwinw I'm sorry this issue got left by the wayside.

> I think only showing `**{` when it's empty would be clearer, the `foo.bar(**{:x => 1})` looks very complex, and it doesn't really add anything

I've just merged #720 which hopefully addresses at least the above aspect of this issue.

> My personal opinion: Don't change the behaviour of 1.x: it is too much effort and is very likely to break things. I'm not saying this is the best possible behaviour, but it's probably good enough given that nobody ever reported an issue for this behaviour.

Agreed, especially as so much time has now passed! 😊 

> The current 2.0.x release has issues when running with Ruby 2.7. I guess the best way of action is to either fix that, or bump the required/recommended Ruby version for the 2.x release to Ruby 3.0

Reading back through the comments on this issue, I'm a bit unclear whether there are any other specific issues that need addressing when running Mocha v2 on Ruby v2.7. If you think there are, can you open another GitHub issue? In the meantime, I'm going to close this one. I hope that's OK!


Honestly, I can't remember the exact issue we were facing here, I guess it was some constructs could not be written to run on both 2.7- and 3.0+. Since support for 2.7 has been dropped a few years ago, and I don't really care for supporting EOL Ruby versions, everything works from my point of view.
> Honestly, I can't remember the exact issue we were facing here, I guess it was some constructs could not be written to run on both 2.7- and 3.0+. Since support for 2.7 has been dropped a few years ago, and I don't really care for supporting EOL Ruby versions, everything works from my point of view.

Thanks 👍 



Fix (patch):
diff --git a/lib/mocha/inspect.rb b/lib/mocha/inspect.rb
index 70272c4a4..e5fd11e01 100755
--- a/lib/mocha/inspect.rb
+++ b/lib/mocha/inspect.rb
@@ -28,7 +28,11 @@ def mocha_inspect
           end
         end.join(', ')
 
-        Hash.ruby2_keywords_hash?(self) ? unwrapped : "{#{unwrapped}}"
+        if Hash.ruby2_keywords_hash?(self)
+          empty? ? '**{}' : unwrapped
+        else
+          "{#{unwrapped}}"
+        end
       end
     end