/
instance_method_stasher.rb
135 lines (114 loc) · 4.05 KB
/
instance_method_stasher.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
module RSpec
module Mocks
# @private
class InstanceMethodStasher
def initialize(object, method)
@object = object
@method = method
@klass = (class << object; self; end)
@original_method = nil
@method_is_stashed = false
end
attr_reader :original_method
if RUBY_VERSION.to_f < 1.9
# @private
def method_is_stashed?
@method_is_stashed
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
def stashed_method_name
"obfuscated_by_rspec_mocks__#{@method}"
end
private :stashed_method_name
# @private
def restore
return unless @method_is_stashed
if @klass.__send__(:method_defined?, @method)
@klass.__send__(:undef_method, @method)
end
@klass.__send__(:alias_method, @method, stashed_method_name)
@klass.__send__(:remove_method, stashed_method_name)
@method_is_stashed = false
end
else
# @private
def method_is_stashed?
!!@original_method
end
# @private
def stash
return if !method_defined_directly_on_klass?
@original_method ||= ::RSpec::Support.method_handle_for(@object, @method)
end
# @private
def restore
return unless @original_method
if @klass.__send__(:method_defined?, @method)
@klass.__send__(:undef_method, @method)
end
handle_restoration_failures do
@klass.__send__(:define_method, @method, @original_method)
end
@original_method = nil
end
end
if RUBY_DESCRIPTION.include?('2.0.0p247')
# ruby 2.0.0-p247 has a bug that we can't work around :(.
# https://bugs.ruby-lang.org/issues/8686
def handle_restoration_failures
yield
rescue TypeError
RSpec.warn_with(
"RSpec failed to properly restore a partial double (#{@object.inspect}) " +
"to its original state due to a known bug in MRI 2.0.0-p247 " +
"(https://bugs.ruby-lang.org/issues/8686). This object may remain " +
"screwed up for the rest of this process. Please upgrade to 2.0.0-p353 or above.",
:call_site => nil, :use_spec_location_as_call_site => true
)
end
else
def handle_restoration_failures
# No known reasons for restoration to fail on other rubies.
yield
end
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 = @klass)
MethodReference.method_defined_at_any_visibility?(klass, @method)
end
def method_owned_by_klass?
owner = @klass.instance_method(@method).owner
# On Ruby 2.0.0+ the owner of a method on a class which has been
# `prepend`ed may actually be an instance, e.g.
# `#<MyClass:0x007fbb94e3cd10>`, rather than the expected `MyClass`.
owner = owner.class unless Module === owner
# On some 1.9s (e.g. rubinius) aliased methods
# can report the wrong owner. Example:
# class MyClass
# class << self
# alias alternate_new new
# end
# end
#
# MyClass.owner(:alternate_new) returns `Class` when incorrect,
# but we need to consider the owner to be `MyClass` because
# it is not actually available on `Class` but is on `MyClass`.
# Hence, we verify that the owner actually has the method defined.
# If the given owner does not have the method defined, we assume
# that the method is actually owned by @klass.
owner == @klass || !(method_defined_on_klass?(owner))
end
end
end
end