surrogate 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require 'set'
2
+ class Surrogate
3
+ # reflects on the Plain Old Ruby Class to give info about methods that are useful for the comparer
4
+ class PorcReflector < Struct.new(:actual)
5
+ def methods
6
+ { instance: {
7
+ inherited: instance_inherited_methods,
8
+ other: instance_other_methods,
9
+ without_bodies: instance_without_bodies,
10
+ },
11
+ class: {
12
+ inherited: class_inherited_methods,
13
+ other: class_other_methods,
14
+ without_bodies: class_without_bodies,
15
+ },
16
+ }
17
+ end
18
+
19
+ def instance_inherited_methods
20
+ Set.new actual.instance_methods - actual.instance_methods(false)
21
+ end
22
+
23
+ def instance_other_methods
24
+ Set.new(actual.instance_methods) - instance_inherited_methods
25
+ end
26
+
27
+ def class_inherited_methods
28
+ Set.new actual.singleton_class.instance_methods - actual.singleton_class.instance_methods(false)
29
+ end
30
+
31
+ def class_other_methods
32
+ Set.new(actual.singleton_class.instance_methods) - class_inherited_methods
33
+ end
34
+
35
+ def class_without_bodies
36
+ Set.new actual.methods.select { |name| actual.method(name).parameters.any? { |param| param.size == 1 } }
37
+ end
38
+
39
+ def instance_without_bodies
40
+ Set.new actual.instance_methods.select { |name| actual.instance_method(name).parameters.any? { |param| param.size == 1 } }
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,7 @@
1
1
  require 'surrogate/rspec/abstract_failure_message'
2
2
  require 'surrogate/rspec/times_predicate'
3
3
  require 'surrogate/rspec/with_filter'
4
+ require 'surrogate/surrogate_instance_reflector'
4
5
 
5
6
  class Surrogate
6
7
  module RSpec
@@ -23,7 +24,7 @@ class Surrogate
23
24
  end
24
25
 
25
26
  def invocations
26
- surrogate.invocations(method_name)
27
+ SurrogateInstanceReflector.new(surrogate).invocations(method_name)
27
28
  end
28
29
 
29
30
  def failure_message_for_should
@@ -1,16 +1,24 @@
1
1
  class Surrogate
2
2
  ::RSpec::Matchers.define :substitute_for do |original_class, options={}|
3
3
 
4
- comparison = nil
4
+ comparison = nil
5
5
  subset_only = options[:subset]
6
+ types = options.fetch :types, true
7
+
8
+ def comparing_fields(comparison, subset_only, types)
9
+ fields = {}
10
+ fields[:instance_not_on_actual ] = comparison[:instance][:not_on_actual]
11
+ fields[:class_not_on_actual ] = comparison[:class ][:not_on_actual]
12
+ fields[:instance_not_on_surrogate] = comparison[:instance][:not_on_surrogate] unless subset_only
13
+ fields[:class_not_on_surrogate ] = comparison[:class ][:not_on_surrogate] unless subset_only
14
+ fields[:instance_types ] = comparison[:instance][:types] if types
15
+ fields[:class_types ] = comparison[:class ][:types] if types
16
+ fields
17
+ end
6
18
 
7
19
  match do |mocked_class|
8
20
  comparison = ApiComparer.new(mocked_class, original_class).compare
9
- if subset_only
10
- (comparison[:instance][:not_on_actual] + comparison[:class][:not_on_actual]).empty?
11
- else
12
- (comparison[:instance].values + comparison[:class].values).inject(:+).empty?
13
- end
21
+ comparing_fields(comparison, subset_only, types).values.inject(:+).empty?
14
22
  end
15
23
 
16
24
  failure_message_for_should do
@@ -18,13 +26,21 @@ class Surrogate
18
26
  extra_class_methods = comparison[:class ][:not_on_actual ].to_a
19
27
  missing_instance_methods = comparison[:instance][:not_on_surrogate].to_a
20
28
  missing_class_methods = comparison[:class ][:not_on_surrogate].to_a
29
+ instance_type_mismatch = comparison[:instance][:types ]
30
+ class_type_mismatch = comparison[:class ][:types ]
31
+
21
32
 
22
33
  differences = []
23
34
  differences << "has extra instance methods: #{extra_instance_methods.inspect}" if extra_instance_methods.any?
24
35
  differences << "has extra class methods: #{extra_class_methods.inspect}" if extra_class_methods.any?
25
36
  differences << "is missing instance methods: #{missing_instance_methods}" if !subset_only && missing_instance_methods.any?
26
37
  differences << "is missing class methods: #{missing_class_methods}" if !subset_only && missing_class_methods.any?
27
- "Was not substitutable because surrogate " << differences.join(', ')
38
+
39
+ if types # this conditional is not tested, nor are these error messages
40
+ instance_type_mismatch.each { |name, types| differences << "##{name} had types #{types.inspect}" }
41
+ class_type_mismatch.each { |name, types| differences << ".#{name} had types #{types.inspect}" }
42
+ end
43
+ "Was not substitutable because surrogate " << differences.join("\n")
28
44
  end
29
45
 
30
46
  failure_message_for_should_not do
@@ -94,7 +94,6 @@ class Surrogate
94
94
 
95
95
 
96
96
 
97
- # rename args to invocation
98
97
  attr_accessor :expected_invocation, :block, :pass, :filter_name
99
98
 
100
99
  def initialize(args=[], filter_name=:default_filter, &block)
@@ -0,0 +1,65 @@
1
+ require 'set'
2
+ class Surrogate
3
+
4
+ # Reflects on the surrogate class to give info about methods that are useful for the comparer
5
+ #
6
+ # It might make sense to not treat instance and class differently, but instead let whoever wants to use this
7
+ # instantiate it with both the class and the singleton class.
8
+ class SurrogateClassReflector < Struct.new(:surrogate_class)
9
+ def methods
10
+ { instance: {
11
+ api: instance_api_methods,
12
+ inherited: instance_inherited_methods,
13
+ other: instance_other_methods,
14
+ without_bodies: instance_without_bodies,
15
+ },
16
+ class: {
17
+ api: class_api_methods,
18
+ inherited: class_inherited_methods,
19
+ other: class_other_methods,
20
+ without_bodies: class_without_bodies,
21
+ },
22
+ }
23
+ end
24
+
25
+ def instance_api_methods
26
+ Set.new class_hatchery.api_method_names
27
+ end
28
+
29
+ def instance_inherited_methods
30
+ Set.new surrogate_class.instance_methods - surrogate_class.instance_methods(false)
31
+ end
32
+
33
+ def instance_other_methods
34
+ Set.new(surrogate_class.instance_methods false) - instance_api_methods
35
+ end
36
+
37
+ def instance_without_bodies
38
+ Set.new class_hatchery.api_method_names.reject { |name| class_hatchery.api_method_for name }
39
+ end
40
+
41
+ def class_api_methods
42
+ Set.new singleton_class_hatchery.api_method_names
43
+ end
44
+
45
+ def class_inherited_methods
46
+ Set.new surrogate_class.singleton_class.instance_methods - surrogate_class.singleton_class.instance_methods(false)
47
+ end
48
+
49
+ def class_other_methods
50
+ Set.new(surrogate_class.singleton_class.instance_methods false) - class_api_methods - class_inherited_methods
51
+ end
52
+
53
+ def class_without_bodies
54
+ Set.new singleton_class_hatchery.api_method_names.reject { |name| singleton_class_hatchery.api_method_for name }
55
+ end
56
+
57
+ def class_hatchery
58
+ @class_hatchery ||= surrogate_class.instance_variable_get :@hatchery
59
+ end
60
+
61
+ def singleton_class_hatchery
62
+ @singleton_class_hatchery ||= surrogate_class.singleton_class.instance_variable_get :@hatchery
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ class Surrogate
2
+
3
+ # Utilities for reflecting on surrogate instances
4
+ #
5
+ # Primarily it exists to avoid having to pollute the surrogate with reflection methods
6
+ class SurrogateInstanceReflector < Struct.new(:surrogate)
7
+ def invocations(method_name)
8
+ hatchling.invocations(method_name)
9
+ end
10
+
11
+ def hatchling
12
+ @hatchling ||= surrogate.instance_variable_get :@hatchling
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  class Surrogate
2
- VERSION = "0.5.5"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -116,7 +116,7 @@ describe Surrogate do
116
116
 
117
117
  # real user must have all of mock user's methods to be substitutable
118
118
  substitutable_real_user_class = Class.new do
119
- def self.find() end
119
+ def self.find(id) end
120
120
  def initialize(id) end
121
121
  def id() end
122
122
  def name() end
@@ -1,13 +1,25 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'define' do
4
+ def class_method_names(surrogate)
5
+ Surrogate::SurrogateClassReflector.new(surrogate).class_api_methods
6
+ end
7
+
8
+ def instance_method_names(surrogate)
9
+ Surrogate::SurrogateClassReflector.new(surrogate).instance_api_methods
10
+ end
11
+
12
+ def invocations(surrogate, method_name)
13
+ Surrogate::SurrogateInstanceReflector.new(surrogate).invocations(method_name)
14
+ end
15
+
4
16
  let(:mocked_class) { Surrogate.endow Class.new }
5
17
  let(:instance) { mocked_class.new }
6
18
 
7
19
  describe 'in the block' do
8
20
  it 'is an api method for the class' do
9
21
  pristine_klass = Class.new { Surrogate.endow(self) { define :find } }
10
- pristine_klass.singleton_class.api_method_names.should == [:find]
22
+ class_method_names(pristine_klass).should == Set[:find]
11
23
  end
12
24
  end
13
25
 
@@ -15,18 +27,11 @@ describe 'define' do
15
27
  describe 'out of the block' do
16
28
  it 'is an api method for the instance' do
17
29
  mocked_class.define :book
18
- mocked_class.api_method_names.should == [:book]
30
+ instance_method_names(mocked_class).should == Set[:book]
19
31
  end
20
32
  end
21
33
 
22
34
  describe 'definition default block invocation' do
23
- xit "something about raising an error if arity is wrong" do
24
- mocked_class.define(:a) { |arg| 1 }
25
- mocked_class.new.a.should == 1
26
- mocked_class.new.a(2).should == 1
27
- mocked_class.new.a(3).should == 1
28
- end
29
-
30
35
  it "is passed the arguments" do
31
36
  arg = nil
32
37
  mocked_class.define(:meth) { |inner_arg| arg = inner_arg }.new.meth(1212)
@@ -45,7 +50,6 @@ describe 'define' do
45
50
  end
46
51
 
47
52
  describe 'declaring the behaviour' do
48
-
49
53
  describe 'for verbs' do
50
54
  before { mocked_class.define :wink }
51
55
 
@@ -144,11 +148,11 @@ describe 'define' do
144
148
 
145
149
 
146
150
  context 'the api method' do
147
- it 'takes any number of arguments' do
148
- mocked_class.define(:meth) { 1 }
149
- mocked_class.new.meth.should == 1
151
+ it 'has the same arity as the method' do
152
+ mocked_class.define(:meth) { |a| a }
150
153
  mocked_class.new.meth(1).should == 1
151
- mocked_class.new.meth(1, 2).should == 1
154
+ expect { mocked_class.new.meth }.to raise_error ArgumentError, /0 for 1/
155
+ expect { mocked_class.new.meth 1, 2 }.to raise_error ArgumentError, /2 for 1/
152
156
  end
153
157
 
154
158
  it 'raises an UnpreparedMethodError when it has no default block' do
@@ -177,58 +181,26 @@ describe 'define' do
177
181
  mocked.meth.should == 123
178
182
  end
179
183
 
180
- describe 'initialization' do
181
- specify 'api methods can be an initialize method' do
182
- mocked_class.define(:initialize) { @abc = 123 }
183
- mocked_class.new.instance_variable_get(:@abc).should == 123
184
- end
185
-
186
- specify 'initialize exsits even if error is raised' do
187
- mocked_class.define(:initialize) { raise "simulate runtime error" }
188
- expect { mocked_class.new }.to raise_error(RuntimeError, /simulate/)
189
- expect { mocked_class.new }.to raise_error(RuntimeError, /simulate/)
190
- end
191
-
192
- specify 'receives args' do
193
- mocked_class.define(:initialize) { |num1, num2| @num = num1 + num2 }
194
- mocked_class.new(25, 75).instance_variable_get(:@num).should == 100
195
- end
196
-
197
- specify 'even works with inheritance' do
198
- superclass = Class.new
199
- superclass.send(:define_method, :initialize) { @a = 1 }
200
- subclass = Surrogate.endow Class.new superclass
201
- subclass.define :abc
202
- subclass.new.instance_variable_get(:@a).should == 1
203
- end
184
+ it 'raises arity errors, even if the value is overridden' do
185
+ mocked_class.define(:meth) { }
186
+ mocked = mocked_class.new
187
+ mocked.instance_variable_set :@meth, "abc"
188
+ expect { mocked.meth "extra", "args" }.to raise_error ArgumentError, /wrong number of arguments \(2 for 0\)/
189
+ end
204
190
 
205
- context 'when not an api method' do
206
- it 'respects arity (this is probably 1.9.3 only)' do
207
- expect { mocked_class.new 1 }.to raise_error ArgumentError, 'wrong number of arguments(1 for 0)'
208
- end
191
+ it 'does not raise arity errors, when there is no default block and the value is overridden' do
192
+ mocked_class.define :meth
193
+ mocked = mocked_class.new
194
+ mocked.instance_variable_set :@meth, "abc"
195
+ mocked.meth 1, 2, 3
196
+ end
209
197
 
210
- describe 'invocations are recorded anyway' do
211
- specify 'even when initialize is defined after surrogate block' do
212
- klass = Class.new do
213
- Surrogate.endow self
214
- def initialize(a) @a = a end
215
- end
216
- klass.new(1).should have_been_initialized_with 1
217
- klass.new(1).instance_variable_get(:@a).should == 1
218
- end
219
-
220
- specify 'even when initialize is defined before surrogate block' do
221
- klass = Class.new do
222
- def initialize(a) @a = a end
223
- Surrogate.endow self
224
- end
225
- klass.new(1).should have_been_initialized_with 1
226
- klass.new(1).instance_variable_get(:@a).should == 1
227
- end
228
- end
229
- end
198
+ it 'can make #initialize an api method' do
199
+ mocked_class.define(:initialize) { @abc = 123 }
200
+ mocked_class.new.instance_variable_get(:@abc).should == 123
230
201
  end
231
202
 
203
+
232
204
  describe 'it takes a block whos return value will be used as the default' do
233
205
  specify 'the block is instance evaled' do
234
206
  mocked_class.define(:meth) { self }
@@ -245,32 +217,32 @@ describe 'define' do
245
217
  end
246
218
 
247
219
  it 'remembers what it was invoked with' do
248
- mocked_class.define(:meth) { nil }
220
+ mocked_class.define(:meth) { |*| nil }
249
221
  mock = mocked_class.new
250
222
  mock.meth 1
251
223
  mock.meth 1, 2
252
- mock.invocations(:meth).should == [Surrogate::Invocation.new([1]), Surrogate::Invocation.new([1, 2])]
224
+ invocations(mock, :meth).should == [Surrogate::Invocation.new([1]), Surrogate::Invocation.new([1, 2])]
253
225
 
254
226
  val = 0
255
227
  mock.meth(1, 2) { val = 3 }
256
- expect { mock.invocations(:meth).last.block.call }.to change { val }.from(0).to(3)
228
+ expect { invocations(mock, :meth).last.block.call }.to change { val }.from(0).to(3)
257
229
  end
258
230
 
259
231
  it 'raises an error if asked about invocations for api methods it does not know' do
260
232
  mocked_class.define :meth1
261
233
  mocked_class.define :meth2
262
234
  mock = mocked_class.new
263
- expect { mock.invocations(:meth1) }.to_not raise_error
264
- expect { mock.invocations(:meth3) }.to raise_error Surrogate::UnknownMethod, /doesn't know "meth3", only knows "initialize", "meth1", "meth2"/
235
+ expect { invocations mock, :meth1 }.to_not raise_error
236
+ expect { invocations mock, :meth3 }.to raise_error Surrogate::UnknownMethod, /doesn't know "meth3", only knows "meth1", "meth2"/
265
237
  end
266
238
  end
267
239
 
268
240
 
269
241
  describe 'clone' do
270
- it 'a repetition or further performance of the klass' do
242
+ example 'acceptance spec' do
271
243
  pristine_klass = Class.new do
272
244
  Surrogate.endow self do
273
- define(:find) { 123 }
245
+ define(:find) { |n| 123 }
274
246
  define(:bind) { 'abc' }
275
247
  end
276
248
  define(:peat) { true }
@@ -286,7 +258,7 @@ describe 'define' do
286
258
  klass2 = pristine_klass.clone
287
259
  klass2.will_find 456
288
260
  klass2.find(2).should == 456
289
- klass1.find.should == 123
261
+ klass1.find(3).should == 123
290
262
 
291
263
  klass1.should have_been_told_to(:find).with(1)
292
264
  klass2.should have_been_told_to(:find).with(2)
@@ -301,15 +273,17 @@ describe 'define' do
301
273
  superclass = Surrogate.endow Class.new
302
274
  superclass.clone.new.should be_a_kind_of superclass
303
275
  end
304
- end
305
276
 
306
- describe '#api_method_names' do
307
- it 'returns the names of the api methods as symbols' do
308
- mocked_class = Class.new do
309
- Surrogate.endow self
310
- define :abc
277
+ describe '.name' do
278
+ it 'is nil for anonymous classes' do
279
+ Surrogate.endow(Class.new).clone.name.should be_nil
280
+ end
281
+
282
+ it "is the class's name suffixed with '.clone' for named classes" do
283
+ klass = Surrogate.endow(Class.new)
284
+ self.class.const_set 'Xyz', klass
285
+ klass.clone.name.should == self.class.name + '::Xyz.clone'
311
286
  end
312
- mocked_class.api_method_names.should == [:abc]
313
287
  end
314
288
  end
315
289
  end
@@ -52,3 +52,134 @@ describe '.last_instance' do
52
52
  end
53
53
  end
54
54
  end
55
+
56
+
57
+ describe 'inspect methods' do
58
+ context 'on the class' do
59
+ context 'when anonymous identifies itself as an anonymous surrogate and lists three each of class and instance methods, alphabetically' do
60
+ example 'no class methods' do
61
+ Surrogate.endow(Class.new).inspect.should == 'AnonymousSurrogate(no api)'
62
+ end
63
+
64
+ example 'one class method' do
65
+ klass = Surrogate.endow(Class.new) { define :cmeth }
66
+ klass.inspect.should == 'AnonymousSurrogate(Class: cmeth)'
67
+ end
68
+
69
+ example 'more than three class methods' do
70
+ Surrogate.endow(Class.new) do
71
+ define :cmethd
72
+ define :cmethc
73
+ define :cmetha
74
+ define :cmethb
75
+ end.inspect.should == 'AnonymousSurrogate(Class: cmetha cmethb cmethc ...)'
76
+ end
77
+
78
+ example 'one instance method' do
79
+ Surrogate.endow(Class.new).define(:imeth).inspect.should == 'AnonymousSurrogate(Instance: imeth)'
80
+ end
81
+
82
+ example 'more than three instance methods' do
83
+ klass = Surrogate.endow Class.new
84
+ klass.define :imethd
85
+ klass.define :imethc
86
+ klass.define :imetha
87
+ klass.define :imethb
88
+ klass.inspect.should == 'AnonymousSurrogate(Instance: imetha imethb imethc ...)'
89
+ end
90
+
91
+ example 'one of each' do
92
+ Surrogate.endow(Class.new) { define :cmeth }.define(:imeth).inspect.should == 'AnonymousSurrogate(Class: cmeth, Instance: imeth)'
93
+ end
94
+
95
+ example 'more than three of each' do
96
+ klass = Surrogate.endow Class.new do
97
+ define :cmethd
98
+ define :cmethc
99
+ define :cmetha
100
+ define :cmethb
101
+ end
102
+ klass.define :imethd
103
+ klass.define :imethc
104
+ klass.define :imetha
105
+ klass.define :imethb
106
+ klass.inspect.should == 'AnonymousSurrogate(Class: cmetha cmethb cmethc ..., Instance: imetha imethb imethc ...)'
107
+ end
108
+ end
109
+
110
+ context 'when the surrogate has a name (e.g. assigned to a constant)' do
111
+ it 'inspects to the name of the constant' do
112
+ klass = Surrogate.endow(Class.new)
113
+ self.class.const_set 'Abc', klass
114
+ klass.inspect.should == self.class.name << '::Abc'
115
+ end
116
+ end
117
+ end
118
+
119
+ context 'on a clone of the surrogate' do
120
+ context 'when the surrogate has a name (e.g. assigned to a constant)' do
121
+ it 'inspects to the name of the constant, cloned' do
122
+ klass = Surrogate.endow(Class.new)
123
+ self.class.const_set 'Abc', klass
124
+ klass.clone.inspect.should == self.class.name << '::Abc.clone'
125
+ end
126
+ end
127
+ end
128
+
129
+ # eventually these should maybe show unique state (expectations/invocations) but for now, this is better than what was there before
130
+ context 'on the instance' do
131
+ context 'when anonymous' do
132
+ it 'identifies itself as an anonymous surrogate and lists three of its methods, alphabetically' do
133
+ klass = Surrogate.endow Class.new
134
+ klass.new.inspect.should == '#<AnonymousSurrogate: no api>'
135
+ klass.define :imethb
136
+ klass.new.inspect.should == '#<AnonymousSurrogate: imethb>'
137
+ klass.define :imetha
138
+ klass.define :imethd
139
+ klass.new.inspect.should == '#<AnonymousSurrogate: imetha imethb imethd>'
140
+ klass.define :imethc
141
+ klass.new.inspect.should == '#<AnonymousSurrogate: imetha imethb imethc ...>'
142
+ end
143
+ end
144
+
145
+ context 'when a clone of an anonymous surrogate' do
146
+ it 'looks the same as any other anonymous surrogate' do
147
+ klass = Surrogate.endow Class.new
148
+ klass.new.inspect.should == '#<AnonymousSurrogate: no api>'
149
+ end
150
+ end
151
+
152
+ context 'when its class has a name (e.g. for a constant)' do
153
+ it 'identifies itself as an instance of the constant and lists three of its methods, alphabetically' do
154
+ klass = Surrogate.endow Class.new
155
+ self.class.const_set 'Abc', klass
156
+ klass.stub name: 'Abc'
157
+ klass.new.inspect.should == "#<Abc: no api>"
158
+ klass.define :imethb
159
+ klass.new.inspect.should == "#<Abc: imethb>"
160
+ klass.define :imetha
161
+ klass.define :imethd
162
+ klass.new.inspect.should == "#<Abc: imetha imethb imethd>"
163
+ klass.define :imethc
164
+ klass.new.inspect.should == "#<Abc: imetha imethb imethc ...>"
165
+ end
166
+ end
167
+ end
168
+
169
+ context 'on an instance of a surrogate clone' do
170
+ context 'when its class has a name (e.g. for a constant)' do
171
+ it 'identifies itself as an instance of the clone of the constant and lists three of its methods, alphabetically' do
172
+ klass = Surrogate.endow Class.new
173
+ self.class.const_set 'Abc', klass
174
+ klass.clone.new.inspect.should == "#<#{self.class}::Abc.clone: no api>"
175
+ klass.define :imethb
176
+ klass.clone.new.inspect.should == "#<#{self.class}::Abc.clone: imethb>"
177
+ klass.define :imetha
178
+ klass.define :imethd
179
+ klass.clone.new.inspect.should == "#<#{self.class}::Abc.clone: imetha imethb imethd>"
180
+ klass.define :imethc
181
+ klass.clone.new.inspect.should == "#<#{self.class}::Abc.clone: imetha imethb imethc ...>"
182
+ end
183
+ end
184
+ end
185
+ end