File: double_injection.rb

package info (click to toggle)
ruby-rr 3.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,424 kB
  • sloc: ruby: 11,405; makefile: 7
file content (271 lines) | stat: -rw-r--r-- 9,522 bytes parent folder | download | duplicates (2)
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
      extend(Module.new do
        def find_or_create(subject_class, method_name)
          instances[subject_class][method_name.to_sym] ||= begin
            new(subject_class, method_name.to_sym).bind
          end
        end

        def find_or_create_by_subject(subject, method_name)
          find_or_create(class << subject; self; end, method_name)
        end

        def find(subject_class, method_name)
          instances[subject_class] && instances[subject_class][method_name.to_sym]
        end

        def find_by_subject(subject, method_name)
          find(class << subject; self; end, method_name)
        end

        def exists?(subject_class, method_name)
          !!find(subject_class, method_name)
        end

        def exists_by_subject?(subject, method_name)
          exists?((class << subject; self; end), method_name)
        end

        def dispatch_method(subject,
                            subject_class,
                            method_name,
                            arguments,
                            keyword_arguments,
                            block)
          subject_eigenclass = (class << subject; self; end)
          if (
            exists?(subject_class, method_name) &&
            ((subject_class == subject_eigenclass) || !subject.is_a?(Class))
          )
            find(subject_class, method_name.to_sym).dispatch_method(subject, arguments, keyword_arguments, block)
          else
            new(subject_class, method_name.to_sym).dispatch_original_method(subject, arguments, keyword_arguments, block)
          end
        end

        def reset
          instances.each do |subject_class, method_double_map|
            SingletonMethodAddedInjection.find(subject_class) && SingletonMethodAddedInjection.find(subject_class).reset
            method_double_map.keys.each do |method_name|
              reset_double(subject_class, method_name)
            end
            Injections::DoubleInjection.instances.delete(subject_class) if Injections::DoubleInjection.instances.has_key?(subject_class)
          end
        end

        def verify(*subjects)
          subject_classes = subjects.empty? ?
            Injections::DoubleInjection.instances.keys :
            subjects.map {|subject| class << subject; self; end}
          subject_classes.each do |subject_class|
            instances.include?(subject_class) &&
              instances[subject_class].keys.each do |method_name|
                verify_double(subject_class, method_name)
              end &&
              instances.delete(subject_class)
          end
        end

        # Verifies the DoubleInjection for the passed in subject and method_name.
        def verify_double(subject_class, method_name)
          Injections::DoubleInjection.find(subject_class, method_name).verify
        ensure
          reset_double subject_class, method_name
        end

        # Resets the DoubleInjection for the passed in subject and method_name.
        def reset_double(subject_class, method_name)
          double_injection = Injections::DoubleInjection.instances[subject_class].delete(method_name)
          double_injection.reset
          Injections::DoubleInjection.instances.delete(subject_class) if Injections::DoubleInjection.instances[subject_class].empty?
        end

        def instances
          @instances ||= HashWithObjectIdKey.new do |hash, subject_class|
            hash.set_with_object_id(subject_class, {})
          end
        end
      end)

      include ClassInstanceMethodDefined

      attr_reader :subject_class, :method_name, :doubles

      MethodArguments = Struct.new(:arguments,
                                   :keyword_arguments,
                                   :block)

      def initialize(subject_class, method_name)
        @subject_class = subject_class
        @method_name = method_name.to_sym
        @doubles = []
        @dispatch_method_delegates_to_dispatch_original_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_has_method_defined?(method_name)
          if subject_has_original_method?
            bind_method
          else
            bind_method_with_alias
          end
        else
          Injections::MethodMissingInjection.find_or_create(subject_class)
          Injections::SingletonMethodAddedInjection.find_or_create(subject_class)
          bind_method_that_self_destructs_and_delegates_to_method_missing
        end
        self
      end

      BoundObjects = {}

      def bind_method_that_self_destructs_and_delegates_to_method_missing
        id = BoundObjects.size
        BoundObjects[id] = subject_class

        subject_class.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
          def #{method_name}(*args, &block)
            ::RR::Injections::DoubleInjection::BoundObjects[#{id}].class_eval do
              remove_method(:#{method_name})
            end
            method_missing(:#{method_name}, *args, &block)
          end
          ruby2_keywords(:#{method_name}) if respond_to?(:ruby2_keywords, true)
        RUBY
        self
      end

      def bind_method
        id = BoundObjects.size
        BoundObjects[id] = subject_class

        if KeywordArguments.fully_supported?
          subject_class.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def #{method_name}(*args, **kwargs, &block)
              arguments = MethodArguments.new(args, kwargs, block)
              obj = ::RR::Injections::DoubleInjection::BoundObjects[#{id}]
              ::RR::Injections::DoubleInjection.dispatch_method(
                self,
                obj,
                :#{method_name},
                arguments.arguments,
                arguments.keyword_arguments,
                arguments.block
              )
            end
          RUBY
        else
          subject_class.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def #{method_name}(*args, &block)
              arguments = MethodArguments.new(args, {}, block)
              obj = ::RR::Injections::DoubleInjection::BoundObjects[#{id}]
              ::RR::Injections::DoubleInjection.dispatch_method(
                self,
                obj,
                :#{method_name},
                arguments.arguments,
                arguments.keyword_arguments,
                arguments.block
              )
            end
            ruby2_keywords(:#{method_name}) if respond_to?(:ruby2_keywords, true)
          RUBY
        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(subject, args, kwargs, block)
        if @dispatch_method_delegates_to_dispatch_original_method
          dispatch_original_method(subject, args, kwargs, block)
        else
          dispatch = MethodDispatches::MethodDispatch.new(
            self,
            subject,
            args,
            kwargs,
            block
          )
          dispatch.call
        end
      end

      def dispatch_original_method(subject, args, kwargs, block)
        dispatch = MethodDispatches::MethodDispatch.new(
          self,
          subject,
          args,
          kwargs,
          block
        )
        dispatch.call_original_method
      end

      def subject_has_original_method_missing?
        class_instance_method_defined(subject_class, MethodDispatches::MethodMissingDispatch.original_method_missing_alias_name)
      end

      def original_method_alias_name
        "__rr__original_#{@method_name}"
      end

      def dispatch_method_delegates_to_dispatch_original_method
        @dispatch_method_delegates_to_dispatch_original_method = true
        yield
      ensure
        @dispatch_method_delegates_to_dispatch_original_method = nil
      end

    protected
      def deferred_bind_method
        if respond_to?(method_name) and
            not 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
    end
  end
end