-
-
Notifications
You must be signed in to change notification settings - Fork 357
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Correctly unstubs methods stubbed with
#any_instance
* Instances stubbed with `#any_instance` would not be usable after the test finished because `MethodDouble` would stash the implementation of the method already overridden by `AnyInstance::Recorder`. When the test finished, that implementation would be restored on the object's singleton class, and any future calls to it would blow up with a stack overflow. * This fix only stashes methods if they are defined on the object's singleton class to begin with; `AnyInstance::Recorder` defines a method on the object's class so that method will not be stashed. * If there is no method on the object's singleton class, RSpec can safely define one there without stashing the original implementation. At the end of the test, the method is simply removed entirely from the singleton class. Any original implementation defined in the object's ancestor chain will show through again. * This issue cannot be fixed on MRI 1.8.6 because it does not support `Method#owner`. However, `#any_instance` itself is not supported on 1.8.6 for the same reason. The fix should not negatively affect 1.8.6, though, because the fallback behavior is to stash the method in all cases (which was the original behavior). * This commit also refactors the stashing behavior out into its own object. While not explicitly necessary, it helped me reason about the fix much easier than when all the responsibility was in `MethodDouble` (which also has other responsibilities). * Fixes #167 * Closes #182
- Loading branch information
Showing
5 changed files
with
149 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# @private | ||
class StashedInstanceMethod | ||
def initialize(klass, method) | ||
@klass = klass | ||
@method = method | ||
|
||
@method_is_stashed = false | ||
end | ||
|
||
# @private | ||
def stash | ||
return if !method_defined_directly_on_klass? || @method_is_stashed | ||
|
||
@klass.__send__(:alias_method, stashed_method_name, @method) | ||
@method_is_stashed = true | ||
end | ||
|
||
private | ||
|
||
# @private | ||
def method_defined_directly_on_klass? | ||
method_defined_on_klass? && method_owned_by_klass? | ||
end | ||
|
||
# @private | ||
def method_defined_on_klass? | ||
@klass.method_defined?(@method) || @klass.private_method_defined?(@method) | ||
end | ||
|
||
if ::UnboundMethod.method_defined?(:owner) | ||
# @private | ||
def method_owned_by_klass? | ||
@klass.instance_method(@method).owner == @klass | ||
end | ||
else | ||
# @private | ||
def method_owned_by_klass? | ||
# On 1.8.6, which does not support Method#owner, we have no choice but | ||
# to assume it's defined on the klass even if it may be defined on | ||
# a superclass. | ||
true | ||
end | ||
end | ||
|
||
# @private | ||
def stashed_method_name | ||
"obfuscated_by_rspec_mocks__#{@method}" | ||
end | ||
|
||
public | ||
|
||
# @private | ||
def restore | ||
return unless @method_is_stashed | ||
|
||
@klass.__send__(:alias_method, @method, stashed_method_name) | ||
@klass.__send__(:remove_method, stashed_method_name) | ||
@method_is_stashed = false | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
require 'spec_helper' | ||
|
||
describe StashedInstanceMethod do | ||
class ExampleClass | ||
def hello | ||
:hello_defined_on_class | ||
end | ||
end | ||
|
||
def singleton_class_for(obj) | ||
class << obj; self; end | ||
end | ||
|
||
it "stashes the current implementation of an instance method so it can be temporarily replaced" do | ||
obj = Object.new | ||
def obj.hello; :hello_defined_on_singleton_class; end; | ||
|
||
stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello) | ||
stashed_method.stash | ||
|
||
def obj.hello; :overridden_hello; end | ||
expect(obj.hello).to eql :overridden_hello | ||
|
||
stashed_method.restore | ||
expect(obj.hello).to eql :hello_defined_on_singleton_class | ||
end | ||
|
||
it "stashes private instance methods" do | ||
obj = Object.new | ||
def obj.hello; :hello_defined_on_singleton_class; end; | ||
singleton_class_for(obj).__send__(:private, :hello) | ||
|
||
stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello) | ||
stashed_method.stash | ||
|
||
def obj.hello; :overridden_hello; end | ||
stashed_method.restore | ||
expect(obj.send(:hello)).to eql :hello_defined_on_singleton_class | ||
end | ||
|
||
it "only stashes methods directly defined on the given class, not its ancestors" do | ||
obj = ExampleClass.new | ||
|
||
stashed_method = StashedInstanceMethod.new(singleton_class_for(obj), :hello) | ||
stashed_method.stash | ||
|
||
def obj.hello; :overridden_hello; end; | ||
expect(obj.hello).to eql :overridden_hello | ||
|
||
stashed_method.restore | ||
expect(obj.hello).to eql :overridden_hello | ||
end | ||
end |
a727464
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.
There's a regression here. Check out the behavior of doing:
Bad behavior is in 328bd5d
Good behavior is in 645b158
Bad behavior, note that #where becomes undefined:
a727464
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. I'll dig into this ASAP.
a727464
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.
@alindeman -- I just ran into this in a project when I updated my bundle. I started to take a look at it and came up with a failing spec. It's in the method_wrongly_removed_when_stubbed_twice_bug branch:
93ef54b
Feel free to use this when you tackle this.
a727464
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 @myronmarston. That would have been my first step, so you've definitely saved me some time.
a727464
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.
@jonhyman, @myronmarston: I pushed dcc92e9 which hopefully fixes this. Please let me know if it doesn't for you. Thanks again for the test, @myronmarston.
a727464
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.
Works for me. Thanks.