1
# Copyright (C) 2009, 2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from ..cleanup import (
23
OperationWithCleanups,
25
from ..sixish import (
28
from . import TestCase
35
class CleanupsTestCase(TestCase):
38
super(CleanupsTestCase, self).setUp()
41
def no_op_cleanup(self):
42
self.call_log.append('no_op_cleanup')
44
def assertLogContains(self, regex):
45
self.assertContainsRe(self.get_log(), regex, re.DOTALL)
47
def failing_cleanup(self):
48
self.call_log.append('failing_cleanup')
49
raise Exception("failing_cleanup goes boom!")
52
class TestRunCleanup(CleanupsTestCase):
54
def test_no_errors(self):
55
"""The function passed to _run_cleanup is run."""
56
self.assertTrue(_run_cleanup(self.no_op_cleanup))
57
self.assertEqual(['no_op_cleanup'], self.call_log)
59
def test_cleanup_with_args_kwargs(self):
60
def func_taking_args_kwargs(*args, **kwargs):
61
self.call_log.append(('func', args, kwargs))
62
_run_cleanup(func_taking_args_kwargs, 'an arg', kwarg='foo')
64
[('func', ('an arg',), {'kwarg': 'foo'})], self.call_log)
66
def test_cleanup_error(self):
67
"""An error from the cleanup function is logged by _run_cleanup, but not
70
This is there's no way for _run_cleanup to know if there's an existing
71
exception in this situation::
75
_run_cleanup(cleanup_func)
76
So, the best _run_cleanup can do is always log errors but never raise
79
self.assertFalse(_run_cleanup(self.failing_cleanup))
80
self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom')
82
def test_cleanup_error_debug_flag(self):
83
"""The -Dcleanup debug flag causes cleanup errors to be reported to the
87
trace.push_log_file(log)
88
debug.debug_flags.add('cleanup')
89
self.assertFalse(_run_cleanup(self.failing_cleanup))
90
self.assertContainsRe(
92
"brz: warning: Cleanup failed:.*failing_cleanup goes boom")
94
def test_prior_error_cleanup_succeeds(self):
95
"""Calling _run_cleanup from a finally block will not interfere with an
96
exception from the try block.
98
def failing_operation():
102
_run_cleanup(self.no_op_cleanup)
103
self.assertRaises(ZeroDivisionError, failing_operation)
104
self.assertEqual(['no_op_cleanup'], self.call_log)
106
def test_prior_error_cleanup_fails(self):
107
"""Calling _run_cleanup from a finally block will not interfere with an
108
exception from the try block even when the cleanup itself raises an
111
The cleanup exception will be logged.
113
def failing_operation():
117
_run_cleanup(self.failing_cleanup)
118
self.assertRaises(ZeroDivisionError, failing_operation)
119
self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom')
122
class TestDoWithCleanups(CleanupsTestCase):
124
def trivial_func(self):
125
self.call_log.append('trivial_func')
126
return 'trivial result'
128
def test_runs_func(self):
129
"""_do_with_cleanups runs the function it is given, and returns the
132
result = _do_with_cleanups([], self.trivial_func)
133
self.assertEqual('trivial result', result)
135
def test_runs_cleanups(self):
136
"""Cleanup functions are run (in the given order)."""
137
cleanup_func_1 = (self.call_log.append, ('cleanup 1',), {})
138
cleanup_func_2 = (self.call_log.append, ('cleanup 2',), {})
139
_do_with_cleanups([cleanup_func_1, cleanup_func_2], self.trivial_func)
141
['trivial_func', 'cleanup 1', 'cleanup 2'], self.call_log)
143
def failing_func(self):
144
self.call_log.append('failing_func')
147
def test_func_error_propagates(self):
148
"""Errors from the main function are propagated (after running
152
ZeroDivisionError, _do_with_cleanups,
153
[(self.no_op_cleanup, (), {})], self.failing_func)
154
self.assertEqual(['failing_func', 'no_op_cleanup'], self.call_log)
156
def test_func_error_trumps_cleanup_error(self):
157
"""Errors from the main function a propagated even if a cleanup raises
160
The cleanup error is be logged.
163
ZeroDivisionError, _do_with_cleanups,
164
[(self.failing_cleanup, (), {})], self.failing_func)
165
self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom')
167
def test_func_passes_and_error_from_cleanup(self):
168
"""An error from a cleanup is propagated when the main function doesn't
169
raise an error. Later cleanups are still executed.
171
exc = self.assertRaises(
172
Exception, _do_with_cleanups,
173
[(self.failing_cleanup, (), {}), (self.no_op_cleanup, (), {})],
175
self.assertEqual('failing_cleanup goes boom!', exc.args[0])
177
['trivial_func', 'failing_cleanup', 'no_op_cleanup'],
180
def test_multiple_cleanup_failures(self):
181
"""When multiple cleanups fail (as tends to happen when something has
182
gone wrong), the first error is propagated, and subsequent errors are
185
cleanups = self.make_two_failing_cleanup_funcs()
186
self.assertRaises(ErrorA, _do_with_cleanups, cleanups,
188
self.assertLogContains('Cleanup failed:.*ErrorB')
189
self.assertFalse('ErrorA' in self.get_log())
191
def make_two_failing_cleanup_funcs(self):
193
raise ErrorA('Error A')
195
raise ErrorB('Error B')
196
return [(raise_a, (), {}), (raise_b, (), {})]
198
def test_multiple_cleanup_failures_debug_flag(self):
200
trace.push_log_file(log)
201
debug.debug_flags.add('cleanup')
202
cleanups = self.make_two_failing_cleanup_funcs()
203
self.assertRaises(ErrorA, _do_with_cleanups, cleanups,
205
self.assertContainsRe(
206
log.getvalue(), "brz: warning: Cleanup failed:.*Error B\n")
207
self.assertEqual(1, log.getvalue().count('brz: warning:'),
210
def test_func_and_cleanup_errors_debug_flag(self):
212
trace.push_log_file(log)
213
debug.debug_flags.add('cleanup')
214
cleanups = self.make_two_failing_cleanup_funcs()
215
self.assertRaises(ZeroDivisionError, _do_with_cleanups, cleanups,
217
self.assertContainsRe(
218
log.getvalue(), "brz: warning: Cleanup failed:.*Error A\n")
219
self.assertContainsRe(
220
log.getvalue(), "brz: warning: Cleanup failed:.*Error B\n")
221
self.assertEqual(2, log.getvalue().count('brz: warning:'))
223
def test_func_may_mutate_cleanups(self):
224
"""The main func may mutate the cleanups before it returns.
226
This allows a function to gradually add cleanups as it acquires
227
resources, rather than planning all the cleanups up-front. The
228
OperationWithCleanups helper relies on this working.
231
def func_that_adds_cleanups():
232
self.call_log.append('func_that_adds_cleanups')
233
cleanups_list.append((self.no_op_cleanup, (), {}))
235
result = _do_with_cleanups(cleanups_list, func_that_adds_cleanups)
236
self.assertEqual('result', result)
238
['func_that_adds_cleanups', 'no_op_cleanup'], self.call_log)
240
def test_cleanup_error_debug_flag(self):
241
"""The -Dcleanup debug flag causes cleanup errors to be reported to the
245
trace.push_log_file(log)
246
debug.debug_flags.add('cleanup')
247
self.assertRaises(ZeroDivisionError, _do_with_cleanups,
248
[(self.failing_cleanup, (), {})], self.failing_func)
249
self.assertContainsRe(
251
"brz: warning: Cleanup failed:.*failing_cleanup goes boom")
252
self.assertEqual(1, log.getvalue().count('brz: warning:'))
255
class ErrorA(Exception): pass
256
class ErrorB(Exception): pass
259
class TestOperationWithCleanups(CleanupsTestCase):
261
def test_cleanup_ordering(self):
262
"""Cleanups are added in LIFO order.
264
So cleanups added before run is called are run last, and the last
265
cleanup added during the func is run first.
269
call_log.append(('func called', foo))
270
op.add_cleanup(call_log.append, 'cleanup 2')
271
op.add_cleanup(call_log.append, 'cleanup 1')
273
owc = OperationWithCleanups(func)
274
owc.add_cleanup(call_log.append, 'cleanup 4')
275
owc.add_cleanup(call_log.append, 'cleanup 3')
276
result = owc.run('foo')
277
self.assertEqual('result', result)
279
[('func called', 'foo'), 'cleanup 1', 'cleanup 2', 'cleanup 3',
280
'cleanup 4'], call_log)
283
class SampleWithCleanups(ObjectWithCleanups):
288
class TestObjectWithCleanups(TestCase):
290
def test_object_with_cleanups(self):
292
s = SampleWithCleanups()
293
s.add_cleanup(a.append, 42)
295
self.assertEqual(a, [42])