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