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 +3 -0
- data/README.rdoc +1 -0
- data/Rakefile +5 -1
- data/VERSION.yml +1 -1
- data/lib/rr.rb +8 -1
- data/lib/rr/double.rb +18 -78
- data/lib/rr/double_definitions/double_definition_creator_proxy.rb +1 -1
- data/lib/rr/hash_with_object_id_key.rb +1 -3
- data/lib/rr/injections/double_injection.rb +119 -0
- data/lib/rr/injections/injection.rb +22 -0
- data/lib/rr/injections/method_missing_injection.rb +62 -0
- data/lib/rr/injections/singleton_method_added_injection.rb +58 -0
- data/lib/rr/method_dispatches/base_method_dispatch.rb +84 -0
- data/lib/rr/method_dispatches/method_dispatch.rb +58 -0
- data/lib/rr/method_dispatches/method_missing_dispatch.rb +59 -0
- data/lib/rr/space.rb +44 -4
- data/lib/rr/spy_verification_proxy.rb +1 -1
- data/spec/rr/double_injection/double_injection_spec.rb +523 -66
- data/spec/rr/double_injection/double_injection_verify_spec.rb +3 -3
- data/spec/rr/double_spec.rb +18 -18
- data/spec/rr/space/hash_with_object_id_key_spec.rb +1 -1
- data/spec/rr/space/space_spec.rb +250 -64
- data/spec/spec_helper.rb +5 -0
- metadata +9 -7
- data/lib/rr/double_injection.rb +0 -136
- data/spec/rr/double_injection/double_injection_bind_spec.rb +0 -99
- data/spec/rr/double_injection/double_injection_dispatching_spec.rb +0 -244
- data/spec/rr/double_injection/double_injection_has_original_method_spec.rb +0 -58
- data/spec/rr/double_injection/double_injection_reset_spec.rb +0 -72
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
|
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
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/
|
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
|
@@ -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
|