Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Resolve an issue using method doubles with keyword arguments on Ruby 3#1385

Merged
JonRowe merged 5 commits into
rspec:mainfrom
bryanp:fix-kwargs-for-ruby3
Jan 7, 2021
Merged

Resolve an issue using method doubles with keyword arguments on Ruby 3#1385
JonRowe merged 5 commits into
rspec:mainfrom
bryanp:fix-kwargs-for-ruby3

Conversation

@bryanp

@bryanp bryanp commented Dec 29, 2020

Copy link
Copy Markdown

This captures and resolves an issue I encountered when running my test suite on Ruby 3. It may resolve more issues than just with and_call_original but that needs more investigation. Happy to fill in more test cases here but I haven't gone through to see what else uses MethodDouble yet.

@bryanp

bryanp commented Dec 29, 2020

Copy link
Copy Markdown
Author

Well this approach clearly has some issues for Ruby < 2.7.

@bryanp

bryanp commented Dec 29, 2020

Copy link
Copy Markdown
Author

1f2d12b fixes some of the test failures, but I'd prefer to use the ruby2_keywords shim. Are we okay adding that dependency?

Comment thread lib/rspec/mocks/method_double.rb Outdated
define_method(method_name) do |*args, &block|
method_double.proxy_method_invoked(self, *args, &block)
end
ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aha! Thats where its needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@bryanp 👏

Just wondering how did you figure it out? 2.7.1's warning traces are kind of unhelpful.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@pirj I found a test case that raised an exception, so there was a pretty strong hint in the backtrace:

     ArgumentError:
       wrong number of arguments (given 1, expected 0; required keyword: bar)
     # ./spec/rspec/mocks/and_call_original_spec.rb:189:in `foo'
     # ./lib/rspec/mocks/message_expectation.rb:101:in `call'
     # ./lib/rspec/mocks/message_expectation.rb:101:in `block in and_call_original'
     # ./lib/rspec/mocks/message_expectation.rb:740:in `call'
     # ./lib/rspec/mocks/message_expectation.rb:572:in `invoke_incrementing_actual_calls_by'
     # ./lib/rspec/mocks/message_expectation.rb:427:in `invoke'
     # ./lib/rspec/mocks/proxy.rb:217:in `message_received'
     # ./lib/rspec/mocks/proxy.rb:361:in `message_received'
     # ./lib/rspec/mocks/method_double.rb:78:in `proxy_method_invoked'
     # ./lib/rspec/mocks/method_double.rb:64:in `block (2 levels) in define_proxy_method'
     # ./spec/rspec/mocks/and_call_original_spec.rb:191:in `block (3 levels) in <top (required)>'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah yes, sadly 2.7 is less helpful

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for the inconvenience 🙏
Just a tip: if you run it on Ruby 3.0, it will raise an exception instead of a warning, which will show a more helpful backtrace.

def instance.foo(bar:); bar; end
expect(instance).to receive(:foo).and_call_original
expect(instance.foo(bar: "baz")).to eq("baz")
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs to be wrapped in an eval for our older ruby test suite, and skipped when theres no keyword argument support. (RSpec 3 supports 1.8.7 up).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ah, makes sense. Did that here: 1433a42

@pirj

pirj commented Dec 29, 2020

Copy link
Copy Markdown
Member

Triggered a dummy build to figure out if those failures are due to the change or unrelated #1386

@bryanp

bryanp commented Dec 30, 2020

Copy link
Copy Markdown
Author

Addressed feedback so far but still getting failures that seem unrelated to my change. Ideas?

@JonRowe

JonRowe commented Jan 1, 2021

Copy link
Copy Markdown
Member

So the build failure is from this change, I can reproduce it on a clean build with only that line changing (spec failed 3.0 and 2.7 but passed 2.5 2.6, add that one line and 3.0 and 2.7 pass but 2.5 2.6 fails)

The why still escapes me for the moment.

@bryanp

bryanp commented Jan 2, 2021

Copy link
Copy Markdown
Author

Have you reproduced the failures locally? I've only seen them on CI so far.

@JonRowe

JonRowe commented Jan 4, 2021

Copy link
Copy Markdown
Member

@bryanp yes, they are from rspec-rails when combined with this branch

@bryanp

bryanp commented Jan 5, 2021

Copy link
Copy Markdown
Author

@JonRowe What's the best way to locally run the rspec-rails specs against this branch?

@JonRowe

JonRowe commented Jan 6, 2021

Copy link
Copy Markdown
Member

@bryanp The RSpec repos will automatically run against the local version that sits in a folder next to them, e.g. if you have:

~/code/rspec-rails
~/code/rspec-mocks

Doing a bundle install in rspec-rails and then running the specs should fail if you have this branch checked out on rspec-mocks.

I am going to look at this when I have the time, but if you beat me to it I will be ecstatic :)

@bryanp

bryanp commented Jan 6, 2021

Copy link
Copy Markdown
Author

Aha, finally reproduced the failure. I'm looking specifically at this one:

rspec ./spec/rspec/rails/example/view_example_group_spec.rb:114 # RSpec::Rails::ViewExampleGroup routes helpers collides with asset helpers uses routes helpers

Here's the important part of the failure:

TypeError:
       can't create instance of singleton class
     # /Users/bryanp/.gem/ruby/2.6.6/gems/railties-6.0.3.4/lib/rails/railtie.rb:167:in `new'
     # /Users/bryanp/.gem/ruby/2.6.6/gems/railties-6.0.3.4/lib/rails/railtie.rb:167:in `instance'
     # /Users/bryanp/.gem/ruby/2.6.6/gems/railties-6.0.3.4/lib/rails/application.rb:100:in `instance'
     # /Users/bryanp/.gem/ruby/2.6.6/gems/railties-6.0.3.4/lib/rails/railtie.rb:183:in `respond_to_missing?'
     # /Users/bryanp/Code/Forks/rspec-mocks/lib/rspec/mocks/method_double.rb:67:in `respond_to?'
     # /Users/bryanp/Code/Forks/rspec-mocks/lib/rspec/mocks/method_double.rb:67:in `block in define_proxy_method'

Couple of observations:

  1. ruby2_keywords calls respond_to?.
  2. The context we call respond_to? on is a RSpecRails::Application class, which is a Railtie.
  3. Railtie::respond_to_missing? tries to create an instance, which we can't do because the context is a Singleton.

I don't yet understand how RSpecRails::Application is created, but an idea is to redefine ::instance to behave like Singleton::instance rather than Railtie::instance. There may be a more obvious solution that I'm missing here.

@pirj

pirj commented Jan 6, 2021

Copy link
Copy Markdown
Member

@bryanp Interesting!
We call if respond_to? ourselves there, it's not ruby2_keywords itself.
Do you think we can add the check for respond_to? somewhere up the chain? Rubies 2.5 & 2.6 don't have it.

Or probably use Module.respond_to?(:ruby2_keywords, true) and rely on the assumption that if Module responds to it, other classes do as well?

@bryanp

bryanp commented Jan 6, 2021

Copy link
Copy Markdown
Author

@pirj lol, you're right we do call respond_to?. I should have finished my coffee before diving into this today :)

I like the Module.respond_to?(:ruby2_keywords, true) idea, but maybe we should use TOPLEVEL_BINDING.receiver instead? That's the approach used in the ruby2_keywords gem here.

@pirj

pirj commented Jan 6, 2021

Copy link
Copy Markdown
Member

I have some serious problem understanding what is going on in that gem while looking at this line 😄

I don't mind either option. To me, Module.respond_to? is more compact.

@mame, can you please advise what is the best option for our case?
We are trying to call ruby2_keywords, but only if the object responds to it. Railtie#respond_to_missing? is calling instance and fails.

In order to avoid that respond_to? call on the current object, is it ok to call it on Module, Object, or TOPLEVEL_BINDING.receiver instead?

@JonRowe

JonRowe commented Jan 6, 2021

Copy link
Copy Markdown
Member

We do use the "unbind the real method and use that" to work around monkey patching in various places, maybe we need that trick in rspec-support for defining keyword support?

@mame

mame commented Jan 7, 2021

Copy link
Copy Markdown
Contributor

Hello!

I think Module.private_method_defined?(:ruby2_keywords) is the best option. If directly calling ruby2_keywords on Railtie is troublesome, Module.instance_method(:ruby2_keywords).bind(self).call(method_name) may work, as @JonRowe said.

Sorry, the check in ruby2_keywords gem looks wrong. Module.respond_to?(:ruby2_keywords) does NOT check if Module#ruby2_keywords is available. Fortunately, it works well because the check returns true in Ruby 2.7 or later due to the toplevel ruby2_keywords method. And thank you for letting me know about the syntactic error 🤣 I will talk with @nobu and will fix.

@bryanp

bryanp commented Jan 7, 2021

Copy link
Copy Markdown
Author

Thank you @mame for you input. I've made the suggested change.

This resolved the issues in Ruby 2.* but 1.8.7 and 1.9.2 are still failing in Travis. I haven't looked into why yet.

@mame

mame commented Jan 7, 2021

Copy link
Copy Markdown
Contributor

Looks like this issue is unrelated to this PR, this time for sure?

https://travis-ci.org/github/rspec/rspec-mocks/jobs/753288703

$ gem install bundler -v '< 2'
ERROR:  Could not find a valid gem 'bundler' (< 2), here is why:
          Unable to download data from https://rubygems.org/ - hostname was not match with the server certificate (https://api.rubygems.org/specs.4.8.gz)
The command "gem install bundler -v '< 2'" failed and exited with 2 during .

@pirj pirj force-pushed the fix-kwargs-for-ruby3 branch from 85504da to 2ca170b Compare January 7, 2021 08:25
@pirj

pirj commented Jan 7, 2021

Copy link
Copy Markdown
Member

There were some changes to .travis.yml, I've rebased this branch on main and force-pushed it.
However, chances of fixing the issue are quite low, since judging by the list of jobs (1.8.7, 1.9.2, ree, jruby-1.7) that the failing build was using the .travis.yml from main anyway, not the one from this branch that also lists 1.9.3 and 2.0.0. I vaguely remember that was the case, can't remember why this is the case.

@JonRowe JonRowe merged commit 117b33d into rspec:main Jan 7, 2021
@JonRowe

JonRowe commented Jan 7, 2021

Copy link
Copy Markdown
Member

Travis is broken and I'm replacing the builds as I can.

@pirj

pirj commented Jan 7, 2021

Copy link
Copy Markdown
Member

@mame Thanks a lot for your help!

@bryanp Thanks a lot for dealing with this issue!

JonRowe added a commit that referenced this pull request Jan 7, 2021
@bryanp bryanp deleted the fix-kwargs-for-ruby3 branch January 7, 2021 13:38
@pirj pirj mentioned this pull request Jan 13, 2021
JonRowe added a commit that referenced this pull request Jan 15, 2021
Resolve an issue using method doubles with keyword arguments on Ruby 3
JonRowe added a commit that referenced this pull request Jan 15, 2021
JonRowe added a commit that referenced this pull request Jan 15, 2021
Resolve an issue using method doubles with keyword arguments on Ruby 3
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
…rgs-for-ruby3

Resolve an issue using method doubles with keyword arguments on Ruby 3

---
This commit was imported from rspec/rspec-mocks@117b33d.
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
…rgs-for-ruby3

Resolve an issue using method doubles with keyword arguments on Ruby 3

---
This commit was imported from rspec/rspec-mocks@a0f33ee.
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 6, 2021
yujinakayama pushed a commit to yujinakayama/rspec-monorepo that referenced this pull request Oct 19, 2021
…rgs-for-ruby3

Resolve an issue using method doubles with keyword arguments on Ruby 3

---
This commit was imported from rspec/rspec-mocks@0306f46.
@eregon

eregon commented Mar 24, 2022

Copy link
Copy Markdown
Contributor

I've run into this weird issue with respond_to?(:ruby2_keywords, true) as well in #1464 and wondered for a while what the heck was going on. bundle exec rspec -b spec in rspec-rails helps to see the first error, which helps. The following errors are all completely undecipherable.

For some reason it only happens on 2.5/2.6, I guess because 1) 2.7+ have the methods, so don't fall into the trap, and 2) only those Ruby versions test the Rails version with this weird behavior? (which still exists on Rails main)
What's pretty crazy here is that the Railtie class defines respond_to_missing? on the Railtie class, not for instances, and try to create an instance in respond_to_missing?, which is definitely not expected for a respond_to_missing? implementation.

@eregon

eregon commented Mar 24, 2022

Copy link
Copy Markdown
Contributor

Sorry, the check in ruby2_keywords gem looks wrong. Module.respond_to?(:ruby2_keywords) does NOT check if Module#ruby2_keywords is available.

FWIW, that was fine and no bug, it finds Module#ruby2_keywords, since it's called on the Module class, which is a module (as all classes are).

@eregon

eregon commented Mar 24, 2022

Copy link
Copy Markdown
Contributor

I've filed an issue to Rails about this, and found at least one spot where they unintentionally create a Railtie instance by a respond_to? check: rails/rails#44761

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants