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 +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
|