Merge lp:~lifeless/testtools/bug-335816-one-outcome-per-test into lp:~testtools-committers/testtools/trunk

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: 63
Merged at revision: 63
Proposed branch: lp:~lifeless/testtools/bug-335816-one-outcome-per-test
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 267 lines (+74/-32)
5 files modified
NEWS (+5/-0)
testtools/runtest.py (+20/-5)
testtools/testcase.py (+19/-12)
testtools/tests/test_runtest.py (+4/-8)
testtools/tests/test_testtools.py (+26/-7)
To merge this branch: bzr merge lp:~lifeless/testtools/bug-335816-one-outcome-per-test
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+25226@code.launchpad.net

Description of the change

Fix the bug where multiple exceptions in a single test corrupt test result objects output.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

jml says ok,

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-01-19 01:40:58 +0000
+++ NEWS 2010-05-13 12:20:50 +0000
@@ -34,6 +34,11 @@
34* The backtrace test result output tests should now pass on windows and other34* The backtrace test result output tests should now pass on windows and other
35 systems where os.sep is not '/'.35 systems where os.sep is not '/'.
3636
37* When a cleanUp or tearDown exception occurs, it is now accumulated as a new
38 traceback in the test details, rather than as a separate call to addError /
39 addException. This makes testtools work better with most TestResult objects
40 and fixes bug #335816.
41
3742
380.9.2430.9.2
39~~~~~44~~~~~
4045
=== modified file 'testtools/runtest.py'
--- testtools/runtest.py 2009-12-31 03:15:19 +0000
+++ testtools/runtest.py 2010-05-13 12:20:50 +0000
@@ -36,6 +36,8 @@
36 classes in the list).36 classes in the list).
37 :ivar exception_caught: An object returned when _run_user catches an37 :ivar exception_caught: An object returned when _run_user catches an
38 exception.38 exception.
39 :ivar _exceptions: A list of caught exceptions, used to do the single
40 reporting of error/failure/skip etc.
39 """41 """
4042
41 def __init__(self, case, handlers=None):43 def __init__(self, case, handlers=None):
@@ -48,6 +50,7 @@
48 self.case = case50 self.case = case
49 self.handlers = handlers or []51 self.handlers = handlers or []
50 self.exception_caught = object()52 self.exception_caught = object()
53 self._exceptions = []
5154
52 def run(self, result=None):55 def run(self, result=None):
53 """Run self.case reporting activity to result.56 """Run self.case reporting activity to result.
@@ -86,7 +89,16 @@
86 result.startTest(self.case)89 result.startTest(self.case)
87 self.result = result90 self.result = result
88 try:91 try:
92 self._exceptions = []
89 self._run_core()93 self._run_core()
94 if self._exceptions:
95 # One or more caught exceptions, now trigger the test's
96 # reporting method for just one.
97 e = self._exceptions.pop()
98 for exc_class, handler in self.handlers:
99 if isinstance(e, exc_class):
100 handler(self.case, self.result, e)
101 break
90 finally:102 finally:
91 result.stopTest(self.case)103 result.stopTest(self.case)
92 return result104 return result
@@ -96,7 +108,9 @@
96 if self.exception_caught == self._run_user(self.case._run_setup,108 if self.exception_caught == self._run_user(self.case._run_setup,
97 self.result):109 self.result):
98 # Don't run the test method if we failed getting here.110 # Don't run the test method if we failed getting here.
99 self.case._runCleanups(self.result)111 e = self.case._runCleanups(self.result)
112 if e is not None:
113 self._exceptions.append(e)
100 return114 return
101 # Run everything from here on in. If any of the methods raise an115 # Run everything from here on in. If any of the methods raise an
102 # exception we'll have failed.116 # exception we'll have failed.
@@ -112,8 +126,9 @@
112 failed = True126 failed = True
113 finally:127 finally:
114 try:128 try:
115 if not self._run_user(129 e = self._run_user(self.case._runCleanups, self.result)
116 self.case._runCleanups, self.result):130 if e is not None:
131 self._exceptions.append(e)
117 failed = True132 failed = True
118 finally:133 finally:
119 if not failed:134 if not failed:
@@ -134,9 +149,9 @@
134 # escape: but they are deprecated anyway.149 # escape: but they are deprecated anyway.
135 exc_info = sys.exc_info()150 exc_info = sys.exc_info()
136 e = exc_info[1]151 e = exc_info[1]
152 self.case.onException(exc_info)
137 for exc_class, handler in self.handlers:153 for exc_class, handler in self.handlers:
138 self.case.onException(exc_info)
139 if isinstance(e, exc_class):154 if isinstance(e, exc_class):
140 handler(self.case, self.result, e)155 self._exceptions.append(e)
141 return self.exception_caught156 return self.exception_caught
142 raise e157 raise e
143158
=== modified file 'testtools/testcase.py'
--- testtools/testcase.py 2010-01-16 04:36:08 +0000
+++ testtools/testcase.py 2010-05-13 12:20:50 +0000
@@ -76,6 +76,7 @@
76 unittest.TestCase.__init__(self, *args, **kwargs)76 unittest.TestCase.__init__(self, *args, **kwargs)
77 self._cleanups = []77 self._cleanups = []
78 self._unique_id_gen = itertools.count(1)78 self._unique_id_gen = itertools.count(1)
79 self._traceback_id_gen = itertools.count(0)
79 self.__setup_called = False80 self.__setup_called = False
80 self.__teardown_called = False81 self.__teardown_called = False
81 self.__details = {}82 self.__details = {}
@@ -145,9 +146,10 @@
145146
146 See the docstring for addCleanup for more information.147 See the docstring for addCleanup for more information.
147148
148 Returns True if all cleanups ran without error, False otherwise.149 :return: None if all cleanups ran without error, the most recently
150 raised exception from the cleanups otherwise.
149 """151 """
150 ok = True152 last_exception = None
151 while self._cleanups:153 while self._cleanups:
152 function, arguments, keywordArguments = self._cleanups.pop()154 function, arguments, keywordArguments = self._cleanups.pop()
153 try:155 try:
@@ -155,9 +157,10 @@
155 except KeyboardInterrupt:157 except KeyboardInterrupt:
156 raise158 raise
157 except:159 except:
158 self._report_error(self, result, None)160 exc_info = sys.exc_info()
159 ok = False161 self._report_traceback(exc_info)
160 return ok162 last_exception = exc_info[1]
163 return last_exception
161164
162 def addCleanup(self, function, *arguments, **keywordArguments):165 def addCleanup(self, function, *arguments, **keywordArguments):
163 """Add a cleanup function to be called after tearDown.166 """Add a cleanup function to be called after tearDown.
@@ -288,8 +291,7 @@
288 predicate(*args, **kwargs)291 predicate(*args, **kwargs)
289 except self.failureException:292 except self.failureException:
290 exc_info = sys.exc_info()293 exc_info = sys.exc_info()
291 self.addDetail('traceback',294 self._report_traceback(exc_info)
292 content.TracebackContent(exc_info, self))
293 raise _ExpectedFailure(exc_info)295 raise _ExpectedFailure(exc_info)
294 else:296 else:
295 raise _UnexpectedSuccess(reason)297 raise _UnexpectedSuccess(reason)
@@ -323,12 +325,14 @@
323325
324 :seealso addOnException:326 :seealso addOnException:
325 """327 """
328 if exc_info[0] not in [
329 TestSkipped, _UnexpectedSuccess, _ExpectedFailure]:
330 self._report_traceback(exc_info)
326 for handler in self.__exception_handlers:331 for handler in self.__exception_handlers:
327 handler(exc_info)332 handler(exc_info)
328333
329 @staticmethod334 @staticmethod
330 def _report_error(self, result, err):335 def _report_error(self, result, err):
331 self._report_traceback()
332 result.addError(self, details=self.getDetails())336 result.addError(self, details=self.getDetails())
333337
334 @staticmethod338 @staticmethod
@@ -337,7 +341,6 @@
337341
338 @staticmethod342 @staticmethod
339 def _report_failure(self, result, err):343 def _report_failure(self, result, err):
340 self._report_traceback()
341 result.addFailure(self, details=self.getDetails())344 result.addFailure(self, details=self.getDetails())
342345
343 @staticmethod346 @staticmethod
@@ -349,9 +352,13 @@
349 self._add_reason(reason)352 self._add_reason(reason)
350 result.addSkip(self, details=self.getDetails())353 result.addSkip(self, details=self.getDetails())
351354
352 def _report_traceback(self):355 def _report_traceback(self, exc_info):
353 self.addDetail('traceback',356 tb_id = advance_iterator(self._traceback_id_gen)
354 content.TracebackContent(sys.exc_info(), self))357 if tb_id:
358 tb_label = 'traceback-%d' % tb_id
359 else:
360 tb_label = 'traceback'
361 self.addDetail(tb_label, content.TracebackContent(exc_info, self))
355362
356 @staticmethod363 @staticmethod
357 def _report_unexpected_success(self, result, err):364 def _report_unexpected_success(self, result, err):
358365
=== modified file 'testtools/tests/test_runtest.py'
--- testtools/tests/test_runtest.py 2009-12-31 03:15:19 +0000
+++ testtools/tests/test_runtest.py 2010-05-13 12:20:50 +0000
@@ -77,14 +77,12 @@
77 e = KeyError('Yo')77 e = KeyError('Yo')
78 def raises():78 def raises():
79 raise e79 raise e
80 def log_exc(self, result, err):80 run = RunTest(case, [(KeyError, None)])
81 log.append((result, err))
82 run = RunTest(case, [(KeyError, log_exc)])
83 run.result = ExtendedTestResult()81 run.result = ExtendedTestResult()
84 status = run._run_user(raises)82 status = run._run_user(raises)
85 self.assertEqual(run.exception_caught, status)83 self.assertEqual(run.exception_caught, status)
86 self.assertEqual([], run.result._events)84 self.assertEqual([], run.result._events)
87 self.assertEqual(["got it", (run.result, e)], log)85 self.assertEqual(["got it"], log)
8886
89 def test__run_user_can_catch_Exception(self):87 def test__run_user_can_catch_Exception(self):
90 case = self.make_case()88 case = self.make_case()
@@ -92,14 +90,12 @@
92 def raises():90 def raises():
93 raise e91 raise e
94 log = []92 log = []
95 def log_exc(self, result, err):93 run = RunTest(case, [(Exception, None)])
96 log.append((result, err))
97 run = RunTest(case, [(Exception, log_exc)])
98 run.result = ExtendedTestResult()94 run.result = ExtendedTestResult()
99 status = run._run_user(raises)95 status = run._run_user(raises)
100 self.assertEqual(run.exception_caught, status)96 self.assertEqual(run.exception_caught, status)
101 self.assertEqual([], run.result._events)97 self.assertEqual([], run.result._events)
102 self.assertEqual([(run.result, e)], log)98 self.assertEqual([], log)
10399
104 def test__run_user_uncaught_Exception_raised(self):100 def test__run_user_uncaught_Exception_raised(self):
105 case = self.make_case()101 case = self.make_case()
106102
=== modified file 'testtools/tests/test_testtools.py'
--- testtools/tests/test_testtools.py 2010-01-16 04:20:26 +0000
+++ testtools/tests/test_testtools.py 2010-05-13 12:20:50 +0000
@@ -301,6 +301,9 @@
301 def runTest(self):301 def runTest(self):
302 self._calls.append('runTest')302 self._calls.append('runTest')
303303
304 def brokenTest(self):
305 raise RuntimeError('Deliberate broken test')
306
304 def tearDown(self):307 def tearDown(self):
305 self._calls.append('tearDown')308 self._calls.append('tearDown')
306 TestCase.tearDown(self)309 TestCase.tearDown(self)
@@ -400,13 +403,29 @@
400 self.assertRaises(403 self.assertRaises(
401 KeyboardInterrupt, self.test.run, self.logging_result)404 KeyboardInterrupt, self.test.run, self.logging_result)
402405
403 def test_multipleErrorsReported(self):406 def test_multipleCleanupErrorsReported(self):
404 # Errors from all failing cleanups are reported.407 # Errors from all failing cleanups are reported as separate backtraces.
405 self.test.addCleanup(lambda: 1/0)408 self.test.addCleanup(lambda: 1/0)
406 self.test.addCleanup(lambda: 1/0)409 self.test.addCleanup(lambda: 1/0)
407 self.test.run(self.logging_result)410 self.logging_result = ExtendedTestResult()
408 self.assertErrorLogEqual(411 self.test.run(self.logging_result)
409 ['startTest', 'addError', 'addError', 'stopTest'])412 self.assertEqual(['startTest', 'addError', 'stopTest'],
413 [event[0] for event in self.logging_result._events])
414 self.assertEqual(set(['traceback', 'traceback-1']),
415 set(self.logging_result._events[1][2].keys()))
416
417 def test_multipleErrorsCoreAndCleanupReported(self):
418 # Errors from all failing cleanups are reported, with stopTest,
419 # startTest inserted.
420 self.test = TestAddCleanup.LoggingTest('brokenTest')
421 self.test.addCleanup(lambda: 1/0)
422 self.test.addCleanup(lambda: 1/0)
423 self.logging_result = ExtendedTestResult()
424 self.test.run(self.logging_result)
425 self.assertEqual(['startTest', 'addError', 'stopTest'],
426 [event[0] for event in self.logging_result._events])
427 self.assertEqual(set(['traceback', 'traceback-1', 'traceback-2']),
428 set(self.logging_result._events[1][2].keys()))
410429
411430
412class TestWithDetails(TestCase):431class TestWithDetails(TestCase):

Subscribers

People subscribed via source and target branches