bond-spy 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
  ]