surrogate 0.1.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/.gitignore +4 -0
- data/.rvmrc +7 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/Readme.md +54 -0
- data/lib/surrogate/api_comparer.rb +126 -0
- data/lib/surrogate/endower.rb +108 -0
- data/lib/surrogate/hatchery.rb +48 -0
- data/lib/surrogate/hatchling.rb +78 -0
- data/lib/surrogate/method_queue.rb +14 -0
- data/lib/surrogate/options.rb +28 -0
- data/lib/surrogate/rspec/api_method_matchers.rb +246 -0
- data/lib/surrogate/rspec/substitutability_matchers.rb +24 -0
- data/lib/surrogate/rspec.rb +2 -0
- data/lib/surrogate/version.rb +3 -0
- data/lib/surrogate.rb +17 -0
- data/spec/acceptance_spec.rb +131 -0
- data/spec/defining_api_methods_spec.rb +295 -0
- data/spec/rspec/have_been_asked_for_its_spec.rb +143 -0
- data/spec/rspec/have_been_initialized_with_spec.rb +29 -0
- data/spec/rspec/have_been_told_to_spec.rb +133 -0
- data/spec/rspec/messages_spec.rb +33 -0
- data/spec/rspec/substitute_for_spec.rb +73 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/api_comparer_spec.rb +138 -0
- data/surrogate.gemspec +24 -0
- metadata +92 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/Readme.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
TODO
|
2
|
+
----
|
3
|
+
|
4
|
+
* substitutability
|
5
|
+
* add methods for substitutability
|
6
|
+
|
7
|
+
|
8
|
+
Features for future vuersions
|
9
|
+
-----------------------------
|
10
|
+
|
11
|
+
* change queue notation from will_x_qeue(1,2,3) to will_x(1,2,3)
|
12
|
+
* arity option
|
13
|
+
* support for raising errors
|
14
|
+
* need some way to talk about and record blocks being passed
|
15
|
+
* support all rspec matchers (RSpec::Mocks::ArgumentMatchers)
|
16
|
+
* assertions for order of invocations & methods
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Future subset substitutability
|
21
|
+
|
22
|
+
# ===== Substitutability =====
|
23
|
+
|
24
|
+
# real user is not a suitable substitute if missing methods that mock user has
|
25
|
+
user_class.should_not substitute_for Class.new
|
26
|
+
|
27
|
+
# real user must have all of mock user's methods to be substitutable
|
28
|
+
substitutable_real_user_class = Class.new do
|
29
|
+
def self.find() end
|
30
|
+
def initialize(id) end
|
31
|
+
def id() end
|
32
|
+
def name() end
|
33
|
+
def address() end
|
34
|
+
def phone_numbers() end
|
35
|
+
def add_phone_number(area_code, number) end
|
36
|
+
end
|
37
|
+
user_class.should substitute_for substitutable_real_user_class
|
38
|
+
user_class.should be_subset_of substitutable_real_user_class
|
39
|
+
|
40
|
+
# real user class is not a suitable substitutable if has extra methods, but is suitable subset
|
41
|
+
real_user_class = substitutable_real_user_class.clone
|
42
|
+
def real_user_class.some_class_meth() end
|
43
|
+
user_class.should_not substitute_for real_user_class
|
44
|
+
user_class.should be_subset_of real_user_class
|
45
|
+
|
46
|
+
real_user_class = substitutable_real_user_class.clone
|
47
|
+
real_user_class.send(:define_method, :some_instance_method) {}
|
48
|
+
user_class.should_not substitute_for real_user_class
|
49
|
+
user_class.should be_subset_of real_user_class
|
50
|
+
|
51
|
+
# subset substitutability does not work for superset
|
52
|
+
real_user_class = substitutable_real_user_class.clone
|
53
|
+
real_user_class.undef_method :address
|
54
|
+
user_class.should_not be_subset_of real_user_class
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class Surrogate
|
4
|
+
|
5
|
+
# compares a surrogate to an object
|
6
|
+
class ApiComparer
|
7
|
+
attr_accessor :surrogate, :actual
|
8
|
+
|
9
|
+
def initialize(surrogate, actual)
|
10
|
+
self.surrogate, self.actual = surrogate, actual
|
11
|
+
end
|
12
|
+
|
13
|
+
def surrogate_methods
|
14
|
+
@surrogate_methods ||= SurrogateMethods.new(surrogate).methods
|
15
|
+
end
|
16
|
+
|
17
|
+
def actual_methods
|
18
|
+
@actual_methods ||= ActualMethods.new(actual).methods
|
19
|
+
end
|
20
|
+
|
21
|
+
def compare
|
22
|
+
@compare ||= {
|
23
|
+
instance: {
|
24
|
+
not_on_surrogate: instance_not_on_surrogate,
|
25
|
+
not_on_actual: instance_not_on_actual,
|
26
|
+
},
|
27
|
+
class: {
|
28
|
+
not_on_surrogate: class_not_on_surrogate,
|
29
|
+
not_on_actual: class_not_on_actual,
|
30
|
+
},
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def instance_not_on_surrogate
|
35
|
+
(actual_methods[:instance][:inherited] + actual_methods[:instance][:other]) -
|
36
|
+
(surrogate_methods[:instance][:inherited] + surrogate_methods[:instance][:api])
|
37
|
+
end
|
38
|
+
|
39
|
+
def instance_not_on_actual
|
40
|
+
surrogate_methods[:instance][:api] - actual_methods[:instance][:inherited] - actual_methods[:instance][:other]
|
41
|
+
end
|
42
|
+
|
43
|
+
def class_not_on_surrogate
|
44
|
+
(actual_methods[:class][:inherited] + actual_methods[:class][:other]) -
|
45
|
+
(surrogate_methods[:class][:inherited] + surrogate_methods[:class][:api])
|
46
|
+
end
|
47
|
+
|
48
|
+
def class_not_on_actual
|
49
|
+
surrogate_methods[:class][:api] - actual_methods[:class][:inherited] - actual_methods[:class][:other]
|
50
|
+
end
|
51
|
+
|
52
|
+
# methods from the actual class (as opposed to "these are actually methods"
|
53
|
+
class ActualMethods < Struct.new(:actual)
|
54
|
+
def methods
|
55
|
+
{ instance: {
|
56
|
+
inherited: instance_inherited_methods,
|
57
|
+
other: instance_other_methods,
|
58
|
+
},
|
59
|
+
class: {
|
60
|
+
inherited: class_inherited_methods,
|
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)
|
68
|
+
end
|
69
|
+
|
70
|
+
def instance_other_methods
|
71
|
+
Set.new(actual.instance_methods) - instance_inherited_methods
|
72
|
+
end
|
73
|
+
|
74
|
+
def class_inherited_methods
|
75
|
+
Set.new actual.singleton_class.instance_methods - actual.singleton_class.instance_methods(false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def class_other_methods
|
79
|
+
Set.new(actual.singleton_class.instance_methods) - class_inherited_methods
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
class SurrogateMethods < Struct.new(:surrogate)
|
85
|
+
def methods
|
86
|
+
{ instance: {
|
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
|
102
|
+
|
103
|
+
def instance_inherited_methods
|
104
|
+
Set.new surrogate.instance_methods - surrogate.instance_methods(false)
|
105
|
+
end
|
106
|
+
|
107
|
+
def instance_other_methods
|
108
|
+
Set.new(surrogate.instance_methods false) - instance_api_methods
|
109
|
+
end
|
110
|
+
|
111
|
+
def class_api_methods
|
112
|
+
Set.new surrogate.singleton_class.api_method_names
|
113
|
+
end
|
114
|
+
|
115
|
+
# should have new
|
116
|
+
def class_inherited_methods
|
117
|
+
Set.new surrogate.singleton_class.instance_methods - surrogate.singleton_class.instance_methods(false) + [:new]
|
118
|
+
end
|
119
|
+
|
120
|
+
# should not have new
|
121
|
+
def class_other_methods
|
122
|
+
Set.new(surrogate.singleton_class.instance_methods false) - class_api_methods - class_inherited_methods
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
class Surrogate
|
2
|
+
|
3
|
+
# adds surrogate behaviour to your class / singleton class / instances
|
4
|
+
class Endower
|
5
|
+
def self.endow(klass, &playlist)
|
6
|
+
new(klass, &playlist).endow
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :klass, :playlist
|
10
|
+
|
11
|
+
def initialize(klass, &playlist)
|
12
|
+
self.klass, self.playlist = klass, playlist
|
13
|
+
end
|
14
|
+
|
15
|
+
def endow
|
16
|
+
endow_klass
|
17
|
+
endow_singleton_class
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def endow_klass
|
23
|
+
a_hatchery_for klass
|
24
|
+
enable_defining_methods klass
|
25
|
+
record_initialization_for_instances_of klass
|
26
|
+
remember_invocations_for_instances_of klass
|
27
|
+
remember_invocations_for_instances_of klass.singleton_class
|
28
|
+
hijack_instantiation_of klass
|
29
|
+
can_get_a_new klass
|
30
|
+
end
|
31
|
+
|
32
|
+
def endow_singleton_class
|
33
|
+
hatchery = a_hatchery_for singleton
|
34
|
+
enable_defining_methods singleton
|
35
|
+
singleton.module_eval &playlist if playlist
|
36
|
+
klass.instance_variable_set :@surrogate, Hatchling.new(klass, hatchery)
|
37
|
+
klass
|
38
|
+
end
|
39
|
+
|
40
|
+
# yeesh :( try to find a better way to do this
|
41
|
+
def record_initialization_for_instances_of(klass)
|
42
|
+
def klass.method_added(meth)
|
43
|
+
return unless meth == :initialize && !@hijacking_initialize
|
44
|
+
@hijacking_initialize = true
|
45
|
+
current_initialize = instance_method :initialize
|
46
|
+
define :initialize do |*args, &block|
|
47
|
+
current_initialize.bind(self).call(*args, &block)
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
@hijacking_initialize = false
|
51
|
+
end
|
52
|
+
initialize = klass.instance_method :initialize
|
53
|
+
klass.send :define_method, :initialize do |*args, &block|
|
54
|
+
initialize.bind(self).call(*args, &block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def singleton
|
59
|
+
klass.singleton_class
|
60
|
+
end
|
61
|
+
|
62
|
+
def can_get_a_new(klass)
|
63
|
+
klass.define_singleton_method :reprise do
|
64
|
+
new_klass = Class.new self
|
65
|
+
surrogate = @surrogate
|
66
|
+
Surrogate.endow new_klass do
|
67
|
+
surrogate.api_methods.each do |method_name, options|
|
68
|
+
define method_name, options.to_hash, &options.default_proc
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@hatchery.api_methods.each do |method_name, options|
|
72
|
+
new_klass.define method_name, options.to_hash, &options.default_proc
|
73
|
+
end
|
74
|
+
new_klass
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def remember_invocations_for_instances_of(klass)
|
79
|
+
klass.send :define_method, :invocations do |method_name|
|
80
|
+
@surrogate.invocations method_name
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def a_hatchery_for(klass)
|
85
|
+
klass.instance_variable_set :@hatchery, Surrogate::Hatchery.new(klass)
|
86
|
+
end
|
87
|
+
|
88
|
+
def hijack_instantiation_of(klass)
|
89
|
+
def klass.new(*args)
|
90
|
+
instance = allocate
|
91
|
+
hatchery = @hatchery
|
92
|
+
instance.instance_eval { @surrogate = Hatchling.new instance, hatchery }
|
93
|
+
instance.send :initialize, *args
|
94
|
+
instance
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def enable_defining_methods(klass)
|
99
|
+
def klass.define(method_name, options={}, &block)
|
100
|
+
@hatchery.define method_name, options, block
|
101
|
+
end
|
102
|
+
|
103
|
+
def klass.api_method_names
|
104
|
+
@hatchery.api_method_names
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Surrogate
|
2
|
+
class Hatchery
|
3
|
+
attr_accessor :klass
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
self.klass = klass
|
7
|
+
defines_methods
|
8
|
+
end
|
9
|
+
|
10
|
+
def defines_methods
|
11
|
+
klass.singleton_class.send :define_method, :define, &method(:define)
|
12
|
+
end
|
13
|
+
|
14
|
+
def define(method_name, options={}, block)
|
15
|
+
add_api_methods_for method_name
|
16
|
+
api_methods[method_name] = Options.new options, block
|
17
|
+
end
|
18
|
+
|
19
|
+
def api_methods
|
20
|
+
@api_methods ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def api_method_names
|
24
|
+
api_methods.keys - [:initialize]
|
25
|
+
end
|
26
|
+
|
27
|
+
# here we need to find better domain terminology
|
28
|
+
def add_api_methods_for(method_name)
|
29
|
+
klass.send :define_method, method_name do |*args, &block|
|
30
|
+
@surrogate.invoke_method method_name, args, &block
|
31
|
+
end
|
32
|
+
|
33
|
+
# verbs
|
34
|
+
klass.send :define_method, "will_#{method_name}" do |*args, &block|
|
35
|
+
if args.size == 1
|
36
|
+
@surrogate.prepare_method method_name, args, &block
|
37
|
+
else
|
38
|
+
@surrogate.prepare_method_queue method_name, args, &block
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# nouns
|
44
|
+
klass.send :alias_method, "will_have_#{method_name}", "will_#{method_name}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Surrogate
|
2
|
+
UnknownMethod = Class.new StandardError
|
3
|
+
class Hatchling
|
4
|
+
attr_accessor :instance, :hatchery
|
5
|
+
|
6
|
+
def initialize(instance, hatchery)
|
7
|
+
self.instance, self.hatchery = instance, hatchery
|
8
|
+
end
|
9
|
+
|
10
|
+
def api_methods
|
11
|
+
hatchery.api_methods
|
12
|
+
end
|
13
|
+
|
14
|
+
def invoke_method(method_name, args, &block)
|
15
|
+
invoked_methods[method_name] << args
|
16
|
+
return get_default method_name, args unless has_ivar? method_name
|
17
|
+
ivar = get_ivar method_name
|
18
|
+
return ivar unless ivar.kind_of? MethodQueue
|
19
|
+
play_from_queue ivar, method_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare_method(method_name, args, &block)
|
23
|
+
set_ivar method_name, *args
|
24
|
+
end
|
25
|
+
|
26
|
+
def prepare_method_queue(method_name, args, &block)
|
27
|
+
set_ivar method_name, MethodQueue.new(args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def invocations(method_name)
|
31
|
+
invoked_methods[method_name]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def invoked_methods
|
37
|
+
@invoked_methods ||= Hash.new do |hash, method_name|
|
38
|
+
must_know method_name
|
39
|
+
hash[method_name] = []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_default(method_name, args)
|
44
|
+
api_methods[method_name].default instance, args do
|
45
|
+
raise UnpreparedMethodError, "#{method_name} has been invoked without being told how to behave"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def play_from_queue(queue, method_name)
|
50
|
+
result = queue.dequeue
|
51
|
+
unset_ivar method_name if queue.empty?
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
def must_know(method_name)
|
56
|
+
return if api_methods.has_key? method_name
|
57
|
+
known_methods = api_methods.keys.map(&:to_s).map(&:inspect).join ', '
|
58
|
+
raise UnknownMethod, "doesn't know \"#{method_name}\", only knows #{known_methods}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_ivar?(method_name)
|
62
|
+
instance.instance_variable_defined? "@#{method_name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_ivar(method_name, value)
|
66
|
+
instance.instance_variable_set "@#{method_name}", value
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_ivar(method_name)
|
70
|
+
instance.instance_variable_get "@#{method_name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def unset_ivar(method_name)
|
74
|
+
instance.send :remove_instance_variable, "@#{method_name}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Surrogate
|
2
|
+
class Options
|
3
|
+
attr_accessor :options, :default_proc
|
4
|
+
|
5
|
+
def initialize(options, default_proc)
|
6
|
+
self.options, self.default_proc = options, default_proc
|
7
|
+
end
|
8
|
+
|
9
|
+
def has?(name)
|
10
|
+
options.has_key? name
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
options[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
options
|
19
|
+
end
|
20
|
+
|
21
|
+
def default(instance, args, &no_default)
|
22
|
+
return options[:default] if options.has_key? :default
|
23
|
+
return instance.instance_exec(*args, &default_proc) if default_proc
|
24
|
+
no_default.call
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|