bond-spy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +27 -0
  7. data/README.rst +65 -0
  8. data/Rakefile +6 -0
  9. data/bin/bond_reconcile.py +385 -0
  10. data/bin/setup +5 -0
  11. data/bond.gemspec +36 -0
  12. data/lib/bond.rb +469 -0
  13. data/lib/bond/spec_helper.rb +32 -0
  14. data/lib/bond/targetable.rb +210 -0
  15. data/lib/bond/version.rb +3 -0
  16. data/spec/bond_spec.rb +158 -0
  17. data/spec/bond_targetable_spec.rb +202 -0
  18. data/spec/spec_helper.rb +2 -0
  19. data/spec/test_observations/.gitignore +2 -0
  20. data/spec/test_observations/bond_spec/Bond_with_agents_should_call_doers_before_returning_result.json +14 -0
  21. data/spec/test_observations/bond_spec/Bond_with_agents_should_call_the_function_passed_as_result_if_it_is_callable.json +23 -0
  22. data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_a_single_doer_if_filter_criteria_are_met.json +10 -0
  23. data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_multiple_doers.json +13 -0
  24. data/spec/test_observations/bond_spec/Bond_with_agents_should_not_call_doers_of_overriden_agents.json +8 -0
  25. data/spec/test_observations/bond_spec/Bond_with_agents_should_override_old_agents_with_newer_agents.json +0 -0
  26. data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_an_exception_if_specified_by_agent.json +9 -0
  27. data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_the_result_of_the_value_passed_to_exception_if_callable.json +10 -0
  28. data/spec/test_observations/bond_spec/Bond_with_agents_should_work_with_multiple_agents_for_different_spy_points.json +23 -0
  29. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_override_old_agents_with_newer_agents_unless_theott8glo1xn.json +22 -0
  30. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_combinations_of_filters.json +37 -0
  31. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_function_filters.json +16 -0
  32. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_single_key_value_filters_of_all_types.json +121 -0
  33. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_nested_hashes_and_arrays_with_hash_sorting.json +31 -0
  34. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_with_a_spy_point_name.json +12 -0
  35. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_without_a_spy_point_name.json +10 -0
  36. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_continue_is_returned.json +10 -0
  37. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_none_is_returned.json +14 -0
  38. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_passes_through_blocks.json +10 -0
  39. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_returns_nil__and_mocks__when_an_agent_returns_nil.json +11 -0
  40. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_private_methods.json +6 -0
  41. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_protected_methods.json +6 -0
  42. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_class_method.json +7 -0
  43. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_method_with_variabl19nhijeqoo.json +13 -0
  44. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_positional_a6qc3d4el92.json +33 -0
  45. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_positional_aott8glo1xn.json +20 -0
  46. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_required_andbcgjq06had.json +17 -0
  47. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_normal_method.json +7 -0
  48. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_all_optional_keyword_arguments.json +10 -0
  49. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_variable_keyword_arguments.json +22 -0
  50. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_changes_the_spy_point_naj4gnwvcu8n.json +5 -0
  51. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_errors_when_mocking_is_r9j7wklng0z.json +6 -0
  52. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_ignores_excluded_keys.json +10 -0
  53. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_mocks_when_one_is_specified.json +10 -0
  54. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_spies_the_return_value_w19nhijeqoo.json +10 -0
  55. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_spies_the_return_value_ww8esw1qdxc.json +10 -0
  56. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_included_module_methods.json +6 -0
  57. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_module_methods.json +7 -0
  58. data/tutorials/binary_search_tree/bst.rb +120 -0
  59. data/tutorials/binary_search_tree/bst_spec.rb +82 -0
  60. data/tutorials/binary_search_tree/run_tests.sh +4 -0
  61. data/tutorials/binary_search_tree/test_observations/.gitignore +2 -0
  62. data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_add_nodes_to_the_BST_correctly__testing_with_Bond.json +20 -0
  63. data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_add_nodes_to_the_BST_correctly__testing_without_Bond.json +3 -0
  64. data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_correctly_delete_nodes_from_the_BST.json +29 -0
  65. data/tutorials/heat_watcher/heat_watcher.rb +107 -0
  66. data/tutorials/heat_watcher/heat_watcher_spec.rb +116 -0
  67. data/tutorials/heat_watcher/run_tests.sh +4 -0
  68. data/tutorials/heat_watcher/test_observations/.gitignore +2 -0
  69. data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_critical_errors.json +142 -0
  70. data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_warnings_and_switch_back_to_OK_status.json +132 -0
  71. metadata +211 -0
@@ -0,0 +1,32 @@
1
+ require_relative '../bond'
2
+
3
+ # This file defines a shared context which should be included
4
+ # into any RSpec test using Bond via:
5
+ #
6
+ # include_context :bond
7
+ #
8
+ # within your `describe` statement. It makes the `bond` variable
9
+ # available for you to use (for e.g. `bond.spy` and `bond.deploy_agent`)
10
+ # and automatically initializes Bond to be used in your tests.
11
+ #
12
+ # You may pass all of the same arguments to the `include_context` statement
13
+ # that you can to {Bond#start_test}. For example, to set the test name:
14
+ #
15
+ # include_context :bond, test_name: 'my_test_name'
16
+ #
17
+
18
+ shared_context :bond do |**settings|
19
+
20
+ let(:bond) { Bond.instance }
21
+
22
+ before :each do |example|
23
+ bond.start_test(example, **settings)
24
+ true
25
+ end
26
+
27
+ after :each do
28
+ if bond.send(:finish_test) == :bond_fail
29
+ fail('BOND_FAIL. Pass BOND_RECONCILE=[kdiff3|console|accept] environment variable to reconcile the observations.')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,210 @@
1
+ # Module which should be `extend`ed into any class or module which you would like
2
+ # to spy on, either via {Bond#spy} or via a {#spy_point}. Within that class/module,
3
+ # the method {#bond} is available to access Bond, i.e. `bond.spy` and `bond.spy_point`.
4
+ #
5
+ # For example, if you would like to spy on MyClass, you would do this:
6
+ #
7
+ # class MyClass
8
+ # extend BondTargetable
9
+ #
10
+ # bond.spy_point(spy_point_name: 'my_name')
11
+ # def method_to_spy_on
12
+ # ...
13
+ # bond.spy(intermediate_state: state_variable)
14
+ # ...
15
+ # end
16
+ # end
17
+ #
18
+ module BondTargetable
19
+
20
+ protected
21
+
22
+ # Keeps track of the arguments to the last spy_point annotation
23
+ @__last_annotation_args = nil
24
+
25
+ # The annotation which indicates that a method should be spied on. Use like:
26
+ #
27
+ # bond.spy_point(options...)
28
+ # def method_being_spied_on()
29
+ #
30
+ # This is safe to use in production code; it will have effects only if the function
31
+ # {Bond#start_test} has been called to initialize Bond.
32
+ # This will automatically spy every call to the `method_being_spied_on`. By default,
33
+ # all supplied arguments are logged. For normal arguments and keyword arguments,
34
+ # they will be logged as `argument_name: value`. For variable arguments,
35
+ # they are logged as `anonymous_parameter{arg_num}: value` (e.g. `anonymous_parameter0: 'foo'`).
36
+ # If the call to {Bond#spy} returns anything other than `:agent_result_none` or
37
+ # `:agent_result_continue` (i.e. through the use of {Bond#deploy_agent}), the spied
38
+ # method will never be called and the result will be returned in its place. Else, the
39
+ # method is called as normal.
40
+ #
41
+ # @param spy_point_name [String] If supplied, overrides the default spy point name, which is
42
+ # `Class.method_name` for class methods and `Class#method_name` for other methods.
43
+ # @param require_agent_result [Boolean] If true, you *must* supply a return value via
44
+ # {Bond#deploy_agent} during testing, else an error is thrown.
45
+ # @param excluded_keys [String, Symbol, Array<String, Symbol>] An array of key/argument
46
+ # names to exclude from the spy. Can be useful if you don't care about the value of some argument.
47
+ # @param spy_result [Boolean] If true, spy on the return value. The spy point name will be
48
+ # `{spy_point_name}.result` and the key name will be `result`.
49
+ # @api public
50
+ def spy_point(spy_point_name: nil, require_agent_result: false, excluded_keys: [],
51
+ spy_result: false)
52
+ @__last_annotation_args = {
53
+ spy_point_name: spy_point_name,
54
+ require_agent_result: require_agent_result,
55
+ # Allow for a single key or an array of them, and map to_s in case they were passed as symbols
56
+ excluded_keys: [*excluded_keys].map(&:to_s),
57
+ spy_result: spy_result
58
+ }
59
+ end
60
+
61
+ # Simple method to access Bond from within a BondTargetable class/module.
62
+ # @api public
63
+ def bond
64
+ PassthroughClass.new(self)
65
+ end
66
+
67
+ public
68
+
69
+ # Hook into method addition, if it was preceded by a call to #spy_point then spy on it.
70
+ # @param name Name of the method being added
71
+ # @private
72
+ def method_added(name)
73
+ super
74
+ return if @__last_annotation_args.nil?
75
+ annotation_args = @__last_annotation_args
76
+ @__last_annotation_args = nil
77
+
78
+ orig_method = instance_method(name)
79
+
80
+ if private_method_defined?(name)
81
+ visibility = :private
82
+ elsif protected_method_defined?(name)
83
+ visibility = :protected
84
+ else
85
+ visibility = :public
86
+ end
87
+
88
+ point_name = annotation_args.delete(:spy_point_name)
89
+ point_name = point_name_from_method(orig_method) if point_name.nil?
90
+
91
+ this = self
92
+ define_method(name) do |*args, **kwargs, &blk|
93
+ this.send(:bond_interceptor, orig_method.bind(self), point_name, annotation_args, *args, **kwargs, &blk)
94
+ end
95
+
96
+ case visibility
97
+ when :protected
98
+ protected name
99
+ when :private
100
+ private name
101
+ end
102
+ end
103
+
104
+ # Hook into singleton method addition, if it was preceded by a call to #spy_point then spy on it.
105
+ # @param name Name of the method being added
106
+ # @private
107
+ def singleton_method_added(name)
108
+ super
109
+ return if @__last_annotation_args.nil?
110
+ annotation_args = @__last_annotation_args
111
+ @__last_annotation_args = nil
112
+
113
+ orig_method = method(name)
114
+
115
+ point_name = annotation_args.delete(:spy_point_name)
116
+ point_name = point_name_from_method(orig_method) if point_name.nil?
117
+
118
+ this = self
119
+ this.define_singleton_method(name) do |*args, **kwargs, &blk|
120
+ this.send(:bond_interceptor, orig_method, point_name, annotation_args, *args, **kwargs, &blk)
121
+ end
122
+
123
+ end
124
+
125
+ private
126
+
127
+ # Method that gets wrapped around methods being spied on. Should never be called directly.
128
+ #
129
+ # @param method [Method] The method that is being spied on
130
+ # @param spy_point_name [String] The name of the spy point
131
+ # @param options [Hash] Hash of options, which should be any of the arguments to {#spy_point}
132
+ # except for `spy_point_name`
133
+ def bond_interceptor(method, spy_point_name, options, *args, **kwargs, &blk)
134
+ return kwargs.empty? ? method.call(*args, &blk) : method.call(*args, **kwargs, &blk) unless Bond.instance.active?
135
+ param_list = method.parameters.select { |type, _| type == :opt || type == :req }.map { |_, name| name }
136
+
137
+ observation = {}
138
+ anon_param_cnt = 0
139
+ args.zip(param_list) do |value, name|
140
+ if name.nil?
141
+ observation["anonymous_parameter#{anon_param_cnt}".to_sym] = value
142
+ else
143
+ observation[name] = value unless options[:excluded_keys].include?(name.to_s)
144
+ end
145
+ end
146
+ kwargs.each do |name, value|
147
+ observation[name] = value unless options[:excluded_keys].include?(name.to_s)
148
+ end
149
+
150
+ ret = Bond.instance.spy(spy_point_name, observation)
151
+ if options[:require_agent_result] && ret == :agent_result_none
152
+ raise "#{spy_point_name} requires mocking but received :agent_result_none"
153
+ end
154
+
155
+ if ret == :agent_result_none || ret == :agent_result_continue
156
+ if kwargs.empty?
157
+ ret = method.call(*args, &blk)
158
+ else
159
+ ret = method.call(*args, **kwargs, &blk)
160
+ end
161
+ end
162
+ Bond.instance.spy("#{spy_point_name}.result", result: ret) if options[:spy_result]
163
+ ret
164
+ end
165
+
166
+ # Extract the default point name from the method object, which is `Class.method_name`
167
+ # for class methods and `Class#method_name` for other methods.
168
+ def point_name_from_method(method)
169
+ method.inspect.to_s.sub(/#<[^:]+: ([^>]+)>/, '\1')
170
+ end
171
+
172
+ # A class that acts as if it was Bond by passing through all method calls
173
+ # *except* for `spy_point`, which it sends back to whatever object was passed
174
+ # in upon initialization (which should be something that `extend`s BondTargetable).
175
+ # This is used since calls to `spy_point` should be directed to BondTargetable
176
+ # and other calls should be directed to Bond, but we want this to happen
177
+ # transparently to the end-user.
178
+ # @private
179
+ class PassthroughClass
180
+ def initialize(parent)
181
+ @parent = parent
182
+ end
183
+
184
+ def spy_point(**kwargs)
185
+ @parent.send(:spy_point, **kwargs)
186
+ end
187
+
188
+ def method_missing(meth, *args)
189
+ Bond.instance.send(meth, *args)
190
+ end
191
+ end
192
+
193
+ # A module to export the `bond` method as an instance method in addition to
194
+ # a class method (which it will already appear as due to the `extend` statement)
195
+ # @private
196
+ module BondTargetableInstanceMethods
197
+ protected
198
+ def bond
199
+ PassthroughClass.new(self)
200
+ end
201
+ end
202
+
203
+ public
204
+ # Used to mix in BondTargetableInstanceMethods, allowing for `bond` to appear as both
205
+ # an instance method and a class method.
206
+ # @private
207
+ def self.extended(base)
208
+ base.include(BondTargetableInstanceMethods)
209
+ end
210
+ end
@@ -0,0 +1,3 @@
1
+ class Bond
2
+ VERSION = '0.1.0'
3
+ end
data/spec/bond_spec.rb ADDED
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bond do
4
+ include_context :bond
5
+
6
+ context 'without any agents' do
7
+ it 'should correctly log some normal arguments with a spy point name' do
8
+ bond.spy('my_point_name', spy_key: 1, spy_key_2: 'string value', spy_key_3: 1.4)
9
+ bond.spy('second_point', spy_key: 'string value 2')
10
+ end
11
+
12
+ it 'should correctly log some normal arguments without a spy point name' do
13
+ bond.spy(spy_key: 1, spy_key_2: 'string value', spy_key_3: 1.4)
14
+ bond.spy(spy_key: 'string value 2')
15
+ end
16
+
17
+ it 'should correctly log nested hashes and arrays with hash sorting' do
18
+ bond.spy(key1: {h1key1: %w(hello world), h1key2: 'hi'},
19
+ key2: [{key2: 'foo', key3: 'bar', key1: 'baz'}, 'array3', 'array2'])
20
+ bond.spy('point_name', key3: {key2: 'foo', key1: 'bar', key3: {hkey: 'hello world'}},
21
+ key1: 'value 1')
22
+ end
23
+ end
24
+
25
+ context 'with agents' do
26
+ it 'should work with multiple agents for different spy points' do
27
+ bond.deploy_agent('my_point_1', result: 'mock result')
28
+ bond.deploy_agent('my_point_2', result: 'mock result 2')
29
+
30
+ bond.spy('point_1_return', return_value: bond.spy('my_point_1'))
31
+ bond.spy('point_2_return', return_value: bond.spy('my_point_2'))
32
+ bond.spy('point_1_return_2', return_value: bond.spy('my_point_1'))
33
+ end
34
+
35
+ context 'with filters' do
36
+ it 'should respect single key/value filters of all types' do
37
+ bond.deploy_agent('my_point', obs_name: 'value', result: 'mocked')
38
+ bond.spy(result: bond.spy('my_point', obs_name: 'value'))
39
+ bond.spy(result: bond.spy('my_point', obs_name: 'not value'))
40
+
41
+ bond.deploy_agent('my_point_eq', obs_name__eq: 'value', result: 'mocked')
42
+ bond.spy(result: bond.spy('my_point_eq', obs_name: 'value'))
43
+ bond.spy(result: bond.spy('my_point_eq', obs_name: 'not value'))
44
+
45
+ bond.deploy_agent('my_point_exact', obs_name__exact: 'value', result: 'mocked')
46
+ bond.spy(result: bond.spy('my_point_exact', obs_name: 'value'))
47
+ bond.spy(result: bond.spy('my_point_exact', obs_name: 'not value'))
48
+
49
+ bond.deploy_agent('my_point_startswith', obs_name__startswith: 'value', result: 'mocked')
50
+ bond.spy(result: bond.spy('my_point_startswith', obs_name: 'value'))
51
+ bond.spy(result: bond.spy('my_point_startswith', obs_name: 'not value'))
52
+ bond.spy(result: bond.spy('my_point_startswith', obs_name: 'value not'))
53
+
54
+ bond.deploy_agent('my_point_endswith', obs_name__endswith: 'value', result: 'mocked')
55
+ bond.spy(result: bond.spy('my_point_endswith', obs_name: 'value'))
56
+ bond.spy(result: bond.spy('my_point_endswith', obs_name: 'not value'))
57
+ bond.spy(result: bond.spy('my_point_endswith', obs_name: 'value not'))
58
+
59
+ bond.deploy_agent('my_point_contains', obs_name__contains: 'value', result: 'mocked')
60
+ bond.spy(result: bond.spy('my_point_contains', obs_name: 'value'))
61
+ bond.spy(result: bond.spy('my_point_contains', obs_name: 'not value'))
62
+ bond.spy(result: bond.spy('my_point_contains', obs_name: 'value not'))
63
+ bond.spy(result: bond.spy('my_point_contains', obs_name: 'not value not'))
64
+ bond.spy(result: bond.spy('my_point_contains', obs_name: 'foobar'))
65
+ end
66
+
67
+ it 'should respect function filters' do
68
+ bond.deploy_agent('my_point', filter: lambda { |obs| obs[:my_key].even? }, result: 'mocked')
69
+ bond.spy(result: bond.spy('my_point', my_key: 5))
70
+ bond.spy(result: bond.spy('my_point', my_key: 10))
71
+ end
72
+
73
+ it 'should override old agents with newer agents unless they are filtered out' do
74
+ bond.deploy_agent('my_point', result: 'first mock')
75
+ bond.deploy_agent('my_point', result: 'second mock')
76
+ bond.deploy_agent('my_point', obs_name: 'foobar', result: 'third mock')
77
+
78
+ bond.spy(result: bond.spy('my_point'))
79
+ bond.spy(result: bond.spy('my_point', obs_name: 'foobar'))
80
+ bond.spy(result: bond.spy('my_point', obs_name: 'baz'))
81
+ end
82
+
83
+ it 'should respect combinations of filters' do
84
+ bond.deploy_agent('my_point', result: 'mocked',
85
+ filter: lambda { |obs| obs.has_key?(:req_key) },
86
+ req_key2: 'value', req_key3__contains: 'value')
87
+ bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'bar', req_key3: 'value'))
88
+ bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'value', req_key3: 'foovaluebar'))
89
+ bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'value', req_key3: 'lacking'))
90
+ bond.spy(result: bond.spy('my_point', req_key2: 'value', req_key3: 'value'))
91
+ end
92
+ end
93
+
94
+ it 'should call the function passed as result if it is callable' do
95
+ mocked_value = 1
96
+ bond.deploy_agent('my_point', result: lambda { |_| mocked_value += 1 }, obs_name: 'foo')
97
+ bond.spy(result: bond.spy('my_point', obs_name: 'foo'))
98
+ bond.spy(result: bond.spy('my_point', obs_name: 'bar'))
99
+ bond.spy(result: bond.spy('my_point', obs_name: 'foo'))
100
+ end
101
+
102
+ it 'should throw an exception if specified by agent' do
103
+ bond.deploy_agent('my_point', exception: TypeError.new('TypeError exception!'))
104
+ begin
105
+ bond.spy('my_point')
106
+ rescue TypeError => e
107
+ bond.spy('rescue_point', except: e.to_s)
108
+ end
109
+ end
110
+
111
+ it 'should throw the result of the value passed to exception if callable' do
112
+ bond.deploy_agent('my_point', exception: lambda { |obs| ArgumentError.new(obs[:key]) })
113
+ begin
114
+ bond.spy('my_point', key: 'Value passed to exception')
115
+ rescue ArgumentError => e
116
+ bond.spy('rescue_point', except: e.to_s)
117
+ end
118
+ end
119
+
120
+ it 'should correctly call a single doer if filter criteria are met' do
121
+ bond.deploy_agent('my_point', my_key__contains: '',
122
+ do: lambda { |obs| bond.spy('internal', val: obs[:my_key]) })
123
+ bond.spy('my_point', my_key: 'value')
124
+ end
125
+
126
+ it 'should correctly call multiple doers' do
127
+ side_effect_value = 0
128
+ doers = [
129
+ lambda { |obs| bond.spy('internal', val: obs[:my_key]) },
130
+ lambda { |obs| side_effect_value += obs[:my_key] }
131
+ ]
132
+ bond.deploy_agent('my_point', do: doers)
133
+ bond.spy('my_point', my_key: 10)
134
+ bond.spy(side_effect_value: side_effect_value)
135
+ end
136
+
137
+ it 'should not call doers of overriden agents' do
138
+ bond.deploy_agent('my_point', do: lambda { |_| bond.spy('bad_agent') })
139
+ bond.deploy_agent('my_point', do: lambda { |_| bond.spy('valid_agent') })
140
+ bond.spy('my_point')
141
+ end
142
+
143
+ it 'should call doers before returning result' do
144
+ bond.deploy_agent('my_point', do: lambda { |_| bond.spy('internal_doer') },
145
+ result: lambda { |_| bond.spy('internal_result'); 'mocked' })
146
+ bond.spy(result: bond.spy('my_point'))
147
+ end
148
+
149
+ end
150
+
151
+ # TODO
152
+ # overriding settings using Bond#settings
153
+ # some different start_test parameters
154
+
155
+ # finish test: creating output dir,
156
+ # some tests for SpyAgent and SpyAgentFilter - break out into separate files?
157
+
158
+ end
@@ -0,0 +1,202 @@
1
+ require 'spec_helper'
2
+
3
+ describe BondTargetable do
4
+ include_context :bond
5
+
6
+ class TestClass
7
+ extend BondTargetable
8
+
9
+ bond.spy_point
10
+ def annotated_standard_method(arg1, arg2) end
11
+
12
+ bond.spy_point
13
+ def self.annotated_class_method(arg1, arg2) end
14
+
15
+ bond.spy_point
16
+ def annotated_method_var_args(arg1, arg2, *args) end
17
+
18
+ bond.spy_point
19
+ def annotated_method_kw_params_mixed(arg1:, arg2: 'default', arg3: 'default') end
20
+
21
+ bond.spy_point
22
+ def annotated_method_kw_params_optional(arg1: 'default', arg2: 'default') end
23
+
24
+ bond.spy_point
25
+ def annotated_method_variable_kw_args(arg1:, arg2: 'default', **kwargs) end
26
+
27
+ bond.spy_point
28
+ def annotated_method_mixed_params(arg1, arg2, arg3: 'default', arg4: 'foobar') end
29
+
30
+ bond.spy_point
31
+ def annotated_method_mixed_variable_params(arg1, *args, arg_n1:, arg_n2: 'default', **kwargs) end
32
+
33
+ bond.spy_point(spy_point_name: 'my_name')
34
+ def annotated_method_with_name; end
35
+
36
+ bond.spy_point(excluded_keys: :arg1)
37
+ def annotated_method_single_exclude(arg1, arg2) end
38
+
39
+ bond.spy_point(excluded_keys: ['arg1', 'arg3'])
40
+ def annotated_method_multiple_exclude(arg1, arg2:, arg3: 'default') end
41
+
42
+ bond.spy_point(spy_point_name: 'mock_required', require_agent_result: true)
43
+ def annotated_method_mocking_required(arg1) 'return' end
44
+
45
+ bond.spy_point(spy_point_name: 'spy_return', spy_result: true)
46
+ def annotated_method_spy_return(arg1) 'return' end
47
+
48
+ bond.spy_point
49
+ def annotated_method_with_block(arg1, &blk) yield; end
50
+
51
+ def method_calling_protected; annotated_protected_method('value') end
52
+
53
+ def method_calling_private; annotated_private_method('value') end
54
+
55
+ protected
56
+ spy_point
57
+ def annotated_protected_method(arg1) end
58
+
59
+ private
60
+ spy_point
61
+ def annotated_private_method(arg1) end
62
+ end
63
+
64
+ let (:tc) { TestClass.new }
65
+
66
+ context 'with different argument types' do
67
+ it 'correctly spies on a normal method' do
68
+ tc.annotated_standard_method(1, 2)
69
+ end
70
+
71
+ it 'correctly spies on a class method' do
72
+ TestClass.annotated_class_method(1, 2)
73
+ end
74
+
75
+ it 'correctly spies on a method with variable arguments' do
76
+ tc.annotated_method_var_args('value1', 'value2')
77
+ tc.annotated_method_var_args('value1', 'value2', 'value3', 'value4')
78
+ end
79
+
80
+ it 'correctly spies on a mix of required and optional keyword arguments' do
81
+ tc.annotated_method_kw_params_mixed(arg1: 'value')
82
+ tc.annotated_method_kw_params_mixed(arg1: 'value', arg3: 'new value')
83
+ tc.annotated_method_kw_params_mixed(arg1: 'value', arg3: 'new value3', arg2: 'new value2')
84
+ end
85
+
86
+ it 'correctly spies on all optional keyword arguments' do
87
+ tc.annotated_method_kw_params_optional
88
+ tc.annotated_method_kw_params_optional(arg1: 'value1', arg2: 'value2')
89
+ end
90
+
91
+ it 'correctly spies on variable keyword arguments' do
92
+ tc.annotated_method_variable_kw_args(arg1: 'value1')
93
+ tc.annotated_method_variable_kw_args(arg1: 'value1', arg2: 'value2')
94
+ tc.annotated_method_variable_kw_args(arg1: 'value1', arg3: 'value3')
95
+ tc.annotated_method_variable_kw_args(arg1: 'value1', arg4: 'value4', arg3: 'value3')
96
+ end
97
+
98
+ it 'correctly spies on a mix of positional and named arguments' do
99
+ tc.annotated_method_mixed_params('foo', 'bar')
100
+ tc.annotated_method_mixed_params('foo', 'bar', arg4: 'new value4')
101
+ tc.annotated_method_mixed_params('foo', 'bar', arg3: 'new value3', arg4: 'new value4')
102
+ end
103
+
104
+ it 'correctly spies on a mix of positional and named arguments, both variable' do
105
+ tc.annotated_method_mixed_variable_params('value1', arg_n1: 'value_n1')
106
+ tc.annotated_method_mixed_variable_params('value1', arg_n1: 'value_n1', arg_n2: 'value_n2')
107
+ tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1', arg_n2: 'value_n2')
108
+ tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1')
109
+ tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1', arg_n3: 'value_n3')
110
+ end
111
+ end
112
+
113
+ context 'with different spy_point parameters' do
114
+ it 'correctly changes the spy point name if it is specified' do
115
+ tc.annotated_method_with_name
116
+ end
117
+
118
+ it 'correctly ignores excluded keys' do
119
+ tc.annotated_method_single_exclude('value1', 'value2')
120
+ tc.annotated_method_multiple_exclude('value1', arg2: 'value2', arg3: 'value3')
121
+ end
122
+
123
+ it 'correctly errors when mocking is required and none is specified' do
124
+ begin
125
+ tc.annotated_method_mocking_required(2)
126
+ fail
127
+ rescue RuntimeError
128
+ end
129
+ end
130
+
131
+ it 'correctly mocks when one is specified' do
132
+ bond.deploy_agent('mock_required', result: 'new return')
133
+ ret = tc.annotated_method_mocking_required(5)
134
+ bond.spy('return_value', return: ret)
135
+ end
136
+
137
+ it 'correctly spies the return value when not mocking' do
138
+ tc.annotated_method_spy_return('arg_value')
139
+ end
140
+
141
+ it 'correctly spies the return value when mocking' do
142
+ bond.deploy_agent('spy_return', result: 'new return')
143
+ tc.annotated_method_spy_return('arg_value')
144
+ end
145
+ end
146
+
147
+ it 'correctly spies protected methods' do
148
+ tc.method_calling_protected
149
+ end
150
+
151
+ it 'correctly spies private methods' do
152
+ tc.method_calling_private
153
+ end
154
+
155
+ it 'correctly continues to the method when agent_result_continue is returned' do
156
+ bond.deploy_agent('mock_required', result: :agent_result_continue)
157
+ ret = tc.annotated_method_mocking_required('value1')
158
+ bond.spy('return value', ret: ret)
159
+ end
160
+
161
+ it 'correctly continues to the method when agent_result_none is returned' do
162
+ bond.deploy_agent('spy_return', result: :agent_result_none)
163
+ ret = tc.annotated_method_spy_return('value')
164
+ bond.spy('return value', ret: ret)
165
+ end
166
+
167
+ it 'correctly passes through blocks' do
168
+ ret = tc.annotated_method_with_block('foo') { 'value' }
169
+ bond.spy('return value', ret: ret)
170
+ end
171
+
172
+ it 'correctly returns nil (and mocks) when an agent returns nil' do
173
+ arr = [0]
174
+ bond.deploy_agent('TestClass#annotated_method_with_block', result: nil)
175
+ ret = tc.annotated_method_with_block('foo') { arr[0] = 1 }
176
+ bond.spy('return value', ret: ret, arr_val: arr[0])
177
+ end
178
+
179
+ context 'with modules' do
180
+ module TestModule
181
+ extend BondTargetable
182
+
183
+ bond.spy_point
184
+ def self.annotated_class_method(arg1, arg2) end
185
+
186
+ bond.spy_point
187
+ def annotated_standard_method(arg1) end
188
+
189
+ end
190
+
191
+ class TestClassWithMixin; include TestModule; end
192
+
193
+ it 'correctly spies on module methods' do
194
+ TestModule.annotated_class_method('value1', 'value2')
195
+ end
196
+
197
+ it 'correctly spies on included module methods' do
198
+ TestClassWithMixin.new.annotated_standard_method('value')
199
+ end
200
+
201
+ end
202
+ end