bond-spy 0.1.0 → 0.2.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/bin/bond_reconcile.py +224 -134
  3. data/bond.gemspec +2 -1
  4. data/lib/bond.rb +53 -35
  5. data/lib/bond/targetable.rb +9 -3
  6. data/lib/bond/version.rb +1 -1
  7. data/spec/bond_spec.rb +16 -0
  8. data/spec/bond_targetable_spec.rb +13 -0
  9. data/spec/test_observations/bond_spec/Bond_with_agents_should_call_doers_before_returning_result.json +4 -4
  10. data/spec/test_observations/bond_spec/Bond_with_agents_should_call_the_function_passed_as_result_if_it_is_callable.json +9 -9
  11. data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_a_single_doer_if_filter_criteria_are_met.json +4 -4
  12. data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_multiple_doers.json +5 -5
  13. data/spec/test_observations/bond_spec/Bond_with_agents_should_not_call_doers_of_overriden_agents.json +2 -2
  14. data/spec/test_observations/bond_spec/Bond_with_agents_should_skip_saving_observations_when_specified.json +18 -0
  15. data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_an_exception_if_specified_by_agent.json +3 -3
  16. data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_the_result_of_the_value_passed_to_exception_if_callable.json +4 -4
  17. data/spec/test_observations/bond_spec/Bond_with_agents_should_work_with_multiple_agents_for_different_spy_points.json +9 -9
  18. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_combinations_of_filters.json +19 -19
  19. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_function_filters.json +6 -6
  20. data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_single_key_value_filters_of_all_types.json +51 -51
  21. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_nested_hashes_and_arrays_with_hash_sorting.json +22 -22
  22. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_with_a_spy_point_name.json +6 -6
  23. data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_without_a_spy_point_name.json +4 -4
  24. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_continue_is_returned.json +4 -4
  25. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_none_is_returned.json +6 -6
  26. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_private_methods.json +2 -2
  27. data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_protected_methods.json +2 -2
  28. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_class_method.json +3 -3
  29. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_normal_method.json +3 -3
  30. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_all_optional_keyword_arguments.json +4 -4
  31. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_variable_keyword_arguments.json +12 -12
  32. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_ignores_excluded_keys.json +4 -4
  33. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_mocks_when_one_is_specified.json +4 -4
  34. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_respects_mock_only.json +17 -0
  35. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_included_module_methods.json +2 -2
  36. data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_module_methods.json +3 -3
  37. data/tutorials/binary_search_tree/bst_spec.rb +1 -5
  38. data/tutorials/binary_search_tree/run_tests.sh +1 -1
  39. data/tutorials/heat_watcher/heat_watcher.rb +2 -2
  40. data/tutorials/heat_watcher/heat_watcher_spec.rb +8 -13
  41. data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_critical_errors.json +7 -49
  42. data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_warnings_and_switch_back_to_OK_status.json +9 -63
  43. metadata +20 -2
data/bond.gemspec CHANGED
@@ -3,7 +3,7 @@ require_relative 'lib/bond/version'
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'bond-spy'
5
5
  spec.version = Bond::VERSION
6
- spec.date = '2016-01-28'
6
+ spec.date = '2016-02-04'
7
7
 
8
8
  spec.authors = ['George Necula', 'Erik Krogen']
9
9
  spec.email = ['necula@cs.berkeley.edu', 'erikkrogen@gmail.com']
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.bindir = 'bin'
31
31
 
32
32
  spec.required_ruby_version = '>= 2.1'
33
+ spec.add_runtime_dependency 'neatjson', '~> 0.6'
33
34
  spec.add_development_dependency 'bundler', '~> 1.10'
34
35
  spec.add_development_dependency 'rake', '~> 10.0'
35
36
  spec.add_development_dependency 'rspec', '~> 3.0'
data/lib/bond.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'singleton'
2
- require 'json'
2
+ require 'neatjson'
3
3
  require 'fileutils'
4
4
  require 'shellwords'
5
5
 
@@ -79,14 +79,18 @@ class Bond
79
79
  # - `:abort` - don't accept any new changes
80
80
  # - `:accept` - accept all new changes
81
81
  # - `:kdiff3` - use the kdiff3 graphical merge tool to reconcile differences
82
+ # @param decimal_precision The precision to use when serializing Float and Double values.
83
+ # If not specified, defaults to 4 decimal places.
82
84
  #
83
85
  def start_test(rspec_test, test_name: nil, spy_groups: nil,
84
- observation_directory: nil, reconcile: nil)
86
+ observation_directory: nil, reconcile: nil,
87
+ decimal_precision: nil)
85
88
 
86
89
  @observations = []
87
90
  @spy_agents = Hash.new { |hash, key|
88
91
  hash[key] = []
89
92
  }
93
+ @decimal_precision = decimal_precision.nil? ? 4 : decimal_precision
90
94
  @observation_directory = nil
91
95
  @current_test = rspec_test
92
96
  @reconcile = reconcile
@@ -116,18 +120,26 @@ class Bond
116
120
  # @param spy_point_name [#to_s] The name of this spy point. Will be used to subsequently
117
121
  # refer to this point for, e.g., {#deploy_agent}. This name also gets printed as
118
122
  # part of the observation with the key `__spy_point__`.
123
+ # @param skip_save_observation [Boolean] Whether or not to skip recording the observation
124
+ # created during this call to spy; other agent actions are enabled regardless. This is
125
+ # used by {BondTargetable#spy_point} to enable `mock_only` spy points.
119
126
  # @param observation Keyword arguments which should be observed.
120
127
  # @return if an agent has been set for this spy point, this will return whatever value
121
128
  # is specified by that agent. Otherwise, returns `:agent_result_none`.
122
- def spy(spy_point_name=nil, **observation)
129
+ def spy(spy_point_name=nil, skip_save_observation=false, **observation)
123
130
  return :agent_result_none unless active? # If we're not testing, don't do anything
124
131
 
125
132
  spy_point_name = spy_point_name.nil? ? nil : spy_point_name.to_s
126
133
 
127
134
  observation[:__spy_point__] = spy_point_name unless spy_point_name.nil?
128
- observation = deep_clone_sort_hashes(observation)
135
+ observation = deep_clone(observation)
129
136
  active_agent = spy_point_name.nil? ? nil : @spy_agents[spy_point_name].find { |agent| agent.process?(observation) }
130
137
 
138
+ do_save_observation = !skip_save_observation
139
+ unless active_agent.nil? or active_agent.skip_save_observation.nil?
140
+ do_save_observation = !active_agent.skip_save_observation
141
+ end
142
+
131
143
  res = :agent_result_none
132
144
  begin
133
145
  unless active_agent.nil?
@@ -135,10 +147,12 @@ class Bond
135
147
  res = active_agent.result(observation)
136
148
  end
137
149
  ensure
138
- formatted = format_observation(observation, active_agent)
139
- @observations <<= formatted
140
- #TODO ETK printing
141
- puts "Observing: #{formatted} #{", returning <#{res.to_s}>" if res != :agent_result_none}"
150
+ if do_save_observation
151
+ formatted = format_observation(observation, active_agent)
152
+ @observations <<= formatted
153
+ #TODO ETK printing
154
+ puts "Observing: #{formatted} #{", returning <#{res.to_s}>" if res != :agent_result_none}"
155
+ end
142
156
  end
143
157
 
144
158
  res
@@ -170,28 +184,20 @@ class Bond
170
184
  # (new changes are not accepted), returns `:bond_fail`. Else returns `:pass`
171
185
  def finish_test
172
186
  fname = observation_file_name
173
- fdir = File.dirname(fname)
174
- unless File.directory?(fdir)
175
- FileUtils.mkdir_p(fdir)
176
- top_git_ignore = File.join(observation_directory, '.gitignore')
177
- unless File.file?(top_git_ignore)
178
- # TODO ETK make this configurable in case you don't use git?
179
- File.open(top_git_ignore, 'w') do |outfile|
180
- outfile.print("*_now.json\n*.diff\n")
181
- end
182
- end
187
+
188
+ if @current_test.exception.nil?
189
+ test_fail = nil
190
+ else
191
+ test_fail = "Test had failure(s): #{@current_test.exception}\n#{@current_test.exception.backtrace.join("\n")}"
183
192
  end
184
193
 
185
- test_fail = !@current_test.exception.nil?
186
-
187
194
  ref_file = fname + '.json'
188
195
  cur_file = fname + '_now.json'
189
196
  File.delete(cur_file) if File.exists?(cur_file)
190
197
  save_observations(cur_file)
191
198
 
192
- reconcile_result = reconcile_observations(ref_file, cur_file,
193
- test_fail ? 'Test had failure(s)!' : nil)
194
- return :test_fail if test_fail
199
+ reconcile_result = reconcile_observations(ref_file, cur_file, test_fail)
200
+ return :test_fail unless test_fail.nil?
195
201
  return reconcile_result
196
202
  ensure
197
203
  @current_test = nil
@@ -272,29 +278,29 @@ class Bond
272
278
  # @return The formatted hash.
273
279
  def format_observation(observation, agent = nil)
274
280
  # TODO ETK actually have formatters
275
- JSON.pretty_generate(observation, indent: ' '*4)
281
+ JSON.neat_generate(observation, sorted: true, decimals: @decimal_precision,
282
+ indent: ' '*4, wrap: true, after_colon: 1)
276
283
  end
277
284
 
278
- # Deep-clones an object while sorting any Hashes at any depth:
285
+ # Deep-clones an object
279
286
  #
280
- # - Hash: Creates a new hash containing all of the old key-value
281
- # pairs sorted by key
282
- # - Array: Creates a new array with the old contents *not* sorted
287
+ # - Hash: Creates a new hash containing all of the old key-value pairs
288
+ # - Array: Creates a new array with the old contents
283
289
  # - Other: Attempts to call Object#clone. If this fails (results in #TypeError)
284
290
  # then the object is returned as-is (assumes that non-cloneable objects
285
291
  # are immutable and thus don't need cloning)
286
292
  #
287
293
  # @param obj The object to be cloned.
288
- # @return The deep-clone with hashes sorted.
289
- def deep_clone_sort_hashes(obj)
294
+ # @return The deep-clone.
295
+ def deep_clone(obj)
290
296
  if obj.is_a?(Hash)
291
297
  {}.tap do |new|
292
- obj.sort.each do |k, v|
293
- new[k] = deep_clone_sort_hashes(v)
298
+ obj.each do |k, v|
299
+ new[k] = deep_clone(v)
294
300
  end
295
301
  end
296
- elsif obj.is_a?(Array) # Don't sort arrays, just clone
297
- obj.map { |x| deep_clone_sort_hashes(x) }
302
+ elsif obj.is_a?(Array)
303
+ obj.map { |x| deep_clone(x) }
298
304
  else
299
305
  begin
300
306
  obj.clone
@@ -353,10 +359,17 @@ class SpyAgent
353
359
  # formatting.
354
360
  #
355
361
  # - Keys that control how the observation is saved. This is processed after all
356
- # the above functions. **NOT YET AVAILABLE**
362
+ # the above functions.
357
363
  #
358
364
  # - `formatter: func` - If specified, a function that is given the observation and
359
365
  # can update it in place. The formatted observation is what gets serialized and saved.
366
+ # **NOT YET AVAILABLE**
367
+ # - `skip_save_observation: Boolean` - If specified, determines whether or not the
368
+ # observation will be saved after all of the agent's other actions have been processed.
369
+ # Useful for hiding observations of a spy point that e.g. is sometimes useful but in some
370
+ # tests is irrelevant and clutters up the observations. This value, if present, will override
371
+ # the `skip_save_observation` parameter of {Bond#spy} and the `mock_only` parameter of
372
+ # {BondTargetable#spy_point}.
360
373
  #
361
374
  def initialize(**opts)
362
375
  # TODO ETK needs formatters
@@ -364,6 +377,7 @@ class SpyAgent
364
377
  @exception_spec = nil
365
378
  @doers = []
366
379
  @filters = []
380
+ @skip_save_observation = nil
367
381
 
368
382
  opts.each do |k, v|
369
383
  case k.to_s # Convert to string in case it was passed as a symbol
@@ -373,12 +387,16 @@ class SpyAgent
373
387
  @exception_spec = v
374
388
  when 'do'
375
389
  @doers = [*v]
390
+ when 'skip_save_observation'
391
+ @skip_save_observation = v
376
392
  else # Must be a filter
377
393
  @filters <<= SpyAgentFilter.new(k.to_s, v)
378
394
  end
379
395
  end
380
396
  end
381
397
 
398
+ attr_reader :skip_save_observation
399
+
382
400
  # Checks if this agent should process this observation
383
401
  # @return true iff this agent should process this observation
384
402
  def process?(observation)
@@ -46,15 +46,21 @@ module BondTargetable
46
46
  # names to exclude from the spy. Can be useful if you don't care about the value of some argument.
47
47
  # @param spy_result [Boolean] If true, spy on the return value. The spy point name will be
48
48
  # `{spy_point_name}.result` and the key name will be `result`.
49
+ # @param mock_only [Boolean] If true, don't record calls to this method as observations.
50
+ # This allows for the ability to mock a method without observing all of its calls, useful
51
+ # if mocking e.g. a small utility method whose call order is unimportant. This can be
52
+ # overridden by using the `skip_save_observation` parameter when deploying an agent
53
+ # ({Bond#deploy_agent}).
49
54
  # @api public
50
55
  def spy_point(spy_point_name: nil, require_agent_result: false, excluded_keys: [],
51
- spy_result: false)
56
+ spy_result: false, mock_only: false)
52
57
  @__last_annotation_args = {
53
58
  spy_point_name: spy_point_name,
54
59
  require_agent_result: require_agent_result,
55
60
  # Allow for a single key or an array of them, and map to_s in case they were passed as symbols
56
61
  excluded_keys: [*excluded_keys].map(&:to_s),
57
- spy_result: spy_result
62
+ spy_result: spy_result,
63
+ mock_only: mock_only
58
64
  }
59
65
  end
60
66
 
@@ -147,7 +153,7 @@ module BondTargetable
147
153
  observation[name] = value unless options[:excluded_keys].include?(name.to_s)
148
154
  end
149
155
 
150
- ret = Bond.instance.spy(spy_point_name, observation)
156
+ ret = Bond.instance.spy(spy_point_name, options[:mock_only], observation)
151
157
  if options[:require_agent_result] && ret == :agent_result_none
152
158
  raise "#{spy_point_name} requires mocking but received :agent_result_none"
153
159
  end
data/lib/bond/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Bond
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/spec/bond_spec.rb CHANGED
@@ -146,6 +146,22 @@ describe Bond do
146
146
  bond.spy(result: bond.spy('my_point'))
147
147
  end
148
148
 
149
+ it 'should skip saving observations when specified' do
150
+ bond.spy('skipped_point', skip_save_observation = true, key: 'value')
151
+
152
+ bond.deploy_agent('skipped_point', result: 'Mock Value')
153
+ ret = bond.spy('skipped_point', skip = true, key: 'value')
154
+ bond.spy('skipped_return_value', val: ret)
155
+
156
+ bond.deploy_agent('normal_point', skip_save_observation: false, result: 'Mock Value')
157
+ ret = bond.spy('normal_point', skip_save_observation = true, key: 'value')
158
+ bond.spy('not_skipped_return_value', val: ret)
159
+
160
+ bond.deploy_agent('skipped_point', skip_save_observation: true, result: 'Mock Value')
161
+ ret = bond.spy('skipped_point', key: 'value')
162
+ bond.spy('skipped_return_value', val: ret)
163
+ end
164
+
149
165
  end
150
166
 
151
167
  # TODO
@@ -45,6 +45,9 @@ describe BondTargetable do
45
45
  bond.spy_point(spy_point_name: 'spy_return', spy_result: true)
46
46
  def annotated_method_spy_return(arg1) 'return' end
47
47
 
48
+ bond.spy_point(spy_point_name: 'mock_only', mock_only: true)
49
+ def annotated_method_mock_only; 'return' end
50
+
48
51
  bond.spy_point
49
52
  def annotated_method_with_block(arg1, &blk) yield; end
50
53
 
@@ -142,6 +145,16 @@ describe BondTargetable do
142
145
  bond.deploy_agent('spy_return', result: 'new return')
143
146
  tc.annotated_method_spy_return('arg_value')
144
147
  end
148
+
149
+ it 'correctly respects mock_only' do
150
+ bond.spy('unmocked_return', val: tc.annotated_method_mock_only)
151
+
152
+ bond.deploy_agent('mock_only', result: 'mocked return')
153
+ bond.spy('mocked_return', val: tc.annotated_method_mock_only)
154
+
155
+ bond.deploy_agent('mock_only', skip_save_observation: false, result: 'mocked return')
156
+ bond.spy('mocked_return', val: tc.annotated_method_mock_only)
157
+ end
145
158
  end
146
159
 
147
160
  it 'correctly spies protected methods' do
@@ -1,14 +1,14 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "internal_doer"
3
+ "__spy_point__": "internal_doer"
4
4
  },
5
5
  {
6
- "__spy_point__": "internal_result"
6
+ "__spy_point__": "internal_result"
7
7
  },
8
8
  {
9
- "__spy_point__": "my_point"
9
+ "__spy_point__": "my_point"
10
10
  },
11
11
  {
12
- "result": "mocked"
12
+ "result": "mocked"
13
13
  }
14
14
  ]
@@ -1,23 +1,23 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "my_point",
4
- "obs_name": "foo"
3
+ "__spy_point__": "my_point",
4
+ "obs_name": "foo"
5
5
  },
6
6
  {
7
- "result": 2
7
+ "result": 2
8
8
  },
9
9
  {
10
- "__spy_point__": "my_point",
11
- "obs_name": "bar"
10
+ "__spy_point__": "my_point",
11
+ "obs_name": "bar"
12
12
  },
13
13
  {
14
- "result": "agent_result_none"
14
+ "result": "agent_result_none"
15
15
  },
16
16
  {
17
- "__spy_point__": "my_point",
18
- "obs_name": "foo"
17
+ "__spy_point__": "my_point",
18
+ "obs_name": "foo"
19
19
  },
20
20
  {
21
- "result": 3
21
+ "result": 3
22
22
  }
23
23
  ]
@@ -1,10 +1,10 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "internal",
4
- "val": "value"
3
+ "__spy_point__": "internal",
4
+ "val": "value"
5
5
  },
6
6
  {
7
- "__spy_point__": "my_point",
8
- "my_key": "value"
7
+ "__spy_point__": "my_point",
8
+ "my_key": "value"
9
9
  }
10
10
  ]
@@ -1,13 +1,13 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "internal",
4
- "val": 10
3
+ "__spy_point__": "internal",
4
+ "val": 10
5
5
  },
6
6
  {
7
- "__spy_point__": "my_point",
8
- "my_key": 10
7
+ "__spy_point__": "my_point",
8
+ "my_key": 10
9
9
  },
10
10
  {
11
- "side_effect_value": 10
11
+ "side_effect_value": 10
12
12
  }
13
13
  ]
@@ -1,8 +1,8 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "valid_agent"
3
+ "__spy_point__": "valid_agent"
4
4
  },
5
5
  {
6
- "__spy_point__": "my_point"
6
+ "__spy_point__": "my_point"
7
7
  }
8
8
  ]
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "__spy_point__": "skipped_return_value",
4
+ "val": "Mock Value"
5
+ },
6
+ {
7
+ "__spy_point__": "normal_point",
8
+ "key": "value"
9
+ },
10
+ {
11
+ "__spy_point__": "not_skipped_return_value",
12
+ "val": "Mock Value"
13
+ },
14
+ {
15
+ "__spy_point__": "skipped_return_value",
16
+ "val": "Mock Value"
17
+ }
18
+ ]
@@ -1,9 +1,9 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "my_point"
3
+ "__spy_point__": "my_point"
4
4
  },
5
5
  {
6
- "__spy_point__": "rescue_point",
7
- "except": "TypeError exception!"
6
+ "__spy_point__": "rescue_point",
7
+ "except": "TypeError exception!"
8
8
  }
9
9
  ]
@@ -1,10 +1,10 @@
1
1
  [
2
2
  {
3
- "__spy_point__": "my_point",
4
- "key": "Value passed to exception"
3
+ "__spy_point__": "my_point",
4
+ "key": "Value passed to exception"
5
5
  },
6
6
  {
7
- "__spy_point__": "rescue_point",
8
- "except": "Value passed to exception"
7
+ "__spy_point__": "rescue_point",
8
+ "except": "Value passed to exception"
9
9
  }
10
10
  ]