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
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: