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

3.10.3 breaks hash argument matching in Ruby 3+ #1460

Closed
alextwoods opened this issue Feb 2, 2022 · 11 comments
Closed

3.10.3 breaks hash argument matching in Ruby 3+ #1460

alextwoods opened this issue Feb 2, 2022 · 11 comments

Comments

@alextwoods
Copy link

alextwoods commented Feb 2, 2022

Subject of the issue

With rspec-mocks 3.10.3 we started seeing test failures in our CI: Previously passing run - fails now with rspec mocks 3.10.3 for ruby 3+. (Note, passes with rspec-mocks 3.10.2).

I believe the change in behavior is related to #1394.

Note: This issue is similar to #1457 but applies to a slightly different case

Your environment

  • Ruby version: 3.0.2
  • rspec-mocks version: 3.10.3

Steps to reproduce


class TestObject
  def initialize(object)
    @object = object
  end

  def foo(arg, options={})
    @object.foo(arg, options)
  end
end

describe 'rspec-mocks 3.10.3' do
  # This code passes in Ruby 2.7, but fails on Ruby 3.
  # Note: this test passes on Ruby 3 w/ rspec-mocks 3.10.2
  it 'breaks in ruby 3.0' do
    object = double('object')
    t = TestObject.new(object)

    expect(object).to receive(:foo).with('arg', opt: 'value')
    t.foo('arg', opt: 'value')
  end

  # This is the issue from: #1457 
  # this code passes in ruby 3
  it 'breaks in ruby 2.7' do
    obj = Object.new
    expect(obj).to receive(:call).with(:foo, **{})
    obj.call(:foo, **{})
  end
end

The above tests can be fixed by either:

  1. Changing the spec to: with('arg', {opt: 'value'})
  2. Or changing the code to use the double splat: @object.foo(arg, **options)

Expected behavior

Tests that pass in 3.10.2 should pass in 3.10.3.

Actual behavior

Some tests fail.

@pirj
Copy link
Member

pirj commented Feb 3, 2022

Thanks for reporting.

Do you want to give this patch a go to see if it resolves the problem?

@alextwoods
Copy link
Author

The patch does not fix the issue, but it does at least provide better failure output, eg:

  expected: ("source", opt_name: "opt-value")
       got: ("source", {:opt_name=>"opt-value"})

tombruijn added a commit to appsignal/appsignal-ruby that referenced this issue Feb 4, 2022
Something changed in the rspec-mocks gem between versions 3.10.2
and 3.10.3, because that's what causes tests to fail like this
on Ruby 3.1.0 and 3.0.3 locally and on the CI:

```
Failure/Error: monitor_transaction(name, env, &block)

  Appsignal received :monitor_transaction with unexpected arguments
    expected: ("perform_job.something", {:key=>:value})
         got: ("perform_job.something", {:key=>:value})
  Diff:
```

In this example the arguments are exactly the same, except they're not
the identical objects. This particular issue seems to be caused by a
recent change that for Ruby 3 keyword argument matching:
rspec/rspec-mocks#1460

## Rewriting specs

What helps is rewriting the tests to not mock method calls and check the
arguments given. Instead, assert what's being stored on the
transactions, like how we want all tests to be written as described in
issue #252.

## Adding curly brackets

For other specs that are a little more difficult to rewrite this way
right now, I've chosen to add additional curly brackets around the
argument expectations so that it's clear to Ruby/RSpec it's a hash
argument and not keywords arguments.

To solve this more permanently, we should rewrite the specs at some
point. We can remove these curly brackets if the issue is fixed in a
future rspec-mocks version as well.

[skip changeset]
@JonRowe
Copy link
Member

JonRowe commented Feb 4, 2022

What this issue is reporting is actually the correct behaviour. On Ruby 3:

expect(object).to receive(:foo).with('arg', opt: 'value')

This is setting an expectation of calling foo('arg', opt: 'value') with opt being a keyword argument. This is why passing in a proper hash is not matching, prior to 3.10.3 this was a bug and hashes would allowed to be silently be passed when keywords were required etc, and using your own snippet to demonstrate that hashes and keywords are not compatible:

class TestObject
  def initialize(object)
    @object = object
  end

  def foo(arg, options={})
    @object.foo(arg, options)
  end
end

class RealObject
  def foo(arg, opt:)
  end
end

TestObject.new(RealObject.new).foo('arg', opt: 'a')
# wrong number of arguments (given 2, expected 1; required keyword: opt) (ArgumentError)

In the naive sense, e.g. without verifying doubles, there is simply no way to do any other behaviour, we must fail a spec for not matching keywords vs hashes, as the default behaviour on Ruby3 matches this, so your example is unsolvable.

Now I expect prehaps in reality you are mocking a collaborator which has a defined behaviour, and indeed with instance doubles we could solve that scenario, where we have a real class we can see if the real object expects keywords or a hash, but we'd have to extend this existing behaviour in that scenario.

@JonRowe JonRowe changed the title 3.10.3 breaks keyword argument matching in Ruby 3+ 3.10.3 breaks hash argument matching in Ruby 3+ Feb 4, 2022
@alextwoods
Copy link
Author

I agree this is generally correct Ruby 3 behavior - a proper hash is not the same as keyword args. My issue is that a patch version change introduced behavior that makes matchers less permissive and breaks existing, working code.

In your RealObject example above, if the RealObject#foo takes a hash, rather than keyword args, then the code is valid and works as expected, eg:

class RealObject
  def foo(arg, options={})
  end
end

TestObject.new(RealObject.new).foo('arg', opt: 'a') # RealObject#foo will be called with option={opt: 'a'}

@JonRowe
Copy link
Member

JonRowe commented Feb 4, 2022

The problem is a generic double has no real implementation, so we should assume Ruby 3 semantics on Ruby 3.

Previously keyword arguments were broken, and this change fixed them. That it unforuntately makes some previous specs invalid (which were incorrect on Ruby 3) is sad but there is no way for a generic double to fix it. I'd happily review a PR to make instance verifying doubles work (.e.g. instance_double("ClassName")).

The simple work around is to set with('arg', {opt: 'a'})

@JonRowe
Copy link
Member

JonRowe commented Feb 4, 2022

The TL;DR is we cannot make expect(double()).to receive(:foo).with('arg', opt: :a) work with both keywords and a hash correctly, so we must pick the Ruby 3 semantics as correct.

tombruijn added a commit to appsignal/appsignal-ruby that referenced this issue Feb 7, 2022
Something changed in the rspec-mocks gem between versions 3.10.2
and 3.10.3, because that's what causes tests to fail like this
on Ruby 3.1.0 and 3.0.3 locally and on the CI:

```
Failure/Error: monitor_transaction(name, env, &block)

  Appsignal received :monitor_transaction with unexpected arguments
    expected: ("perform_job.something", {:key=>:value})
         got: ("perform_job.something", {:key=>:value})
  Diff:
```

In this example the arguments are exactly the same, except they're not
the identical objects. This particular issue seems to be caused by a
recent change that for Ruby 3 keyword argument matching:
rspec/rspec-mocks#1460

## Rewriting specs

What helps is rewriting the tests to not mock method calls and check the
arguments given. Instead, assert what's being stored on the
transactions, like how we want all tests to be written as described in
issue #252.

## Adding curly brackets

For other specs that are a little more difficult to rewrite this way
right now, I've chosen to add additional curly brackets around the
argument expectations so that it's clear to Ruby/RSpec it's a hash
argument and not keywords arguments.

To solve this more permanently, we should rewrite the specs at some
point. We can remove these curly brackets if the issue is fixed in a
future rspec-mocks version as well.

[skip changeset]
varyonic added a commit to varyonic/activeadmin-rails that referenced this issue Feb 7, 2022
Previously keyword arguments were broken, and this change fixed them. That it unforuntately makes some previous specs invalid (which were incorrect on Ruby 3) is sad.
ramonskie added a commit to cloudfoundry/bosh-openstack-cpi-release that referenced this issue Feb 10, 2022
mtasaka added a commit to mtasaka/yard that referenced this issue Feb 12, 2022
rspec-mocks 3.10.3 changed "with" syntax behavior to support
ruby 3 keyword arguments separation:

rspec/rspec-mocks#1394
rspec/rspec-mocks#1460

Fix yard testsuite accordingly.

Fixes lsegal#1432
joshcooper added a commit to joshcooper/puppet that referenced this issue Feb 17, 2022
Previously the test failed on ruby 3.0.3 and rspec 3.10.3 due to expecting a
method to be called with keyword arguments, but getting called with a hash.
The issue is described in [1], but the problem stems from using a "double" since
rspec has no way of knowing whether the "real" object accepts keyword args or a
hash. This commit avoids the issue entirely by using a real object.

[1] rspec/rspec-mocks#1460
@gingerlime
Copy link

We're seeing a similar issue(?) after upgrading rspec-rails, which updated rspec-mocks as a dependency...

     Failure/Error: update_async(user.id, properties)

       ClassName received :method with unexpected arguments
         expected: (5, {:subscription_started_date=>1646810075})
              got: (5, {:subscription_started_date=>1646810075})

aitor added a commit to devengoapp/pfs-ruby that referenced this issue Jun 7, 2022
dragon-dxw added a commit to dxw/mail-notify that referenced this issue Aug 8, 2022
We additionally pin rspec-mocks to 3.10.2 because 3.10.3
has a breaking change where keyword and hash behaviour is different.

rspec/rspec-mocks#1460

We should probably change the tests to handle the Ruby 3 native format,
but a working Ruby 3 instance is probably useful to developing that.
@raldred
Copy link

raldred commented Aug 25, 2022

We have this issue during upgrade to ruby 3, the diff is misleading.
It seems the diff can't distinguish between a hash being passed or keyword args.

In our case the hash being passed needed double splatting.

#<Double "check"> received :create with unexpected arguments
         expected: ("1111111-123123-123123", {:reports=>[{:name=>"identity"}, {:name=>"document"}], :tags=>["USA"], :type=>"express"})
              got: ("1111111-123123-123123", {:reports=>[{:name=>"identity"}, {:name=>"document"}], :tags=>["USA"], :type=>"express"})

Fix:

@api.check.create(applicant_id, **create_check_params(country_code: country_code))

@pirj
Copy link
Member

pirj commented Aug 25, 2022 via email

yaf pushed a commit to betagouv/rdv-service-public that referenced this issue Jan 27, 2023
* Update ruby to (3.1.2 -> 3.2.0) and bundler (2.3.7 -> 2.4.3)

* Leave rubocop TargetRubyVersion at 3.1.2, because upgrade is hard

* Use new shiny nokogiri version 1.14.0

* Update rspec-mocks to fix rspec/rspec-mocks#1460

* Fix specs since exception message now contains class name
yaf pushed a commit to betagouv/rdv-service-public that referenced this issue Jan 27, 2023
* Déplace la gestion du droit d'admin de territoire

* Update spec/requests/admin/territories/edit_territory_spec.rb

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Update app/views/admin/territories/agents/edit.html.slim

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Ruby 3.2.0 (#3253)

* Update ruby to (3.1.2 -> 3.2.0) and bundler (2.3.7 -> 2.4.3)

* Leave rubocop TargetRubyVersion at 3.1.2, because upgrade is hard

* Use new shiny nokogiri version 1.14.0

* Update rspec-mocks to fix rspec/rspec-mocks#1460

* Fix specs since exception message now contains class name

* Update spec/features/agents/admin_can_configure_the_territory_spec.rb

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Update spec/models/concerns/can_have_territorial_access_spec.rb

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Update spec/features/agents/admin_can_configure_the_territory_spec.rb

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Update spec/models/concerns/can_have_territorial_access_spec.rb

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>

* Retire un doublon

Co-authored-by: François Ferrandis <francois.ferrandis@beta.gouv.fr>
deborahchua added a commit to alphagov/support-api that referenced this issue Feb 9, 2023
Passing hash and keyword values as function parameters in tests
fail when using rspec-mocks >= v3.10.3.

See rspec/rspec-mocks#1460
for more information.
deborahchua added a commit to alphagov/support-api that referenced this issue Feb 28, 2023
Passing hash and keyword values as function parameters in tests
fail when using rspec-mocks >= v3.10.3.

See rspec/rspec-mocks#1460
for more information.
gclssvglx added a commit to alphagov/search-api that referenced this issue Mar 14, 2023
Running the tests at this point generates the following errors:

  expected: ({:body=>[{:index=>{:_id=>"/cheese", :_type=>"help_page"}}, {:link=>"/cheese", :title=>"We love cheese"}], :index=>"govuk_test"}) (keyword arguments)
       got: ({:body=>[{:index=>{:_id=>"/cheese", :_type=>"help_page"}}, {:link=>"/cheese", :title=>"We love cheese"}], :index=>"govuk_test"}) (options hash)

  expected: ({:body=>[{:delete=>{:_id=>"/cheese", :_type=>"help_page"}}], :index=>"govuk_test"}) (keyword arguments)
       got: ({:body=>[{:delete=>{:_id=>"/cheese", :_type=>"help_page"}}], :index=>"govuk_test"}) (options hash)

These are caused by passing hash and keyword values as function parameters in tests
when using rspec-mocks >= v3.10.3.

See rspec/rspec-mocks#1460 and
[here](alphagov/support-api@506bfe3) for more information.
david22swan added a commit to david22swan/puppetlabs-java_ks that referenced this issue Apr 18, 2023
david22swan added a commit to david22swan/puppetlabs-java_ks that referenced this issue Apr 18, 2023
RulerOf added a commit to RulerOf/kitchen-ec2 that referenced this issue Apr 19, 2023
RSpec tests under Ruby 3 were broken by ambiguous arguments. This patch
fixes the test suite under Ruby 3 and is backward compatible.

Discussion on this issue can be found in rspec repo:
rspec/rspec-mocks#1460
RulerOf added a commit to RulerOf/kitchen-ec2 that referenced this issue Apr 19, 2023
RSpec tests under Ruby 3 were broken by ambiguous arguments.
This patch fixes the test suite under Ruby 3 and is backward compatible.

Discussion on this issue can be found in rspec repo:
rspec/rspec-mocks#1460

Signed-off-by: Andrew Bobulsky <rulerof@gmail.com>
mec pushed a commit to dxw/mail-notify that referenced this issue Apr 28, 2023
We additionally pin rspec-mocks to 3.10.2 because 3.10.3
has a breaking change where keyword and hash behaviour is different.

rspec/rspec-mocks#1460

We should probably change the tests to handle the Ruby 3 native format,
but a working Ruby 3 instance is probably useful to developing that.
david22swan added a commit to david22swan/puppetlabs-java_ks that referenced this issue May 18, 2023
tas50 added a commit to test-kitchen/kitchen-ec2 that referenced this issue Jun 14, 2023
* fix(tests): Ruby 3 compatibility for RSpec suite

RSpec tests under Ruby 3 were broken by ambiguous arguments.
This patch fixes the test suite under Ruby 3 and is backward compatible.

Discussion on this issue can be found in rspec repo:
rspec/rspec-mocks#1460

Signed-off-by: Andrew Bobulsky <rulerof@gmail.com>

* Minor chefstyle fixes

Signed-off-by: Tim Smith <tsmith84@gmail.com>

---------

Signed-off-by: Andrew Bobulsky <rulerof@gmail.com>
Signed-off-by: Tim Smith <tsmith84@gmail.com>
Co-authored-by: Tim Smith <tsmith@chef.io>
Co-authored-by: Tim Smith <tim@mondoo.com>
msuzoagu added a commit to msuzoagu/codelog that referenced this issue Jul 12, 2023
body: Ruby 3.x keyword arguments (“kwargs”) are a first-class concept in Ruby 3.x onwards thus need to make it clear to Ruby/Rspec that arg is an actual hash not keyword argument.

footer:
- See [rspec/rspec-mocks#1460](rspec/rspec-mocks#1460)
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

No branches or pull requests

6 participants