surrogate 0.5.5 → 0.6.0
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/.rvmrc +0 -3
- data/Changelog.md +21 -0
- data/Readme.md +64 -76
- data/Readme.md.mountain_berry_fields +70 -77
- data/gemfiles/rspec_mocks_2.11 +1 -1
- data/lib/surrogate.rb +1 -1
- data/lib/surrogate/api_comparer.rb +59 -61
- data/lib/surrogate/argument_errorizer.rb +43 -0
- data/lib/surrogate/endower.rb +41 -37
- data/lib/surrogate/hatchery.rb +6 -1
- data/lib/surrogate/hatchling.rb +5 -0
- data/lib/surrogate/method_definition.rb +55 -0
- data/lib/surrogate/porc_reflector.rb +43 -0
- data/lib/surrogate/rspec/invocation_matcher.rb +2 -1
- data/lib/surrogate/rspec/substitute_for.rb +23 -7
- data/lib/surrogate/rspec/with_filter.rb +0 -1
- data/lib/surrogate/surrogate_class_reflector.rb +65 -0
- data/lib/surrogate/surrogate_instance_reflector.rb +15 -0
- data/lib/surrogate/version.rb +1 -1
- data/spec/acceptance_spec.rb +1 -1
- data/spec/defining_api_methods_spec.rb +51 -77
- data/spec/other_shit_spec.rb +131 -0
- data/spec/rspec/block_support_spec.rb +2 -2
- data/spec/rspec/initialization_matcher_spec.rb +12 -4
- data/spec/rspec/rspec_mocks_integration_spec.rb +1 -1
- data/spec/rspec/substitute_for_spec.rb +90 -4
- data/spec/unit/api_comparer_spec.rb +120 -4
- data/spec/unit/argument_errorizer_spec.rb +50 -0
- data/surrogate.gemspec +0 -2
- data/todo +44 -0
- metadata +21 -24
- data/lib/surrogate/options.rb +0 -41
data/gemfiles/rspec_mocks_2.11
CHANGED
data/lib/surrogate.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'set'
|
2
|
+
require 'surrogate/surrogate_class_reflector'
|
3
|
+
require 'surrogate/porc_reflector'
|
2
4
|
|
3
5
|
class Surrogate
|
4
6
|
|
@@ -11,11 +13,11 @@ class Surrogate
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def surrogate_methods
|
14
|
-
@surrogate_methods ||=
|
16
|
+
@surrogate_methods ||= SurrogateClassReflector.new(surrogate).methods
|
15
17
|
end
|
16
18
|
|
17
19
|
def actual_methods
|
18
|
-
@actual_methods ||=
|
20
|
+
@actual_methods ||= PorcReflector.new(actual).methods
|
19
21
|
end
|
20
22
|
|
21
23
|
def compare
|
@@ -23,10 +25,12 @@ class Surrogate
|
|
23
25
|
instance: {
|
24
26
|
not_on_surrogate: instance_not_on_surrogate,
|
25
27
|
not_on_actual: instance_not_on_actual,
|
28
|
+
types: instance_types,
|
26
29
|
},
|
27
30
|
class: {
|
28
31
|
not_on_surrogate: class_not_on_surrogate,
|
29
32
|
not_on_actual: class_not_on_actual,
|
33
|
+
types: class_types,
|
30
34
|
},
|
31
35
|
}
|
32
36
|
end
|
@@ -49,78 +53,72 @@ class Surrogate
|
|
49
53
|
surrogate_methods[:class][:api] - actual_methods[:class][:inherited] - actual_methods[:class][:other]
|
50
54
|
end
|
51
55
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
other: class_other_methods,
|
62
|
-
},
|
63
|
-
}
|
64
|
-
end
|
65
|
-
|
66
|
-
def instance_inherited_methods
|
67
|
-
Set.new actual.instance_methods - actual.instance_methods(false)
|
56
|
+
# types are only shown for methods on both objects
|
57
|
+
def class_types
|
58
|
+
surrogate_class_methods = surrogate_methods[:class][:api] + surrogate_methods[:class][:inherited]
|
59
|
+
actual_class_methods = actual_methods[:class][:inherited] + actual_methods[:class][:other]
|
60
|
+
class_methods_that_should_match = (surrogate_class_methods & actual_class_methods) - surrogate_methods[:class][:without_bodies] - actual_methods[:class][:without_bodies]
|
61
|
+
class_methods_that_should_match.each_with_object Hash.new do |name, hash|
|
62
|
+
surrogate_type, actual_type = class_types_for name
|
63
|
+
next if surrogate_type == actual_type
|
64
|
+
hash[name] = { surrogate: surrogate_type, actual: actual_type }
|
68
65
|
end
|
66
|
+
end
|
69
67
|
|
70
|
-
|
71
|
-
|
68
|
+
# types are only shown for methods on both objects
|
69
|
+
def instance_types
|
70
|
+
surrogate_instance_methods = surrogate_methods[:instance][:api] + surrogate_methods[:instance][:inherited]
|
71
|
+
actual_instance_methods = actual_methods[:instance][:inherited] + actual_methods[:instance][:other]
|
72
|
+
instance_methods_that_should_match = (surrogate_instance_methods & actual_instance_methods) - surrogate_methods[:instance][:without_bodies] - actual_methods[:instance][:without_bodies]
|
73
|
+
instance_methods_that_should_match.each_with_object Hash.new do |name, hash|
|
74
|
+
surrogate_type, actual_type = instance_types_for name
|
75
|
+
next if surrogate_type == actual_type
|
76
|
+
hash[name] = { surrogate: surrogate_type, actual: actual_type }
|
72
77
|
end
|
78
|
+
end
|
73
79
|
|
74
|
-
|
75
|
-
Set.new actual.singleton_class.instance_methods - actual.singleton_class.instance_methods(false)
|
76
|
-
end
|
80
|
+
private
|
77
81
|
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
def class_types_for(name)
|
83
|
+
surrogate_method = class_api_method_for name
|
84
|
+
surrogate_method &&= to_lambda surrogate_method
|
85
|
+
surrogate_method ||= surrogate.method name
|
86
|
+
actual_method = actual.method name
|
87
|
+
return type_for(surrogate_method), type_for(actual_method)
|
81
88
|
end
|
82
89
|
|
90
|
+
def instance_types_for(name)
|
91
|
+
surrogate_method = instance_api_method_for name
|
92
|
+
surrogate_method &&= to_lambda surrogate_method
|
93
|
+
surrogate_method ||= surrogate.instance_method name
|
94
|
+
actual_method = actual.instance_method name
|
95
|
+
return type_for(surrogate_method), type_for(actual_method)
|
96
|
+
end
|
83
97
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
api: instance_api_methods,
|
88
|
-
inherited: instance_inherited_methods,
|
89
|
-
other: instance_other_methods,
|
90
|
-
},
|
91
|
-
class: {
|
92
|
-
api: class_api_methods,
|
93
|
-
inherited: class_inherited_methods,
|
94
|
-
other: class_other_methods,
|
95
|
-
},
|
96
|
-
}
|
97
|
-
end
|
98
|
-
|
99
|
-
def instance_api_methods
|
100
|
-
Set.new surrogate.api_method_names
|
101
|
-
end
|
98
|
+
def type_for(method)
|
99
|
+
method.parameters.map(&:first)
|
100
|
+
end
|
102
101
|
|
103
|
-
|
104
|
-
|
105
|
-
|
102
|
+
def to_lambda(proc)
|
103
|
+
obj = Object.new
|
104
|
+
obj.singleton_class.send :define_method, :abc123, &proc
|
105
|
+
obj.method :abc123
|
106
|
+
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
def instance_api_method_for(name)
|
109
|
+
class_hatchery.api_method_for name
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
113
|
-
|
112
|
+
def class_api_method_for(name)
|
113
|
+
singleton_class_hatchery.api_method_for name
|
114
|
+
end
|
114
115
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
end
|
116
|
+
def class_hatchery
|
117
|
+
@class_hatchery ||= surrogate.instance_variable_get :@hatchery
|
118
|
+
end
|
119
119
|
|
120
|
-
|
121
|
-
|
122
|
-
Set.new(surrogate.singleton_class.instance_methods false) - class_api_methods - class_inherited_methods
|
123
|
-
end
|
120
|
+
def singleton_class_hatchery
|
121
|
+
@singleton_class_hatchery ||= surrogate.singleton_class.instance_variable_get :@hatchery
|
124
122
|
end
|
125
123
|
end
|
126
124
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Surrogate
|
2
|
+
|
3
|
+
# Give it a name and lambda, it will raise an argument error if they don't match, without actually invoking the method.
|
4
|
+
# Its error message includes the signature of the message (maybe should also show what was passed in?)
|
5
|
+
class ArgumentErrorizer
|
6
|
+
attr_accessor :name, :empty_lambda
|
7
|
+
|
8
|
+
def initialize(name, lambda_or_method)
|
9
|
+
self.name, self.empty_lambda = name.to_s, lambda_with_same_params_as(lambda_or_method)
|
10
|
+
end
|
11
|
+
|
12
|
+
def match!(*args)
|
13
|
+
empty_lambda.call *args
|
14
|
+
rescue ArgumentError => e
|
15
|
+
raise ArgumentError, e.message + " in #{name}(#{lambda_signature empty_lambda})"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def lambda_with_same_params_as(lambda_or_method)
|
21
|
+
eval "->(" << lambda_signature(lambda_or_method) << ") {}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def lambda_signature(lambda_or_method)
|
25
|
+
lambda_or_method.parameters.map { |type, name| param_for type, name }.compact.join(', ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def param_for(type, name)
|
29
|
+
case type
|
30
|
+
when :req
|
31
|
+
name
|
32
|
+
when :opt
|
33
|
+
"#{name}='?'"
|
34
|
+
when :rest
|
35
|
+
"*#{name}"
|
36
|
+
when :block
|
37
|
+
"&#{name}"
|
38
|
+
else
|
39
|
+
raise "forgot to account for #{type.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/surrogate/endower.rb
CHANGED
@@ -34,8 +34,7 @@ class Surrogate
|
|
34
34
|
klass.extend ClassMethods
|
35
35
|
add_hatchery_to klass
|
36
36
|
enable_defining_methods klass
|
37
|
-
|
38
|
-
remember_invocations_for_instances_of klass
|
37
|
+
klass.send :include, InstanceMethods
|
39
38
|
invoke_hooks klass
|
40
39
|
end
|
41
40
|
|
@@ -44,7 +43,6 @@ class Surrogate
|
|
44
43
|
enable_defining_methods singleton
|
45
44
|
singleton.module_eval &block if block
|
46
45
|
klass.instance_variable_set :@hatchling, Hatchling.new(klass, hatchery)
|
47
|
-
remember_invocations_for_instances_of singleton
|
48
46
|
invoke_hooks singleton
|
49
47
|
klass
|
50
48
|
end
|
@@ -53,37 +51,10 @@ class Surrogate
|
|
53
51
|
self.class.hooks.each { |hook| hook.call klass }
|
54
52
|
end
|
55
53
|
|
56
|
-
# yeesh :( pretty sure there isn't a better way to do this
|
57
|
-
def record_initialization_for_instances_of(klass)
|
58
|
-
def klass.method_added(meth)
|
59
|
-
return if meth != :initialize || @hijacking_initialize
|
60
|
-
@hijacking_initialize = true
|
61
|
-
current_initialize = instance_method :initialize
|
62
|
-
|
63
|
-
# `define' records the args while maintaining the old behaviour
|
64
|
-
# we have to do it stupidly like this because there is no to_proc on an unbound method
|
65
|
-
define :initialize do |*args, &block|
|
66
|
-
current_initialize.bind(self).call(*args, &block)
|
67
|
-
end
|
68
|
-
ensure
|
69
|
-
@hijacking_initialize = false
|
70
|
-
end
|
71
|
-
initialize = klass.instance_method :initialize
|
72
|
-
klass.__send__ :define_method, :initialize do |*args, &block|
|
73
|
-
initialize.bind(self).call(*args, &block)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
54
|
def singleton
|
78
55
|
klass.singleton_class
|
79
56
|
end
|
80
57
|
|
81
|
-
def remember_invocations_for_instances_of(klass)
|
82
|
-
klass.__send__ :define_method, :invocations do |method_name|
|
83
|
-
@hatchling.invocations method_name
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
58
|
def add_hatchery_to(klass)
|
88
59
|
klass.instance_variable_set :@hatchery, Surrogate::Hatchery.new(klass)
|
89
60
|
end
|
@@ -92,10 +63,6 @@ class Surrogate
|
|
92
63
|
def klass.define(method_name, options={}, &block)
|
93
64
|
@hatchery.define method_name, options, &block
|
94
65
|
end
|
95
|
-
|
96
|
-
def klass.api_method_names
|
97
|
-
@hatchery.api_method_names
|
98
|
-
end
|
99
66
|
end
|
100
67
|
end
|
101
68
|
|
@@ -106,8 +73,9 @@ class Surrogate
|
|
106
73
|
# Should this be dup? (dup seems to copy singleton methods) and may be able to use #initialize_copy to reset ivars
|
107
74
|
# Can we just remove this feature an instead provide a reset feature which could be hooked into in before/after blocks (e.g. https://github.com/rspec/rspec-core/blob/622505d616d950ed53d12c6e82dbb953ba6241b4/lib/rspec/core/mocking/with_rspec.rb)
|
108
75
|
def clone
|
109
|
-
hatchling, hatchery = @hatchling, @hatchery
|
76
|
+
hatchling, hatchery, parent_name = @hatchling, @hatchery, name
|
110
77
|
Class.new self do
|
78
|
+
extend Module.new { define_method(:name) { parent_name && parent_name + '.clone' } } # inherit the name -- use module so that ApiComparison comes out correct (real classes inherit their name method)
|
111
79
|
Surrogate.endow self do
|
112
80
|
hatchling.api_methods.each { |name, options| define name, options.to_hash, &options.default_proc }
|
113
81
|
end
|
@@ -116,12 +84,11 @@ class Surrogate
|
|
116
84
|
end
|
117
85
|
|
118
86
|
# Custom new, because user can define initialize, and we need to record it
|
119
|
-
# Can we move this into the redefinition of initialize and have it explicitly record itself?
|
120
87
|
def new(*args)
|
121
88
|
instance = allocate
|
122
89
|
self.last_instance = instance
|
123
90
|
instance.instance_variable_set :@hatchling, Hatchling.new(instance, @hatchery)
|
124
|
-
instance.
|
91
|
+
instance.__send__ :initialize, *args
|
125
92
|
instance
|
126
93
|
end
|
127
94
|
|
@@ -132,5 +99,42 @@ class Surrogate
|
|
132
99
|
def last_instance=(instance)
|
133
100
|
Thread.current["surrogate_last_instance_#{self.object_id}"] = instance
|
134
101
|
end
|
102
|
+
|
103
|
+
|
104
|
+
def inspect
|
105
|
+
return name if name
|
106
|
+
methods = SurrogateClassReflector.new(self).methods
|
107
|
+
method_inspections = []
|
108
|
+
|
109
|
+
# add class methods
|
110
|
+
if methods[:class][:api].any?
|
111
|
+
meth_names = methods[:class][:api].to_a.sort.take(4)
|
112
|
+
meth_names[-1] = '...' if meth_names.size == 4
|
113
|
+
method_inspections << "Class: #{meth_names.join ' '}"
|
114
|
+
end
|
115
|
+
|
116
|
+
# add instance methods
|
117
|
+
if methods[:instance][:api].any?
|
118
|
+
meth_names = methods[:instance][:api].to_a.sort.take(4)
|
119
|
+
meth_names[-1] = '...' if meth_names.size == 4
|
120
|
+
method_inspections << "Instance: #{meth_names.join ' '}"
|
121
|
+
end
|
122
|
+
|
123
|
+
# when no class or instance methods
|
124
|
+
method_inspections << "no api" if method_inspections.empty?
|
125
|
+
|
126
|
+
"AnonymousSurrogate(#{method_inspections.join ', '})"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Use module so the method is inherited. This allows proper matching (e.g. other object will inherit inspect from Object)
|
131
|
+
module InstanceMethods
|
132
|
+
def inspect
|
133
|
+
methods = SurrogateClassReflector.new(self.class).methods[:instance][:api].sort.take(4)
|
134
|
+
methods[-1] = '...' if methods.size == 4
|
135
|
+
methods << 'no api' if methods.empty?
|
136
|
+
class_name = self.class.name || 'AnonymousSurrogate'
|
137
|
+
"#<#{class_name}: #{methods.join ' '}>"
|
138
|
+
end
|
135
139
|
end
|
136
140
|
end
|
data/lib/surrogate/hatchery.rb
CHANGED
@@ -15,7 +15,7 @@ class Surrogate
|
|
15
15
|
add_api_method_for method_name
|
16
16
|
add_verb_helpers_for method_name
|
17
17
|
add_noun_helpers_for method_name
|
18
|
-
api_methods[method_name] =
|
18
|
+
api_methods[method_name] = MethodDefinition.new method_name, options, block
|
19
19
|
klass
|
20
20
|
end
|
21
21
|
|
@@ -27,6 +27,11 @@ class Surrogate
|
|
27
27
|
api_methods.keys - [:initialize]
|
28
28
|
end
|
29
29
|
|
30
|
+
def api_method_for(name)
|
31
|
+
options = api_methods[name]
|
32
|
+
options && options.default_proc
|
33
|
+
end
|
34
|
+
|
30
35
|
private
|
31
36
|
|
32
37
|
def klass_can_define_api_methods
|
data/lib/surrogate/hatchling.rb
CHANGED
@@ -20,6 +20,7 @@ class Surrogate
|
|
20
20
|
invocation = Invocation.new(args, &block)
|
21
21
|
invoked_methods[method_name] << invocation
|
22
22
|
return get_default method_name, invocation, &block unless has_ivar? method_name
|
23
|
+
interfaces_must_match! method_name, args
|
23
24
|
Value.factory(get_ivar method_name).value(method_name)
|
24
25
|
end
|
25
26
|
|
@@ -40,6 +41,10 @@ class Surrogate
|
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
44
|
+
def interfaces_must_match!(method_name, args)
|
45
|
+
api_methods[method_name].must_match! args
|
46
|
+
end
|
47
|
+
|
43
48
|
def get_default(method_name, invocation)
|
44
49
|
api_methods[method_name].default instance, invocation do
|
45
50
|
raise UnpreparedMethodError, "#{method_name} has been invoked without being told how to behave"
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'surrogate/argument_errorizer'
|
2
|
+
|
3
|
+
class Surrogate
|
4
|
+
|
5
|
+
# A surrogate's `define` keyword results in one of these
|
6
|
+
class MethodDefinition
|
7
|
+
attr_accessor :name, :options, :default_proc
|
8
|
+
|
9
|
+
def initialize(name, options, default_proc)
|
10
|
+
self.name, self.options, self.default_proc = name, options, default_proc
|
11
|
+
end
|
12
|
+
|
13
|
+
def has?(name)
|
14
|
+
options.has_key? name
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
options[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
options
|
23
|
+
end
|
24
|
+
|
25
|
+
def must_match!(args)
|
26
|
+
default_proc && errorizer.match!(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def default(instance, invocation, &no_default)
|
30
|
+
if options.has_key? :default
|
31
|
+
options[:default]
|
32
|
+
elsif default_proc
|
33
|
+
default_proc_as_method_on(instance).call(*invocation.args, &invocation.block)
|
34
|
+
else
|
35
|
+
no_default.call
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def errorizer
|
42
|
+
@errorizer ||= ArgumentErrorizer.new name, default_proc
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_proc_as_method_on(instance)
|
46
|
+
unique_name = "surrogate_temp_method_#{Time.now.to_i}_#{rand 10000000}"
|
47
|
+
klass = instance.singleton_class
|
48
|
+
klass.__send__ :define_method, unique_name, &default_proc
|
49
|
+
as_method = klass.instance_method unique_name
|
50
|
+
klass.__send__ :remove_method, unique_name
|
51
|
+
as_method.bind instance
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|