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.
- checksums.yaml +4 -4
- data/bin/bond_reconcile.py +224 -134
- data/bond.gemspec +2 -1
- data/lib/bond.rb +53 -35
- data/lib/bond/targetable.rb +9 -3
- data/lib/bond/version.rb +1 -1
- data/spec/bond_spec.rb +16 -0
- data/spec/bond_targetable_spec.rb +13 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_call_doers_before_returning_result.json +4 -4
- data/spec/test_observations/bond_spec/Bond_with_agents_should_call_the_function_passed_as_result_if_it_is_callable.json +9 -9
- data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_a_single_doer_if_filter_criteria_are_met.json +4 -4
- data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_multiple_doers.json +5 -5
- data/spec/test_observations/bond_spec/Bond_with_agents_should_not_call_doers_of_overriden_agents.json +2 -2
- data/spec/test_observations/bond_spec/Bond_with_agents_should_skip_saving_observations_when_specified.json +18 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_an_exception_if_specified_by_agent.json +3 -3
- data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_the_result_of_the_value_passed_to_exception_if_callable.json +4 -4
- data/spec/test_observations/bond_spec/Bond_with_agents_should_work_with_multiple_agents_for_different_spy_points.json +9 -9
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_combinations_of_filters.json +19 -19
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_function_filters.json +6 -6
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_single_key_value_filters_of_all_types.json +51 -51
- data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_nested_hashes_and_arrays_with_hash_sorting.json +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
- data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_without_a_spy_point_name.json +4 -4
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_continue_is_returned.json +4 -4
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_none_is_returned.json +6 -6
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_private_methods.json +2 -2
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_protected_methods.json +2 -2
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_class_method.json +3 -3
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_normal_method.json +3 -3
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_all_optional_keyword_arguments.json +4 -4
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_variable_keyword_arguments.json +12 -12
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_ignores_excluded_keys.json +4 -4
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_mocks_when_one_is_specified.json +4 -4
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_respects_mock_only.json +17 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_included_module_methods.json +2 -2
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_module_methods.json +3 -3
- data/tutorials/binary_search_tree/bst_spec.rb +1 -5
- data/tutorials/binary_search_tree/run_tests.sh +1 -1
- data/tutorials/heat_watcher/heat_watcher.rb +2 -2
- data/tutorials/heat_watcher/heat_watcher_spec.rb +8 -13
- data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_critical_errors.json +7 -49
- data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_warnings_and_switch_back_to_OK_status.json +9 -63
- 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-
|
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 '
|
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 =
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
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.
|
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
|
285
|
+
# Deep-clones an object
|
279
286
|
#
|
280
|
-
# - Hash: Creates a new hash containing all of the old key-value
|
281
|
-
#
|
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
|
289
|
-
def
|
294
|
+
# @return The deep-clone.
|
295
|
+
def deep_clone(obj)
|
290
296
|
if obj.is_a?(Hash)
|
291
297
|
{}.tap do |new|
|
292
|
-
obj.
|
293
|
-
new[k] =
|
298
|
+
obj.each do |k, v|
|
299
|
+
new[k] = deep_clone(v)
|
294
300
|
end
|
295
301
|
end
|
296
|
-
elsif obj.is_a?(Array)
|
297
|
-
obj.map { |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.
|
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)
|
data/lib/bond/targetable.rb
CHANGED
@@ -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
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
|
-
|
3
|
+
"__spy_point__": "internal_doer"
|
4
4
|
},
|
5
5
|
{
|
6
|
-
|
6
|
+
"__spy_point__": "internal_result"
|
7
7
|
},
|
8
8
|
{
|
9
|
-
|
9
|
+
"__spy_point__": "my_point"
|
10
10
|
},
|
11
11
|
{
|
12
|
-
|
12
|
+
"result": "mocked"
|
13
13
|
}
|
14
14
|
]
|
@@ -1,23 +1,23 @@
|
|
1
1
|
[
|
2
2
|
{
|
3
|
-
|
4
|
-
|
3
|
+
"__spy_point__": "my_point",
|
4
|
+
"obs_name": "foo"
|
5
5
|
},
|
6
6
|
{
|
7
|
-
|
7
|
+
"result": 2
|
8
8
|
},
|
9
9
|
{
|
10
|
-
|
11
|
-
|
10
|
+
"__spy_point__": "my_point",
|
11
|
+
"obs_name": "bar"
|
12
12
|
},
|
13
13
|
{
|
14
|
-
|
14
|
+
"result": "agent_result_none"
|
15
15
|
},
|
16
16
|
{
|
17
|
-
|
18
|
-
|
17
|
+
"__spy_point__": "my_point",
|
18
|
+
"obs_name": "foo"
|
19
19
|
},
|
20
20
|
{
|
21
|
-
|
21
|
+
"result": 3
|
22
22
|
}
|
23
23
|
]
|
data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_multiple_doers.json
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
[
|
2
2
|
{
|
3
|
-
|
4
|
-
|
3
|
+
"__spy_point__": "internal",
|
4
|
+
"val": 10
|
5
5
|
},
|
6
6
|
{
|
7
|
-
|
8
|
-
|
7
|
+
"__spy_point__": "my_point",
|
8
|
+
"my_key": 10
|
9
9
|
},
|
10
10
|
{
|
11
|
-
|
11
|
+
"side_effect_value": 10
|
12
12
|
}
|
13
13
|
]
|
@@ -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,10 +1,10 @@
|
|
1
1
|
[
|
2
2
|
{
|
3
|
-
|
4
|
-
|
3
|
+
"__spy_point__": "my_point",
|
4
|
+
"key": "Value passed to exception"
|
5
5
|
},
|
6
6
|
{
|
7
|
-
|
8
|
-
|
7
|
+
"__spy_point__": "rescue_point",
|
8
|
+
"except": "Value passed to exception"
|
9
9
|
}
|
10
10
|
]
|