surrogate 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # set this with
2
- # export BUNDLE_GEMFILE=gemfiles/rspec_mocks_2.10
2
+ # export BUNDLE_GEMFILE=gemfiles/rspec_mocks_2.11
3
3
 
4
4
  source "http://rubygems.org"
5
5
 
@@ -1,7 +1,7 @@
1
1
  require 'surrogate/version'
2
2
  require 'surrogate/hatchling'
3
3
  require 'surrogate/hatchery'
4
- require 'surrogate/options'
4
+ require 'surrogate/method_definition'
5
5
  require 'surrogate/values'
6
6
  require 'surrogate/endower'
7
7
  require 'surrogate/api_comparer'
@@ -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 ||= SurrogateMethods.new(surrogate).methods
16
+ @surrogate_methods ||= SurrogateClassReflector.new(surrogate).methods
15
17
  end
16
18
 
17
19
  def actual_methods
18
- @actual_methods ||= ActualMethods.new(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
- # 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)
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
- def instance_other_methods
71
- Set.new(actual.instance_methods) - instance_inherited_methods
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
- def class_inherited_methods
75
- Set.new actual.singleton_class.instance_methods - actual.singleton_class.instance_methods(false)
76
- end
80
+ private
77
81
 
78
- def class_other_methods
79
- Set.new(actual.singleton_class.instance_methods) - class_inherited_methods
80
- end
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
- 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
98
+ def type_for(method)
99
+ method.parameters.map(&:first)
100
+ end
102
101
 
103
- def instance_inherited_methods
104
- Set.new surrogate.instance_methods - surrogate.instance_methods(false)
105
- end
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
- def instance_other_methods
108
- Set.new(surrogate.instance_methods false) - instance_api_methods
109
- end
108
+ def instance_api_method_for(name)
109
+ class_hatchery.api_method_for name
110
+ end
110
111
 
111
- def class_api_methods
112
- Set.new surrogate.singleton_class.api_method_names
113
- end
112
+ def class_api_method_for(name)
113
+ singleton_class_hatchery.api_method_for name
114
+ end
114
115
 
115
- # should have new and clone (don't screw up substitutability because of how we implement these)
116
- def class_inherited_methods
117
- Set.new surrogate.singleton_class.instance_methods - surrogate.singleton_class.instance_methods(false)
118
- end
116
+ def class_hatchery
117
+ @class_hatchery ||= surrogate.instance_variable_get :@hatchery
118
+ end
119
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
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
@@ -34,8 +34,7 @@ class Surrogate
34
34
  klass.extend ClassMethods
35
35
  add_hatchery_to klass
36
36
  enable_defining_methods klass
37
- record_initialization_for_instances_of klass
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.send :initialize, *args
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
@@ -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] = Options.new options, block
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
@@ -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
+