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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ff046742322ed31d8ac6ce9b5145d67072af7593
4
- data.tar.gz: 95c911f15617180b17afc73b0812c56225cb8112
3
+ metadata.gz: c65fb7c7aa4cf11b582ae1620a91b522d88aaee3
4
+ data.tar.gz: fc50ff99e60d3c387394c886d48196722f056c6c
5
5
  SHA512:
6
- metadata.gz: e168396ccc1ab5a002be950f5680b26b31a26186ef0e6003b3bd54bc511591973367128683d2da958627ce3ba65e2dfbb1db71c68f66851d19ff174c9b4f5b56
7
- data.tar.gz: f6511ca661d7395403668d315dfbc96f47b9578d1757528a8c25b47e1e2dfe56b963590fa237643e870788cc5bb8ed1095bfb8eec8972adec535c42b4acf398b
6
+ metadata.gz: dfaa6acd7d9c78967231e194b652db630b271d07d6091a0bc7add63b8c598bf253ca97bd9451216d98203e5a896cd51a8789b999b1ceefd2a73928b7f03aba75
7
+ data.tar.gz: 72e3e53c5616f4948f8a0d0c2bbf0d758fa2df145a16161bf905421143bc49bebd1946c9596fbc4daf14a52b82f20d8edf3416e1018b6320263ea764ed1e6efe
@@ -3,9 +3,11 @@
3
3
  from __future__ import print_function
4
4
 
5
5
  import os
6
- import re
7
- import shutil
6
+ import difflib
7
+ import string
8
+ import random
8
9
  import sys
10
+ from bond_dialog import OptionDialog
9
11
 
10
12
  try:
11
13
  # Import bond safely
@@ -24,6 +26,8 @@ class ReconcileTool:
24
26
  Base class for reconcile tools
25
27
  """
26
28
 
29
+ TMP_FILE_BASE_NAME = '/tmp/bond_tmp_'
30
+
27
31
  @staticmethod
28
32
  def select(reconcile_tool=None):
29
33
 
@@ -36,12 +40,15 @@ class ReconcileTool:
36
40
  if reconcile_tool == 'console':
37
41
  return ReconcileToolConsole()
38
42
 
43
+ if reconcile_tool == 'dialog':
44
+ return ReconcileToolDialog()
45
+
39
46
  if reconcile_tool == 'kdiff3':
40
47
  return ReconcileToolKdiff3()
41
48
 
42
49
  if reconcile_tool is None:
43
50
  # Look at the environment variable BOND_RECONCILE
44
- reconcile_tool = os.environ.get('BOND_RECONCILE', 'abort')
51
+ reconcile_tool = os.environ.get('BOND_RECONCILE', 'console')
45
52
  if reconcile_tool is not None:
46
53
  return ReconcileTool.select(reconcile_tool)
47
54
 
@@ -59,16 +66,69 @@ class ReconcileTool:
59
66
  """
60
67
  return os.system(cmd)
61
68
 
69
+ @staticmethod
70
+ def _get_user_input_console(prompt, options, single_char_options):
71
+ """
72
+ Get input from the user using a console.
73
+ :param prompt: The main prompt string to display to the user.
74
+ :param options: A tuple of options to present to the user for them to select; must have
75
+ at least one. The last option is the default.
76
+ :param single_char_options: A tuple of the single-character versions of each of the
77
+ options supplied; this tuple must be the same length as ``options``,
78
+ and each single-character version must appear within the option itself.
79
+ :return: The user response, which is guaranteed to be one of the supplied ``options`` (if the user
80
+ input an invalid response the default option is used).
81
+ """
82
+ opts_with_single_char = \
83
+ [string.replace(opt, char, '[' + char + ']', 1) for opt, char in zip(options, single_char_options)]
84
+ # Default option, highlight it in bold
85
+ opts_with_single_char[-1] = '\033[1m' + opts_with_single_char[-1] + '\033[0m'
86
+ response = raw_input(prompt + ' (' + ' | '.join(opts_with_single_char) + '): ')
87
+ if len(response) == 0: # No input; return the default
88
+ return options[-1]
89
+ elif len(response) == 1: # Single-character; find matching option
90
+ return next((opt for opt, char in zip(options, single_char_options) if char == response), options[-1])
91
+ else:
92
+ # Make sure response is a valid option; if not return the default
93
+ return next((opt for opt in options if opt == response), options[-1])
94
+
95
+ @staticmethod
96
+ def _get_user_input_dialog(prompt, options):
97
+ """
98
+ Get input from the user using a dialog box.
99
+ :param prompt: The main prompt string to display to the user.
100
+ :param options: A tuple of options to present to the user for them to select; must have
101
+ at least one. The last option is the default.
102
+ :return: The user response, which is guaranteed to be one of the supplied ``options``
103
+ (if the user input an invalid response the default option is used).
104
+ """
105
+ return OptionDialog.create_dialog_get_value(prompt, options)
106
+
62
107
  @staticmethod
63
108
  @spy_point(enabled_for_groups='bond_self_test',
64
109
  require_agent_result=True,
110
+ excluded_keys=('extra_dialog_prompt','single_char_options'),
65
111
  spy_result=True)
66
- def _read_console(prompt):
112
+ def _get_user_input(prompt, options, single_char_options, extra_dialog_prompt=''):
67
113
  """
68
- A function to read from the console
114
+ Acquire input from the user. Defaults to using the console to ask for input. If
115
+ a console is not available, falls back to using a popup dialog window.
116
+ :param prompt: The main prompt string to display to the user.
117
+ :param options: A tuple of options to present to the user for them to select; must have
118
+ at least one. The last option is the default.
119
+ :param single_char_options: A tuple of the single-character versions of each of the
120
+ options supplied; this tuple must be the same length as ``options``,
121
+ and each single-character version must appear within the option itself.
122
+ :param extra_dialog_prompt: An extra message to display before the prompt only if the fallback
123
+ dialog box is used.
124
+ :return: The user response, which is guaranteed to be one of the supplied ``options`` (if the user
125
+ input an invalid response the default option is used).
69
126
  """
70
- return raw_input(prompt)
71
-
127
+ if sys.stdin.isatty(): # We use the console to retrieve input
128
+ return ReconcileTool._get_user_input_console(prompt, options, single_char_options)
129
+ else: # We use a dialog box to retrieve input
130
+ print('System console not found; using a dialog box to retrieve input instead.')
131
+ return ReconcileTool._get_user_input_dialog(extra_dialog_prompt + '\n' + prompt, options)
72
132
 
73
133
  @staticmethod
74
134
  @spy_point(enabled_for_groups='bond_self_test')
@@ -81,23 +141,26 @@ class ReconcileTool:
81
141
  print(what)
82
142
 
83
143
  @staticmethod
84
- def _quick_diff(reference_file, current_file, diff_file):
144
+ @spy_point(enabled_for_groups='bond_self_test', mock_only=True)
145
+ def _random_string():
85
146
  """
86
- Compute a diff between files and save it into a diff_file.
87
- Return True if there are no diffs
147
+ Generate a short random string
88
148
  """
89
- # TODO: implicit dependency on a 'diff' command line tool with the same usage syntax that you're expecting
90
- return (0 == ReconcileTool._invoke_command('diff -u -b "{0}" "{1}" >"{2}"'.format(reference_file,
91
- current_file,
92
- diff_file)))
149
+ return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))
93
150
 
94
151
  @staticmethod
95
- def _aux_file_name(current_file,
96
- flavor):
152
+ def _tmp_file_name(flavor):
97
153
  """
98
- The name of the auxiliary (e.g., diff, merged) file to use
154
+ The name of the temporary file to use with a flavor-specific
155
+ (e.g. diff, curr, ref) ending.
99
156
  """
100
- return current_file + "." + flavor
157
+ # We include a short random string to avoid potential collisions
158
+ return ReconcileTool.TMP_FILE_BASE_NAME + ReconcileTool._random_string() + "." + flavor
159
+
160
+ @staticmethod
161
+ @spy_point(enabled_for_groups='bond_self_test')
162
+ def _compute_diff(reference_lines, current_lines):
163
+ return list(difflib.unified_diff(reference_lines, current_lines, 'reference', 'current'))
101
164
 
102
165
  def __init__(self):
103
166
  pass
@@ -105,66 +168,60 @@ class ReconcileTool:
105
168
  def reconcile(self,
106
169
  test_name,
107
170
  reference_file,
108
- current_file,
171
+ current_lines,
109
172
  no_save=None):
110
173
  """
111
174
  Reconcile the differences
112
175
  @param test_name: the name of the test (for messages)
113
176
  @param reference_file: the name of the reference observation file
114
- @param current_file: the name of the current observation file
177
+ @param current_lines: a list of the lines which make up the current set of observations
115
178
  :param no_save: if present, then disallows saving a new reference file.
116
179
  This parameter should be a string explaining why saving is disallowed.
117
180
  """
118
181
 
119
- if not os.path.isfile(reference_file):
182
+ if os.path.isfile(reference_file):
183
+ with open(reference_file, 'r') as f:
184
+ reference_lines = f.readlines()
185
+ else:
120
186
  # if we do not have the reference file, pretend we have an empty one
121
187
  ReconcileTool._print('WARNING: No reference observation file found for {}: {}'.format(test_name, reference_file))
122
-
123
- with open(reference_file, 'w') as f:
124
- pass
125
- # We continue
188
+ reference_lines = list()
126
189
 
127
190
  # Compute a quick difference
128
- diff_file = self._aux_file_name(current_file, "diff")
129
- try:
130
- if self._quick_diff(reference_file, current_file, diff_file):
131
- # There are no differences
132
- return True
133
-
134
- # There are differences
135
- merged_file = self.invoke_tool(test_name,
136
- reference_file,
137
- current_file,
138
- diff_file,
139
- no_save=no_save)
140
- if merged_file is not None:
141
- # Accepted differences
142
- if no_save:
143
- ReconcileTool._print("Not saving reference observation file for {}: {}".format(test_name,
144
- no_save))
145
- else:
146
- ReconcileTool._print('Saving updated reference observation file for {}'.format(test_name))
147
- shutil.move(merged_file, reference_file)
148
- return True
191
+ unified_diff = self._compute_diff(reference_lines, current_lines)
192
+
193
+ if len(unified_diff) == 0:
194
+ # There are no differences
195
+ return True
196
+
197
+ # There are differences
198
+ merged_lines = self.invoke_tool(test_name,
199
+ reference_lines,
200
+ current_lines,
201
+ unified_diff,
202
+ no_save=no_save)
203
+ if merged_lines is not None:
204
+ # Accepted differences
205
+ if no_save:
206
+ ReconcileTool._print("Not saving reference observation file for {}: {}".format(test_name,
207
+ no_save))
149
208
  else:
150
- return False
151
-
152
- finally:
153
- # Delete the files
154
- if os.path.isfile(diff_file):
155
- os.unlink(diff_file)
156
- if os.path.isfile(current_file):
157
- os.unlink(current_file)
209
+ ReconcileTool._print('Saving updated reference observation file for {}'.format(test_name))
210
+ if os.path.isfile(reference_file):
211
+ os.unlink(reference_file)
212
+ with open(reference_file, 'w') as f:
213
+ f.writelines(merged_lines)
214
+ return True
215
+ else:
216
+ return False
158
217
 
159
218
  def show_diff(self,
160
219
  test_name,
161
- diff_file):
220
+ unified_diff):
162
221
  """
163
- Show the diff, and return it
222
+ Show the lines of the diff, and return it as a string
164
223
  """
165
- diffs = ''
166
- with open(diff_file, 'r') as f:
167
- diffs = f.read()
224
+ diffs = ''.join(unified_diff)
168
225
  if diffs:
169
226
  ReconcileTool._print('There were differences in observations for {}: '.format(test_name))
170
227
  ReconcileTool._print(diffs)
@@ -175,14 +232,14 @@ class ReconcileTool:
175
232
  return diffs
176
233
 
177
234
  def invoke_tool(self, test_name,
178
- reference_file,
179
- current_file,
180
- diff_file,
235
+ reference_lines,
236
+ current_lines,
237
+ unified_diff,
181
238
  no_save=None):
182
239
  """
183
240
  Invoke the actual tool
184
241
  @param
185
- @return either False, for a failed merge, or the name of the file to use as the new reference
242
+ @return either False, for a failed merge, or a list of the new lines to use as the reference
186
243
  """
187
244
  assert False, 'Must override'
188
245
 
@@ -194,12 +251,12 @@ class ReconcileToolAbort(ReconcileTool):
194
251
 
195
252
  def invoke_tool(self,
196
253
  test_name,
197
- reference_file,
198
- current_file,
199
- diff_file,
254
+ reference_lines,
255
+ current_lines,
256
+ unified_diff,
200
257
  no_save=None):
201
258
  if not no_save:
202
- self.show_diff(test_name, diff_file)
259
+ self.show_diff(test_name, unified_diff)
203
260
  ReconcileTool._print('Aborting (reconcile=abort) due to differences for test {}'.format(test_name))
204
261
  return None
205
262
 
@@ -211,60 +268,70 @@ class ReconcileToolAccept(ReconcileTool):
211
268
 
212
269
  def invoke_tool(self,
213
270
  test_name,
214
- reference_file,
215
- current_file,
216
- diff_file,
271
+ reference_lines,
272
+ current_lines,
273
+ unified_diff,
217
274
  no_save=None):
218
- diffs = self.show_diff(test_name, diff_file)
275
+ self.show_diff(test_name, unified_diff)
219
276
  if not no_save:
220
277
  ReconcileTool._print('Accepting (reconcile=accept) differences for test {}'.format(test_name))
221
- return current_file
278
+ return current_lines
222
279
 
223
280
 
224
281
  class ReconcileToolConsole(ReconcileTool):
225
282
  """
226
- Uses diff and console prompts to accept the changes
283
+ Uses diff and console prompts to accept the changes. Falls back to a dialog window
284
+ if no console is available for input.
227
285
  """
228
286
 
229
287
  def invoke_tool(self,
230
288
  test_name,
231
- reference_file,
232
- current_file,
233
- diff_file,
289
+ reference_lines,
290
+ current_lines,
291
+ unified_diff,
234
292
  no_save=None):
235
293
 
294
+ extra_msg = None
236
295
  while True:
237
296
  if no_save:
238
- prompt = 'Observations are shown for {}. Saving them not allowed because test failed.'.format(
239
- test_name,
240
- ) + ' Use the diff option to show the differences. ([k]diff3 | [d]iff | [e] errors | *): '
297
+ prompt = 'Observations are shown for {}. Saving them not allowed because test failed. ' \
298
+ 'Use the diff option to show the differences.'.format(test_name)
299
+ response = self._input(prompt, ('kdiff3', 'diff', 'errors', 'continue'),
300
+ ('k', 'd', 'e', 'c'),
301
+ extra_msg if extra_msg else '\n'.join(current_lines))
241
302
  else:
242
303
  # Show the diff
243
- self.show_diff(test_name, diff_file)
244
- prompt = 'Do you want to accept the changes ({}) ? ( [y]es | [k]diff3 | *): '.format(test_name)
245
-
246
- response = ReconcileTool._read_console(prompt)
304
+ diff = self.show_diff(test_name, unified_diff)
305
+ prompt = 'Do you want to accept the changes ({})?'.format(test_name)
306
+ response = self._input(prompt, ('kdiff3', 'yes', 'no'), ('k', 'y', 'n'), diff)
247
307
 
248
- if response == 'k':
249
- return ReconcileToolKdiff3().invoke_tool(test_name, reference_file, current_file, diff_file,
308
+ if response == 'kdiff3':
309
+ return ReconcileToolKdiff3().invoke_tool(test_name, reference_lines, current_lines, unified_diff,
250
310
  no_save=no_save)
251
-
252
- if response == 'd' and no_save:
253
- self.show_diff(test_name, diff_file)
311
+ elif response == 'diff':
312
+ extra_msg = self.show_diff(test_name, unified_diff)
254
313
  continue
255
-
256
- if response == 'e' and no_save:
257
- ReconcileTool._print("Test {} had errors:\n{}".format(test_name, no_save))
314
+ elif response == 'errors':
315
+ extra_msg = "Test {} had errors:\n{}".format(test_name, no_save)
316
+ ReconcileTool._print('\033[91m' + extra_msg + '\033[0m')
258
317
  continue
259
-
260
- if response == 'y' and not no_save:
318
+ elif response == 'yes' and not no_save:
261
319
  ReconcileTool._print('Accepting differences for test {}'.format(test_name))
262
- return current_file
263
-
264
- if not no_save:
320
+ return current_lines
321
+ elif not no_save:
265
322
  ReconcileTool._print('Rejecting differences for test {}'.format(test_name))
266
323
  return None
267
324
 
325
+ def _input(self, prompt, options, single_char_options, extra_dialog_prompt=None):
326
+ return ReconcileTool._get_user_input(prompt, options, single_char_options, extra_dialog_prompt)
327
+
328
+
329
+ class ReconcileToolDialog(ReconcileToolConsole):
330
+ """
331
+ Merge with a dialog window.
332
+ """
333
+ def _input(self, prompt, options, single_char_options, extra_dialog_prompt=None):
334
+ return ReconcileTool._get_user_input_dialog(extra_dialog_prompt + '\n' + prompt, options)
268
335
 
269
336
 
270
337
  class ReconcileToolKdiff3(ReconcileTool):
@@ -274,57 +341,77 @@ class ReconcileToolKdiff3(ReconcileTool):
274
341
 
275
342
  def invoke_tool(self,
276
343
  test_name,
277
- reference_file,
278
- current_file,
279
- diff_file,
344
+ reference_lines,
345
+ current_lines,
346
+ unified_diff,
280
347
  no_save=None):
281
348
 
282
- if no_save:
283
- response = ReconcileTool._read_console(
284
- "\n!!! MERGING NOT ALLOWED for {}: {}. Want to start kdiff3? ([y] | *): ".format(test_name,
285
- no_save))
286
- if response != "y":
349
+ # Save the current lines out to a temporary file to use with kdiff3
350
+ current_file = self._tmp_file_name('curr')
351
+ reference_file = self._tmp_file_name('ref')
352
+ try:
353
+ with open(current_file, 'w') as f:
354
+ f.writelines(current_lines)
355
+ with open(reference_file, 'w') as f:
356
+ f.writelines(reference_lines)
357
+
358
+ merged_file = None
359
+ if no_save:
360
+ response = ReconcileTool._get_user_input(
361
+ "\n!!! MERGING NOT ALLOWED for {}: {}. Want to start kdiff3?".format(test_name, no_save),
362
+ ('yes', 'no'), ('y', 'n'))
363
+ if response != 'yes':
364
+ return None
365
+
366
+ cmd = ('kdiff3 "{reference_file}" --L1 "{test_name}_REFERENCE" '
367
+ '"{current_file}" --L2 "{test_name}_CURRENT" ').format(
368
+ reference_file=reference_file,
369
+ current_file=current_file,
370
+ test_name=test_name)
371
+ ReconcileTool._invoke_command(cmd)
287
372
  return None
288
373
 
289
- cmd = ('kdiff3 "{reference_file}" --L1 "{test_name}_REFERENCE" '
290
- '"{current_file}" --L2 "{test_name}_CURRENT" ').format(
291
- reference_file=reference_file,
292
- current_file=current_file,
293
- test_name=test_name)
294
- else:
295
- merged_file = self._aux_file_name(current_file, 'merged')
296
-
297
- cmd = ('kdiff3 -m "{reference_file}" --L1 "{test_name}_REFERENCE" '
298
- '"{current_file}" --L2 "{test_name}_CURRENT" '
299
- ' -o "{merged_file}"').format(reference_file=reference_file,
300
- current_file=current_file,
301
- merged_file=merged_file,
302
- test_name=test_name)
303
-
304
- print(cmd)
305
- code = ReconcileTool._invoke_command(cmd)
306
- if no_save:
307
- return None
308
- else:
309
- if code == 0 :
310
- # Merged ok
311
- return merged_file
312
374
  else:
313
- if os.path.isfile(merged_file):
314
- os.unlink(merged_file)
315
- return None
375
+ merged_file = self._tmp_file_name('merged')
376
+ cmd = ('kdiff3 -m "{reference_file}" --L1 "{test_name}_REFERENCE" '
377
+ '"{current_file}" --L2 "{test_name}_CURRENT" '
378
+ ' -o "{merged_file}"').format(reference_file=reference_file,
379
+ current_file=current_file,
380
+ merged_file=merged_file,
381
+ test_name=test_name)
382
+
383
+ code = ReconcileTool._invoke_command(cmd)
384
+ if code == 0:
385
+ # Merged ok
386
+ with open(merged_file, 'r') as f:
387
+ merged_lines = f.readlines()
388
+ message = 'Merge successful; saving a new reference file. '
389
+ ret = merged_lines
390
+ else:
391
+ message = 'Merge unsuccessful; not saving a new reference file. '
392
+ ret = None
393
+
394
+ ReconcileTool._get_user_input(message, ('continue',), ('c',))
395
+ return ret
396
+ finally:
397
+ if os.path.isfile(current_file):
398
+ os.unlink(current_file)
399
+ if os.path.isfile(reference_file):
400
+ os.unlink(reference_file)
401
+ if merged_file is not None and os.path.isfile(merged_file):
402
+ os.unlink(merged_file)
316
403
 
317
404
 
318
405
  def reconcile_observations(settings,
319
406
  test_name,
320
407
  reference_file,
321
- current_file,
408
+ current_lines,
322
409
  no_save=None):
323
410
  """
324
411
  Reconcile the observations
325
412
  :param settings: a settings object
326
413
  :param reference_file: the reference file
327
- :param current_file: the current file with observations
414
+ :param current_lines: a list of all of the lines in the current set of observations
328
415
  :param no_save: If present, then saving of new references is not allowed. This parameter
329
416
  should be a short string explaining why saving is not allowed.
330
417
  :return:
@@ -333,7 +420,7 @@ def reconcile_observations(settings,
333
420
  reconcile_tool = ReconcileTool.select(settings.get('reconcile'))
334
421
  return reconcile_tool.reconcile(test_name,
335
422
  reference_file,
336
- current_file,
423
+ current_lines,
337
424
  no_save=no_save)
338
425
 
339
426
 
@@ -361,6 +448,9 @@ if __name__ == '__main__':
361
448
  print('The current file does not exist: {}'.format(opts.current), file=sys.stderr)
362
449
  sys.exit(1)
363
450
 
451
+ with open(opts.current, 'r') as f:
452
+ current_lines = f.readlines()
453
+ os.unlink(opts.current)
364
454
 
365
455
  # Guess the test name from the current file
366
456
  if opts.test:
@@ -378,7 +468,7 @@ if __name__ == '__main__':
378
468
  if reconcile_observations(main_settings,
379
469
  main_test_name,
380
470
  opts.reference,
381
- opts.current,
471
+ current_lines,
382
472
  no_save=opts.no_save):
383
473
  sys.exit(0)
384
474
  else: