-
-
Notifications
You must be signed in to change notification settings - Fork 358
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
Fix stubbing prepended only methods #1218
Fix stubbing prepended only methods #1218
Conversation
e253622
to
610871a
Compare
Thank you! This looks like a neat fix to me. Will wait for the build to go green before merging. |
@xaviershay Thanks for the support. I realized that there's still a failing test:
Which is: it 'uses the method signature from `#initialize` for arg verification' do
pending "Failing on JRuby due to https://github.com/jruby/jruby/issues/2565" if RSpec::Support::Ruby.jruby?
subclass = Class.new(klass) do
private_class_method :new
end
prevents(/arguments/) { allow(subclass).to receive(:new).with(1) }
allow(subclass).to receive(:new).with(1, 2)
end It's actually not failing, but it would mark the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @godfat!
lib/rspec/mocks/method_double.rb
Outdated
if Object.instance_method(:respond_to?).bind(object).call(@method_name, true) | ||
Object.instance_method(:method).bind(object).call(@method_name).owner | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple things...
- It looks like this is only used from
restore_original_visibility
, below. Can it be actuallyprivate
instead of just being hidden from the YARD docs via@private
? We use@private
only for methods that need to be publicly accessible for other parts of rspec-mocks to be able to call, but that we consider to not be part of our public API. - What's the reason for the
instance_method(...).bind(...).call(...)
business? (It'd probably be obvious if I had worked in rspec-mocks recently, but I haven't). Alternately, we do haveRSpec::Support.method_handle_for(object, method_name).call(*args)
that you could use that does something similar--it might be worth using here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good calls, thanks.
As for instance_method(...).bind(...).call(...)
, it's needed because the method method
and respond_to?
might be stubbed, and when that happened obviously it's not working.
I tried RSpec::Support.method_handle_for
but then spec/rspec/mocks/and_call_original_spec.rb:257
would fail with:
2) and_call_original on a partial double that overrides #method works for a singleton method
Failure/Error: RSpec::Support.method_handle_for(object, :method).call(@method_name).owner
ArgumentError:
wrong number of arguments (given 1, expected 0)
# /Users/godfat/project/fork/rspec-mocks/lib/rspec/mocks/method_double.rb:56:in `method'
All the other tests except the still failing one passed. Any idea? Maybe it's a bug in RSpec::Support.method_handle_for
?
Woops, with the slow wi-fi on this bus I didn't see your comment there @xaviershay as I was reviewing :). |
610871a
to
938dcbf
Compare
I pushed the current progress. Now the only failing test is: it "correctly restores the visibility of methods whose visibility has been tweaked on the singleton class" do
# hello is a private method when mixed in, but public on the module
# itself
mod = Module.new {
extend self
def hello; :hello; end
private :hello
class << self; public :hello; end;
}
expect(mod.hello).to eq(:hello)
allow(mod).to receive(:hello) { :stub }
reset mod
expect(mod.hello).to eq(:hello)
end Due to:
It's not passing because That method was a bit complex and I am still thinking a general way which could cover this edge case. |
|
||
if @method_stasher.method_is_stashed? | ||
@method_stasher.restore | ||
restore_original_visibility |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, we could remove all the checks in restore_original_visibility
when this is only called when @method_stasher.method_is_stashed?
938dcbf
to
6b87c96
Compare
Oh, Ruby 2.3.7+ passed but all the other older Rubies broke. We're really supporting very old rubies. I'll revisit this later as I don't have older rubies installed locally. Any help would be appreciated though, as I am personally not too interested in older rubies. I'll not force push too much from now as I don't want to pollute the related issue anymore. |
Is there any plan to drop support for older Rubies? |
In RSpec 4 we will drop some but not necessarily all older Rubies. |
Given the ruby 2 branch is mostly trouble free it is unlikely to be dropped |
Thanks. I could probably see if I could fix one more older ruby, but if that won't fix even older rubies, I think I am not looking into them then... |
c364a0a
to
29359ee
Compare
allow_any_instance_of on self extending mod. See: * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44417#note_430167033 * rspec/rspec-mocks#1218
5b5020a
to
a063443
Compare
bf20ad0
to
daaf184
Compare
@@ -165,8 +165,6 @@ class << self | |||
|
|||
context "on a class with a private `new`" do | |||
it 'uses the method signature from `#initialize` for arg verification' do | |||
pending "Failing on JRuby due to https://github.com/jruby/jruby/issues/2565" if RSpec::Support::Ruby.jruby? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jruby 9.2.13.0 can pass this so removing it, otherwise RSpec is complaining this is actually passing.
@@ -350,6 +363,21 @@ class << self; public :hello; end; | |||
expect(mod.hello).to eq(:hello) | |||
end | |||
|
|||
it "correctly restores from allow_any_instance_of for self extend" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test can catch the need for !Mocks.space.any_instance_recorder_for(owner, true)
in the other condition. It was discovered in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44417#note_429658296 thanks to @engwan !
d09b15f
to
dc3efde
Compare
I am in favor of merging this to 4-0-dev and move on. |
@benoittgt @godfat @JonRowe @pirj |
If it was rebased from that branch and build passed, yes. |
237bea9
to
2fbe1c2
Compare
I rebased against |
2fbe1c2
to
8013e34
Compare
Push-forced to trigger CI. Running. |
8013e34
to
b7e09e4
Compare
It doesn't seem like CI is running unless some maintainers approve this. Let me know what else needs to be done because I am not clear what we're missing. |
Approved, running. |
Thanks. Looks like it's all green. Do we need anything else?
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for all the hard work!
@JonRowe Should we merge to 4-0-dev, or wait until we release 4.0, and release this in 4.0.1?
@@ -249,6 +249,11 @@ def new_rspec_prepended_module | |||
end | |||
end | |||
|
|||
def method_owner | |||
@method_owner ||= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it affect the performance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's memoized so I don't think we'll see anything noticeable in practice. If we disregard memoization and run simple benchmark, object.method(method_name).owner
is about 3 times faster:
Warming up --------------------------------------
object.method.owner 261.517k i/100ms
bind and call 91.219k i/100ms
Calculating -------------------------------------
object.method.owner 2.661M (± 0.9%) i/s - 13.337M in 5.011938s
bind and call 914.184k (± 1.0%) i/s - 4.652M in 5.089434s
From running this:
require 'benchmark/ips'
Benchmark.ips do |x|
object = Object.new
x.report('object.method.owner') do
object.method(:to_s).owner
end
x.report('bind and call') do
Object.instance_method(:method).bind(object).call(:to_s).owner
end
end
Well, or we can remove the test and calling out that do not override method
, which I fully support :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm clearly on your side with you that one shouldn't override Kernel
's internals with unrelated application-level implementations. However, someone's code may break with a weird message, and they will open a ticket, and we'll have to address it, since even though this might not be a good style to override method
, it's not our call to enforce that in any way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, of course, that's why I was half joking there :)
Half joking, half serious, because there are a lot of other methods we're relying on, and of course we won't want to be so defensive to do it this way for most of the built-in methods? (or a rule of thumb is, whenever someone is filing a ticket 🤷 This is also relying on someone isn't trying to break RSpec intentionally)
I see method
or send
is more commonly overridden with a completely different behaviour (if it's the same behaviour like some people will override method_missing
also with respond_to_missing?
properly, that would be mostly alright, but issues might still happen especially for mocks implementation which often talks about proxies), and that's why we have __send__
we can use, sadly not for method
and it may be confusing that __method__
is actually a completely different method! Not like __send__
is an alias of send
.
Sorry that this is probably falling down to a rant. To be more on the topic, since the test is already there, I don't think a regression is acceptable. However if we never added that test, I would think it's fine to just say "don't override method
". Maybe RSpec 5, or maybe it doesn't matter after all. It's just one method right now. We don't have an army of Object.instance_method(method_name).bind(object)
yet. Or do we? Maybe it's inside one of the support libraries already? 😅 To be honest I won't be surprised!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We defend against a number of quite odd things:
# This class protects us against Mutex.new stubbed out within tests
rspec/rspec-core#2886, rspec/rspec-support#431 and in a few other places.
Maybe it's inside one of the support libraries already? 😅
Good point, it is! 😄
https://github.com/rspec/rspec-support/blob/d63133f478408c1d965e673b96ad10ef5a5d183f/lib/rspec/support.rb#L53
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We defend against a number of quite odd things:
Oh my, I was guilty here. I am sure I was once lazy and stubbed File.read
without explicit about the arguments, and it caused some issues. However being explicit about a specific argument did the trick for me.
I wonder if this kinds of things will need a more general approach. For example, will this work?
RSpec::Support::Source::File = ::File.dup
Good point, it is! 😄
Ok, thank you for spotting that. I updated the code to use it instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kinds of things
I can think of three cases we've touched:
- Ruby core method stubbed (
File.read
,File.expand_path
,Mutex.new
) - Ruby core method overridden in a subclass (
Kernel.method
) - Attempt to define a method with memoized helpers that would break
Example
(to_s
,initialize
)
Not sure if there could be a common solution to all of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby core method stubbed (File.read, File.expand_path, Mutex.new)
I just tried this does work:
PreservedFile = File.dup
def File.read
'break you'
end
PreservedFile.read('README.md')
We can have a list of preserved classes.
module RSpec
module Preserved
File = ::File.dup
Mutex = ::Mutex.dup
end
end
Ruby core method overridden in a subclass (Kernel.method)
We can always do it via the same trick.
module RSpec
module Preserved
module_function
def send_kernel(object, message, *args, **kargs)
::Kernel.instance_method(message).bind(object).call(*args, **kargs)
end
end
end
method_owner = RSpec::Preserved.send_kernel(object, :method).call(method_name).owner
Edited: Fixed above send_kernel
usage.
Attempt to define a method with memoized helpers that would break Example (to_s, initialize)
For to_s
and initialize
I think we should just raise an error like now, indeed. It would be too much work for those to be overridden. Even if we can stick with __to_s__
or __initialize__
and play around with allocate
, it's easily broken with other integration.
Not sure if there could be a common solution to all of this.
I didn't mean a common solution to all of above. I meant a common solution for the same issue. For example, we use the same way to resolve File.read
or Mutex.new
(a module method) got overridden. We use the same way to resolve a particular instance method got overridden.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our policy is to do the method work around for cases as they occur
b52a58d
to
b7f7700
Compare
Previously, we're assuming the method must be defined in the singleton class. However this is not always true. Whenever the method was only defined in the prepended module, then it's not defined in the singleton class. We need to find the owner of the method instead, which is the prepended module. Closes rspec#1213
69b5dd5
to
604dc95
Compare
@godfat, thanks a lot for getting this to the finish line, and for an interesting conversation. |
@fledman There's this ticket https://github.com/rspec/rspec/issues/61. This PR was not exactly planned for the release, but it turned out to be easier to target it for 4.0 rather than support all the older Ruby versions. If you wish to give RSpec 4 a try, you can do it right away: # Gemfile
%w[rspec rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib|
gem "#{lib}", github: "rspec/#{lib}", branch: '4-0-dev'
end Any pre-release bug report from a real-world project is highly appreciated. |
@pirj I got a relatively large codebase running successfully the most troublesome adjustment was the change to it was very unexpected that also some Ruby built-ins no longer being assertable this way e.g. otherwise it was just getting the Gemfile to resolve all the conflicts due to it being a pre-release:
|
I suppose it would have been helpful if there had been deprecation warnings for the switch from |
@fledman Thanks a lot for putting your time into this! We'll add deprecation warnings in 3.99, e.g. for I really hope strict predicate matchers are for the good.
Float::INFINITY.infinite? # => 1 Oh, that is true. Well, truthy. There were a couple of similar ones, and it's mostly surprising that they don't return |
@pirj maybe you can have both strict (true/false) and loose (truthy/falsey) forms of the be/have matchers? that way, regardless of which is the "default", it is easy to do inline adjustment some syntax possibilities:
alternatively there could be an explicitly truthy/falsey matcher |
…bbing-prepended-only-methods Fix stubbing prepended only methods --- This commit was imported from rspec/rspec-mocks@c84498e.
Previously, we're assuming the method must be defined in the
singleton class. However this is not always true. Whenever the
method was only defined in the prepended module, then it's not
defined in the singleton class. We need to find the owner of
the method instead, which is the prepended module.
Closes #1213