bond-spy 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.
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