rr 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,3 +1,6 @@
1
+ - Handle lazily defined methods (where respond_to? returns true yet the method is not yet defined and the first call to method_missing defines the method). This pattern is used in ActiveRecord and ActionMailer.
2
+ - Fixed warning about aliasing #instance_exec in jruby. http://github.com/btakita/rr/issues#issue/9 (Patch by Nathan Sobo)
3
+
1
4
  0.10.2
2
5
  - RR properly proxies subjects with private methods [http://github.com/btakita/rr/issues/#issue/7]. Identified by Matthew O'Connor.
3
6
 
data/README.rdoc CHANGED
@@ -322,6 +322,7 @@ With any development effort, there are countless people who have contributed
322
322
  to making it possible. We all are standing on the shoulders of giants.
323
323
  * Andreas Haller for patches
324
324
  * Aslak Hellesoy for Developing Rspec
325
+ * Christopher Redinger for patches
325
326
  * Dan North for syntax ideas
326
327
  * Dave Astels for some BDD inspiration
327
328
  * David Chelimsky for encouragement to make the RR framework, for developing the Rspec mock framework, syntax ideas, and patches
data/Rakefile CHANGED
@@ -47,7 +47,11 @@ begin
47
47
  rescue LoadError
48
48
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
49
49
  end
50
- RUBYFORGE_PACKAGE_NAME = "rr (Double R)"
50
+ RUBYFORGE_PACKAGE_NAME = "rr (Double Ruby)"
51
+ # The package was renamed from "rr (Double R)" to "rr (Double Ruby)".
52
+ # When this was last run, the script did not work for the new name but it did work for the old name.
53
+ # Perhaps more time was needed for the name change to propagate?
54
+ #RUBYFORGE_PACKAGE_NAME = "rr (Double R)"
51
55
 
52
56
  # This is hacked to get around the 3 character limitation for package names on Rubyforge.
53
57
  # http://rubyforge.org/tracker/index.php?func=detail&aid=27026&group_id=5&atid=102
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :minor: 10
3
- :patch: 2
3
+ :patch: 4
4
4
  :major: 0
data/lib/rr.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  dir = File.dirname(__FILE__)
2
2
  require 'rubygems'
3
+ require 'forwardable'
3
4
 
4
5
  require "#{dir}/rr/errors/rr_error"
5
6
  require "#{dir}/rr/errors/subject_does_not_implement_method_error"
@@ -14,7 +15,13 @@ require "#{dir}/rr/errors/spy_verification_errors/double_injection_not_found_err
14
15
  require "#{dir}/rr/errors/spy_verification_errors/invocation_count_error"
15
16
 
16
17
  require "#{dir}/rr/space"
17
- require "#{dir}/rr/double_injection"
18
+ require "#{dir}/rr/injections/injection"
19
+ require "#{dir}/rr/injections/double_injection"
20
+ require "#{dir}/rr/injections/method_missing_injection"
21
+ require "#{dir}/rr/injections/singleton_method_added_injection"
22
+ require "#{dir}/rr/method_dispatches/base_method_dispatch"
23
+ require "#{dir}/rr/method_dispatches/method_dispatch"
24
+ require "#{dir}/rr/method_dispatches/method_missing_dispatch"
18
25
  require "#{dir}/rr/hash_with_object_id_key"
19
26
  require "#{dir}/rr/recorded_calls"
20
27
  require "#{dir}/rr/proc_from_block"
data/lib/rr/double.rb CHANGED
@@ -28,22 +28,6 @@ module RR
28
28
  verify_method_signature if definition.verify_method_signature?
29
29
  double_injection.register_double self
30
30
  end
31
-
32
- # Double#call calls the Double's implementation. The return
33
- # value of the implementation is returned.
34
- #
35
- # A TimesCalledError is raised when the times called
36
- # exceeds the expected TimesCalledExpectation.
37
- def call(double_injection, *args, &block)
38
- if verbose?
39
- puts Double.formatted_name(double_injection.method_name, args)
40
- end
41
- times_called_expectation.attempt if definition.times_matcher
42
- space.verify_ordered_double(self) if ordered?
43
- yields!(block)
44
- return_value = call_implementation(double_injection, *args, &block)
45
- definition.after_call_proc ? extract_subject_from_return_value(definition.after_call_proc.call(return_value)) : return_value
46
- end
47
31
 
48
32
  # Double#exact_match? returns true when the passed in arguments
49
33
  # exactly match the ArgumentEqualityExpectation arguments.
@@ -98,6 +82,18 @@ module RR
98
82
  self.class.formatted_name(method_name, expected_arguments)
99
83
  end
100
84
 
85
+ def method_call(args)
86
+ if verbose?
87
+ puts Double.formatted_name(method_name, args)
88
+ end
89
+ times_called_expectation.attempt if definition.times_matcher
90
+ space.verify_ordered_double(self) if ordered?
91
+ end
92
+
93
+ def implementation_is_original_method?
94
+ definition.implementation_is_original_method?
95
+ end
96
+
101
97
  protected
102
98
  def ordered?
103
99
  definition.ordered?
@@ -106,21 +102,6 @@ module RR
106
102
  def verbose?
107
103
  definition.verbose?
108
104
  end
109
-
110
- def yields!(block)
111
- if definition.yields_value
112
- if block
113
- block.call(*definition.yields_value)
114
- else
115
- raise ArgumentError, "A Block must be passed into the method call when using yields"
116
- end
117
- end
118
- end
119
-
120
- def call_implementation(double_injection, *args, &block)
121
- return_value = do_call_implementation_and_get_return_value(double_injection, *args, &block)
122
- extract_subject_from_return_value(return_value)
123
- end
124
105
 
125
106
  def verify_times_matcher_is_set
126
107
  unless definition.times_matcher
@@ -138,19 +119,19 @@ module RR
138
119
  raise RR::Errors::SubjectDoesNotImplementMethodError unless definition.subject.respond_to?(double_injection.send(:original_method_alias_name))
139
120
  raise RR::Errors::SubjectHasDifferentArityError unless arity_matches?
140
121
  end
141
-
122
+
142
123
  def subject_arity
143
124
  definition.subject.method(double_injection.send(:original_method_alias_name)).arity
144
125
  end
145
-
126
+
146
127
  def subject_accepts_only_varargs?
147
128
  subject_arity == -1
148
129
  end
149
-
130
+
150
131
  def subject_accepts_varargs?
151
132
  subject_arity < 0
152
133
  end
153
-
134
+
154
135
  def arity_matches?
155
136
  return true if subject_accepts_only_varargs?
156
137
  if subject_accepts_varargs?
@@ -159,54 +140,13 @@ module RR
159
140
  return subject_arity == args.size
160
141
  end
161
142
  end
162
-
143
+
163
144
  def args
164
145
  definition.argument_expectation.expected_arguments
165
146
  end
166
-
167
- def do_call_implementation_and_get_return_value(double_injection, *args, &block)
168
- if definition.implementation_is_original_method?
169
- if double_injection.object_has_original_method?
170
- double_injection.call_original_method(*args, &block)
171
- else
172
- double_injection.subject.__send__(
173
- :method_missing,
174
- method_name,
175
- *args,
176
- &block
177
- )
178
- end
179
- else
180
- if implementation
181
- if implementation.is_a?(Method)
182
- implementation.call(*args, &block)
183
- else
184
- args << ProcFromBlock.new(&block) if block
185
- implementation.call(*args)
186
- end
187
- else
188
- nil
189
- end
190
- end
191
- end
192
-
193
- def extract_subject_from_return_value(return_value)
194
- case return_value
195
- when DoubleDefinitions::DoubleDefinition
196
- return_value.root_subject
197
- when DoubleDefinitions::DoubleDefinitionCreatorProxy
198
- return_value.__creator__.root_subject
199
- else
200
- return_value
201
- end
202
- end
203
-
204
- def implementation
205
- definition.implementation
206
- end
207
147
 
208
148
  def argument_expectation
209
149
  definition.argument_expectation
210
- end
150
+ end
211
151
  end
212
152
  end
@@ -4,7 +4,7 @@ module RR
4
4
  class << self
5
5
  def blank_slate_methods
6
6
  instance_methods.each do |m|
7
- unless m =~ /^_/ || m.to_s == 'object_id' || m.to_s == 'respond_to?' || m.to_s == 'method_missing'
7
+ unless m =~ /^_/ || m.to_s == 'object_id' || m.to_s == 'respond_to?' || m.to_s == 'method_missing' || m.to_s == 'instance_eval' || m.to_s == 'instance_exec'
8
8
  alias_method "__blank_slated_#{m}", m
9
9
  undef_method m
10
10
  end
@@ -3,9 +3,7 @@ module RR
3
3
  class HashWithObjectIdKey < ::Hash #:nodoc:
4
4
  def initialize
5
5
  @keys = {}
6
- super do |hash, subject_object|
7
- hash.set_with_object_id(subject_object, {})
8
- end
6
+ super
9
7
  end
10
8
 
11
9
  alias_method :get_with_object_id, :[]
@@ -0,0 +1,119 @@
1
+ module RR
2
+ module Injections
3
+ # RR::DoubleInjection is the binding of an subject and a method.
4
+ # A double_injection has 0 to many Double objects. Each Double
5
+ # has Argument Expectations and Times called Expectations.
6
+ class DoubleInjection < Injection
7
+ attr_reader :subject_class, :method_name, :doubles
8
+
9
+ MethodArguments = Struct.new(:arguments, :block)
10
+
11
+ def initialize(subject, method_name, subject_class)
12
+ @subject = subject
13
+ @subject_class = subject_class
14
+ @method_name = method_name.to_sym
15
+ @doubles = []
16
+ end
17
+
18
+ # RR::DoubleInjection#register_double adds the passed in Double
19
+ # into this DoubleInjection's list of Double objects.
20
+ def register_double(double)
21
+ @doubles << double
22
+ end
23
+
24
+ # RR::DoubleInjection#bind injects a method that acts as a dispatcher
25
+ # that dispatches to the matching Double when the method
26
+ # is called.
27
+ def bind
28
+ if subject_respond_to_method?(method_name)
29
+ if subject_has_method_defined?(method_name)
30
+ bind_method_with_alias
31
+ else
32
+ space.method_missing_injection(subject)
33
+ space.singleton_method_added_injection(subject)
34
+ end
35
+ else
36
+ bind_method
37
+ end
38
+ self
39
+ end
40
+
41
+ # RR::DoubleInjection#verify verifies each Double
42
+ # TimesCalledExpectation are met.
43
+ def verify
44
+ @doubles.each do |double|
45
+ double.verify
46
+ end
47
+ end
48
+ # RR::DoubleInjection#reset removes the injected dispatcher method.
49
+
50
+ # It binds the original method implementation on the subject
51
+ # if one exists.
52
+ def reset
53
+ if subject_has_original_method?
54
+ subject_class.__send__(:alias_method, method_name, original_method_alias_name)
55
+ subject_class.__send__(:remove_method, original_method_alias_name)
56
+ else
57
+ if subject_has_method_defined?(method_name)
58
+ subject_class.__send__(:remove_method, method_name)
59
+ end
60
+ end
61
+ end
62
+
63
+ def dispatch_method(args, block)
64
+ dispatch = MethodDispatches::MethodDispatch.new(self, args, block)
65
+ if @bypass_bound_method
66
+ dispatch.call_original_method
67
+ else
68
+ dispatch.call
69
+ end
70
+ end
71
+
72
+ def dispatch_method_missing(method_name, args, block)
73
+ MethodDispatches::MethodMissingDispatch.new(subject, method_name, args, block).call
74
+ end
75
+
76
+ def subject_has_original_method_missing?
77
+ subject_respond_to_method?(original_method_missing_alias_name)
78
+ end
79
+
80
+ def original_method_alias_name
81
+ "__rr__original_#{@method_name}"
82
+ end
83
+
84
+ def original_method_missing_alias_name
85
+ MethodDispatches::MethodMissingDispatch.original_method_missing_alias_name
86
+ end
87
+
88
+ def bypass_bound_method
89
+ @bypass_bound_method = true
90
+ yield
91
+ ensure
92
+ @bypass_bound_method = nil
93
+ end
94
+
95
+ protected
96
+ def deferred_bind_method
97
+ unless subject_has_method_defined?(original_method_alias_name)
98
+ bind_method_with_alias
99
+ end
100
+ @performed_deferred_bind = true
101
+ end
102
+
103
+ def bind_method_with_alias
104
+ subject_class.__send__(:alias_method, original_method_alias_name, method_name)
105
+ bind_method
106
+ end
107
+
108
+ def bind_method
109
+ returns_method = <<-METHOD
110
+ def #{@method_name}(*args, &block)
111
+ arguments = MethodArguments.new(args, block)
112
+ RR::Space.double_injection(self, :#{@method_name}).dispatch_method(arguments.arguments, arguments.block)
113
+ end
114
+ METHOD
115
+ subject_class.class_eval(returns_method, __FILE__, __LINE__ - 5)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,22 @@
1
+ module RR
2
+ module Injections
3
+ class Injection
4
+ include Space::Reader
5
+
6
+ attr_reader :subject
7
+
8
+ def subject_has_method_defined?(method_name)
9
+ @subject.methods.include?(method_name.to_s) || @subject.protected_methods.include?(method_name.to_s) || @subject.private_methods.include?(method_name.to_s)
10
+ end
11
+
12
+ def subject_has_original_method?
13
+ subject_respond_to_method?(original_method_alias_name)
14
+ end
15
+
16
+ protected
17
+ def subject_respond_to_method?(method_name)
18
+ subject_has_method_defined?(method_name) || @subject.respond_to?(method_name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,62 @@
1
+ module RR
2
+ module Injections
3
+ class MethodMissingInjection < Injection
4
+ def initialize(subject)
5
+ @subject = subject
6
+ end
7
+
8
+ def bind
9
+ unless subject.respond_to?(original_method_alias_name)
10
+ unless subject.respond_to?(:method_missing)
11
+ @placeholder_method_defined = true
12
+ subject_class.class_eval do
13
+ def method_missing(method_name, *args, &block)
14
+ super
15
+ end
16
+ end
17
+ end
18
+ subject_class.__send__(:alias_method, original_method_alias_name, :method_missing)
19
+ bind_method
20
+ end
21
+ self
22
+ end
23
+
24
+ def reset
25
+ if subject_has_method_defined?(original_method_alias_name)
26
+ memoized_original_method_alias_name = original_method_alias_name
27
+ placeholder_method_defined = @placeholder_method_defined
28
+ subject_class.class_eval do
29
+ if placeholder_method_defined
30
+ remove_method :method_missing
31
+ else
32
+ alias_method :method_missing, memoized_original_method_alias_name
33
+ end
34
+ remove_method memoized_original_method_alias_name
35
+ end
36
+ end
37
+ end
38
+
39
+ def dispatch_method(method_name, args, block)
40
+ MethodDispatches::MethodMissingDispatch.new(subject, method_name, args, block).call
41
+ end
42
+
43
+ protected
44
+ def subject_class
45
+ class << subject; self; end
46
+ end
47
+
48
+ def bind_method
49
+ returns_method = <<-METHOD
50
+ def method_missing(method_name, *args, &block)
51
+ RR::Space.method_missing_injection(self).dispatch_method(method_name, args, block)
52
+ end
53
+ METHOD
54
+ subject_class.class_eval(returns_method, __FILE__, __LINE__ - 4)
55
+ end
56
+
57
+ def original_method_alias_name
58
+ MethodDispatches::MethodMissingDispatch.original_method_missing_alias_name
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ module RR
2
+ module Injections
3
+ class SingletonMethodAddedInjection < Injection
4
+ def initialize(subject)
5
+ @subject = subject
6
+ end
7
+
8
+ def bind
9
+ unless subject.respond_to?(original_method_alias_name)
10
+ unless subject.respond_to?(:singleton_method_added)
11
+ @placeholder_method_defined = true
12
+ subject_class.class_eval do
13
+ def singleton_method_added(method_name)
14
+ super
15
+ end
16
+ end
17
+ end
18
+
19
+ memoized_subject = subject
20
+ memoized_space = space
21
+ memoized_original_method_alias_name = original_method_alias_name
22
+ subject_class.__send__(:alias_method, original_method_alias_name, :singleton_method_added)
23
+ subject_class.__send__(:define_method, :singleton_method_added) do |method_name_arg|
24
+ if memoized_space.double_injection_exists?(memoized_subject, method_name_arg)
25
+ memoized_space.double_injection(memoized_subject, method_name_arg).send(:deferred_bind_method)
26
+ end
27
+ send(memoized_original_method_alias_name, method_name_arg)
28
+ end
29
+ end
30
+ self
31
+ end
32
+
33
+ def reset
34
+ if subject_has_method_defined?(original_method_alias_name)
35
+ memoized_original_method_alias_name = original_method_alias_name
36
+ placeholder_method_defined = @placeholder_method_defined
37
+ subject_class.class_eval do
38
+ if placeholder_method_defined
39
+ remove_method :singleton_method_added
40
+ else
41
+ alias_method :singleton_method_added, memoized_original_method_alias_name
42
+ end
43
+ remove_method memoized_original_method_alias_name
44
+ end
45
+ end
46
+ end
47
+
48
+ protected
49
+ def subject_class
50
+ class << subject; self; end
51
+ end
52
+
53
+ def original_method_alias_name
54
+ "__rr__original_singleton_method_added"
55
+ end
56
+ end
57
+ end
58
+ end