/
double_injection.rb
131 lines (115 loc) · 4.28 KB
/
double_injection.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
module RR
module Injections
# RR::DoubleInjection is the binding of an subject and a method.
# A double_injection has 0 to many Double objects. Each Double
# has Argument Expectations and Times called Expectations.
class DoubleInjection < Injection
attr_reader :subject_class, :method_name, :doubles
MethodArguments = Struct.new(:arguments, :block)
def initialize(subject, method_name, subject_class)
@subject = subject
@subject_class = subject_class
@method_name = method_name.to_sym
@doubles = []
@bypass_bound_method = nil
end
# RR::DoubleInjection#register_double adds the passed in Double
# into this DoubleInjection's list of Double objects.
def register_double(double)
@doubles << double
end
# RR::DoubleInjection#bind injects a method that acts as a dispatcher
# that dispatches to the matching Double when the method
# is called.
def bind
if subject_respond_to_method?(method_name)
if subject_has_method_defined?(method_name)
if subject_is_proxy_for_method?(method_name)
bind_method
else
bind_method_with_alias
end
else
space.method_missing_injection(subject)
space.singleton_method_added_injection(subject)
end
else
bind_method
end
self
end
# RR::DoubleInjection#verify verifies each Double
# TimesCalledExpectation are met.
def verify
@doubles.each do |double|
double.verify
end
end
# RR::DoubleInjection#reset removes the injected dispatcher method.
# It binds the original method implementation on the subject
# if one exists.
def reset
if subject_has_original_method?
subject_class.__send__(:remove_method, method_name)
subject_class.__send__(:alias_method, method_name, original_method_alias_name)
subject_class.__send__(:remove_method, original_method_alias_name)
else
if subject_has_method_defined?(method_name)
subject_class.__send__(:remove_method, method_name)
end
end
end
def dispatch_method(args, block)
dispatch = MethodDispatches::MethodDispatch.new(self, args, block)
if @bypass_bound_method
dispatch.call_original_method
else
dispatch.call
end
end
def dispatch_method_missing(method_name, args, block)
MethodDispatches::MethodMissingDispatch.new(subject, method_name, args, block).call
end
def subject_has_original_method_missing?
subject_respond_to_method?(original_method_missing_alias_name)
end
def original_method_alias_name
"__rr__original_#{@method_name}"
end
def original_method_missing_alias_name
MethodDispatches::MethodMissingDispatch.original_method_missing_alias_name
end
def bypass_bound_method
@bypass_bound_method = true
yield
ensure
@bypass_bound_method = nil
end
protected
def subject_is_proxy_for_method?(method_name)
subject_eigen = (class << @subject; self; end)
method_owner = subject_eigen.instance_methods.include?(method_name.to_s) ? subject_eigen.instance_method(method_name).owner : nil
!method_owner || !(subject_class.object_id == method_owner.object_id || subject_class.ancestors.include?(method_owner))
end
def deferred_bind_method
unless subject_has_method_defined?(original_method_alias_name)
bind_method_with_alias
end
@performed_deferred_bind = true
end
def bind_method_with_alias
subject_class.__send__(:alias_method, original_method_alias_name, method_name)
bind_method
end
def bind_method
subject = @subject.is_a?(Class) && !@subject.name.empty? ? @subject.name : "self"
subject_class.class_eval(<<-METHOD, __FILE__, __LINE__ + 1)
def #{@method_name}(*args, &block)
arguments = MethodArguments.new(args, block)
RR::Space.double_injection(#{subject}, :#{@method_name}).dispatch_method(arguments.arguments, arguments.block)
end
METHOD
end
end
end
end