rr 0.10.2 → 0.10.4

Sign up to get free protection for your applications and to get access to all the features.
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