Merge lp:~lifeless/testtools/bug-335816-one-outcome-per-test into lp:~testtools-committers/testtools/trunk
- bug-335816-one-outcome-per-test
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
testtools developers | Pending | ||
Review via email: mp+25226@code.launchpad.net |
Commit message
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 : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2010-01-19 01:40:58 +0000 | |||
3 | +++ NEWS 2010-05-13 12:20:50 +0000 | |||
4 | @@ -34,6 +34,11 @@ | |||
5 | 34 | * The backtrace test result output tests should now pass on windows and other | 34 | * The backtrace test result output tests should now pass on windows and other |
6 | 35 | systems where os.sep is not '/'. | 35 | systems where os.sep is not '/'. |
7 | 36 | 36 | ||
8 | 37 | * When a cleanUp or tearDown exception occurs, it is now accumulated as a new | ||
9 | 38 | traceback in the test details, rather than as a separate call to addError / | ||
10 | 39 | addException. This makes testtools work better with most TestResult objects | ||
11 | 40 | and fixes bug #335816. | ||
12 | 41 | |||
13 | 37 | 42 | ||
14 | 38 | 0.9.2 | 43 | 0.9.2 |
15 | 39 | ~~~~~ | 44 | ~~~~~ |
16 | 40 | 45 | ||
17 | === modified file 'testtools/runtest.py' | |||
18 | --- testtools/runtest.py 2009-12-31 03:15:19 +0000 | |||
19 | +++ testtools/runtest.py 2010-05-13 12:20:50 +0000 | |||
20 | @@ -36,6 +36,8 @@ | |||
21 | 36 | classes in the list). | 36 | classes in the list). |
22 | 37 | :ivar exception_caught: An object returned when _run_user catches an | 37 | :ivar exception_caught: An object returned when _run_user catches an |
23 | 38 | exception. | 38 | exception. |
24 | 39 | :ivar _exceptions: A list of caught exceptions, used to do the single | ||
25 | 40 | reporting of error/failure/skip etc. | ||
26 | 39 | """ | 41 | """ |
27 | 40 | 42 | ||
28 | 41 | def __init__(self, case, handlers=None): | 43 | def __init__(self, case, handlers=None): |
29 | @@ -48,6 +50,7 @@ | |||
30 | 48 | self.case = case | 50 | self.case = case |
31 | 49 | self.handlers = handlers or [] | 51 | self.handlers = handlers or [] |
32 | 50 | self.exception_caught = object() | 52 | self.exception_caught = object() |
33 | 53 | self._exceptions = [] | ||
34 | 51 | 54 | ||
35 | 52 | def run(self, result=None): | 55 | def run(self, result=None): |
36 | 53 | """Run self.case reporting activity to result. | 56 | """Run self.case reporting activity to result. |
37 | @@ -86,7 +89,16 @@ | |||
38 | 86 | result.startTest(self.case) | 89 | result.startTest(self.case) |
39 | 87 | self.result = result | 90 | self.result = result |
40 | 88 | try: | 91 | try: |
41 | 92 | self._exceptions = [] | ||
42 | 89 | self._run_core() | 93 | self._run_core() |
43 | 94 | if self._exceptions: | ||
44 | 95 | # One or more caught exceptions, now trigger the test's | ||
45 | 96 | # reporting method for just one. | ||
46 | 97 | e = self._exceptions.pop() | ||
47 | 98 | for exc_class, handler in self.handlers: | ||
48 | 99 | if isinstance(e, exc_class): | ||
49 | 100 | handler(self.case, self.result, e) | ||
50 | 101 | break | ||
51 | 90 | finally: | 102 | finally: |
52 | 91 | result.stopTest(self.case) | 103 | result.stopTest(self.case) |
53 | 92 | return result | 104 | return result |
54 | @@ -96,7 +108,9 @@ | |||
55 | 96 | if self.exception_caught == self._run_user(self.case._run_setup, | 108 | if self.exception_caught == self._run_user(self.case._run_setup, |
56 | 97 | self.result): | 109 | self.result): |
57 | 98 | # Don't run the test method if we failed getting here. | 110 | # Don't run the test method if we failed getting here. |
59 | 99 | self.case._runCleanups(self.result) | 111 | e = self.case._runCleanups(self.result) |
60 | 112 | if e is not None: | ||
61 | 113 | self._exceptions.append(e) | ||
62 | 100 | return | 114 | return |
63 | 101 | # Run everything from here on in. If any of the methods raise an | 115 | # Run everything from here on in. If any of the methods raise an |
64 | 102 | # exception we'll have failed. | 116 | # exception we'll have failed. |
65 | @@ -112,8 +126,9 @@ | |||
66 | 112 | failed = True | 126 | failed = True |
67 | 113 | finally: | 127 | finally: |
68 | 114 | try: | 128 | try: |
71 | 115 | if not self._run_user( | 129 | e = self._run_user(self.case._runCleanups, self.result) |
72 | 116 | self.case._runCleanups, self.result): | 130 | if e is not None: |
73 | 131 | self._exceptions.append(e) | ||
74 | 117 | failed = True | 132 | failed = True |
75 | 118 | finally: | 133 | finally: |
76 | 119 | if not failed: | 134 | if not failed: |
77 | @@ -134,9 +149,9 @@ | |||
78 | 134 | # escape: but they are deprecated anyway. | 149 | # escape: but they are deprecated anyway. |
79 | 135 | exc_info = sys.exc_info() | 150 | exc_info = sys.exc_info() |
80 | 136 | e = exc_info[1] | 151 | e = exc_info[1] |
81 | 152 | self.case.onException(exc_info) | ||
82 | 137 | for exc_class, handler in self.handlers: | 153 | for exc_class, handler in self.handlers: |
83 | 138 | self.case.onException(exc_info) | ||
84 | 139 | if isinstance(e, exc_class): | 154 | if isinstance(e, exc_class): |
86 | 140 | handler(self.case, self.result, e) | 155 | self._exceptions.append(e) |
87 | 141 | return self.exception_caught | 156 | return self.exception_caught |
88 | 142 | raise e | 157 | raise e |
89 | 143 | 158 | ||
90 | === modified file 'testtools/testcase.py' | |||
91 | --- testtools/testcase.py 2010-01-16 04:36:08 +0000 | |||
92 | +++ testtools/testcase.py 2010-05-13 12:20:50 +0000 | |||
93 | @@ -76,6 +76,7 @@ | |||
94 | 76 | unittest.TestCase.__init__(self, *args, **kwargs) | 76 | unittest.TestCase.__init__(self, *args, **kwargs) |
95 | 77 | self._cleanups = [] | 77 | self._cleanups = [] |
96 | 78 | self._unique_id_gen = itertools.count(1) | 78 | self._unique_id_gen = itertools.count(1) |
97 | 79 | self._traceback_id_gen = itertools.count(0) | ||
98 | 79 | self.__setup_called = False | 80 | self.__setup_called = False |
99 | 80 | self.__teardown_called = False | 81 | self.__teardown_called = False |
100 | 81 | self.__details = {} | 82 | self.__details = {} |
101 | @@ -145,9 +146,10 @@ | |||
102 | 145 | 146 | ||
103 | 146 | See the docstring for addCleanup for more information. | 147 | See the docstring for addCleanup for more information. |
104 | 147 | 148 | ||
106 | 148 | Returns True if all cleanups ran without error, False otherwise. | 149 | :return: None if all cleanups ran without error, the most recently |
107 | 150 | raised exception from the cleanups otherwise. | ||
108 | 149 | """ | 151 | """ |
110 | 150 | ok = True | 152 | last_exception = None |
111 | 151 | while self._cleanups: | 153 | while self._cleanups: |
112 | 152 | function, arguments, keywordArguments = self._cleanups.pop() | 154 | function, arguments, keywordArguments = self._cleanups.pop() |
113 | 153 | try: | 155 | try: |
114 | @@ -155,9 +157,10 @@ | |||
115 | 155 | except KeyboardInterrupt: | 157 | except KeyboardInterrupt: |
116 | 156 | raise | 158 | raise |
117 | 157 | except: | 159 | except: |
121 | 158 | self._report_error(self, result, None) | 160 | exc_info = sys.exc_info() |
122 | 159 | ok = False | 161 | self._report_traceback(exc_info) |
123 | 160 | return ok | 162 | last_exception = exc_info[1] |
124 | 163 | return last_exception | ||
125 | 161 | 164 | ||
126 | 162 | def addCleanup(self, function, *arguments, **keywordArguments): | 165 | def addCleanup(self, function, *arguments, **keywordArguments): |
127 | 163 | """Add a cleanup function to be called after tearDown. | 166 | """Add a cleanup function to be called after tearDown. |
128 | @@ -288,8 +291,7 @@ | |||
129 | 288 | predicate(*args, **kwargs) | 291 | predicate(*args, **kwargs) |
130 | 289 | except self.failureException: | 292 | except self.failureException: |
131 | 290 | exc_info = sys.exc_info() | 293 | exc_info = sys.exc_info() |
134 | 291 | self.addDetail('traceback', | 294 | self._report_traceback(exc_info) |
133 | 292 | content.TracebackContent(exc_info, self)) | ||
135 | 293 | raise _ExpectedFailure(exc_info) | 295 | raise _ExpectedFailure(exc_info) |
136 | 294 | else: | 296 | else: |
137 | 295 | raise _UnexpectedSuccess(reason) | 297 | raise _UnexpectedSuccess(reason) |
138 | @@ -323,12 +325,14 @@ | |||
139 | 323 | 325 | ||
140 | 324 | :seealso addOnException: | 326 | :seealso addOnException: |
141 | 325 | """ | 327 | """ |
142 | 328 | if exc_info[0] not in [ | ||
143 | 329 | TestSkipped, _UnexpectedSuccess, _ExpectedFailure]: | ||
144 | 330 | self._report_traceback(exc_info) | ||
145 | 326 | for handler in self.__exception_handlers: | 331 | for handler in self.__exception_handlers: |
146 | 327 | handler(exc_info) | 332 | handler(exc_info) |
147 | 328 | 333 | ||
148 | 329 | @staticmethod | 334 | @staticmethod |
149 | 330 | def _report_error(self, result, err): | 335 | def _report_error(self, result, err): |
150 | 331 | self._report_traceback() | ||
151 | 332 | result.addError(self, details=self.getDetails()) | 336 | result.addError(self, details=self.getDetails()) |
152 | 333 | 337 | ||
153 | 334 | @staticmethod | 338 | @staticmethod |
154 | @@ -337,7 +341,6 @@ | |||
155 | 337 | 341 | ||
156 | 338 | @staticmethod | 342 | @staticmethod |
157 | 339 | def _report_failure(self, result, err): | 343 | def _report_failure(self, result, err): |
158 | 340 | self._report_traceback() | ||
159 | 341 | result.addFailure(self, details=self.getDetails()) | 344 | result.addFailure(self, details=self.getDetails()) |
160 | 342 | 345 | ||
161 | 343 | @staticmethod | 346 | @staticmethod |
162 | @@ -349,9 +352,13 @@ | |||
163 | 349 | self._add_reason(reason) | 352 | self._add_reason(reason) |
164 | 350 | result.addSkip(self, details=self.getDetails()) | 353 | result.addSkip(self, details=self.getDetails()) |
165 | 351 | 354 | ||
169 | 352 | def _report_traceback(self): | 355 | def _report_traceback(self, exc_info): |
170 | 353 | self.addDetail('traceback', | 356 | tb_id = advance_iterator(self._traceback_id_gen) |
171 | 354 | content.TracebackContent(sys.exc_info(), self)) | 357 | if tb_id: |
172 | 358 | tb_label = 'traceback-%d' % tb_id | ||
173 | 359 | else: | ||
174 | 360 | tb_label = 'traceback' | ||
175 | 361 | self.addDetail(tb_label, content.TracebackContent(exc_info, self)) | ||
176 | 355 | 362 | ||
177 | 356 | @staticmethod | 363 | @staticmethod |
178 | 357 | def _report_unexpected_success(self, result, err): | 364 | def _report_unexpected_success(self, result, err): |
179 | 358 | 365 | ||
180 | === modified file 'testtools/tests/test_runtest.py' | |||
181 | --- testtools/tests/test_runtest.py 2009-12-31 03:15:19 +0000 | |||
182 | +++ testtools/tests/test_runtest.py 2010-05-13 12:20:50 +0000 | |||
183 | @@ -77,14 +77,12 @@ | |||
184 | 77 | e = KeyError('Yo') | 77 | e = KeyError('Yo') |
185 | 78 | def raises(): | 78 | def raises(): |
186 | 79 | raise e | 79 | raise e |
190 | 80 | def log_exc(self, result, err): | 80 | run = RunTest(case, [(KeyError, None)]) |
188 | 81 | log.append((result, err)) | ||
189 | 82 | run = RunTest(case, [(KeyError, log_exc)]) | ||
191 | 83 | run.result = ExtendedTestResult() | 81 | run.result = ExtendedTestResult() |
192 | 84 | status = run._run_user(raises) | 82 | status = run._run_user(raises) |
193 | 85 | self.assertEqual(run.exception_caught, status) | 83 | self.assertEqual(run.exception_caught, status) |
194 | 86 | self.assertEqual([], run.result._events) | 84 | self.assertEqual([], run.result._events) |
196 | 87 | self.assertEqual(["got it", (run.result, e)], log) | 85 | self.assertEqual(["got it"], log) |
197 | 88 | 86 | ||
198 | 89 | def test__run_user_can_catch_Exception(self): | 87 | def test__run_user_can_catch_Exception(self): |
199 | 90 | case = self.make_case() | 88 | case = self.make_case() |
200 | @@ -92,14 +90,12 @@ | |||
201 | 92 | def raises(): | 90 | def raises(): |
202 | 93 | raise e | 91 | raise e |
203 | 94 | log = [] | 92 | log = [] |
207 | 95 | def log_exc(self, result, err): | 93 | run = RunTest(case, [(Exception, None)]) |
205 | 96 | log.append((result, err)) | ||
206 | 97 | run = RunTest(case, [(Exception, log_exc)]) | ||
208 | 98 | run.result = ExtendedTestResult() | 94 | run.result = ExtendedTestResult() |
209 | 99 | status = run._run_user(raises) | 95 | status = run._run_user(raises) |
210 | 100 | self.assertEqual(run.exception_caught, status) | 96 | self.assertEqual(run.exception_caught, status) |
211 | 101 | self.assertEqual([], run.result._events) | 97 | self.assertEqual([], run.result._events) |
213 | 102 | self.assertEqual([(run.result, e)], log) | 98 | self.assertEqual([], log) |
214 | 103 | 99 | ||
215 | 104 | def test__run_user_uncaught_Exception_raised(self): | 100 | def test__run_user_uncaught_Exception_raised(self): |
216 | 105 | case = self.make_case() | 101 | case = self.make_case() |
217 | 106 | 102 | ||
218 | === modified file 'testtools/tests/test_testtools.py' | |||
219 | --- testtools/tests/test_testtools.py 2010-01-16 04:20:26 +0000 | |||
220 | +++ testtools/tests/test_testtools.py 2010-05-13 12:20:50 +0000 | |||
221 | @@ -301,6 +301,9 @@ | |||
222 | 301 | def runTest(self): | 301 | def runTest(self): |
223 | 302 | self._calls.append('runTest') | 302 | self._calls.append('runTest') |
224 | 303 | 303 | ||
225 | 304 | def brokenTest(self): | ||
226 | 305 | raise RuntimeError('Deliberate broken test') | ||
227 | 306 | |||
228 | 304 | def tearDown(self): | 307 | def tearDown(self): |
229 | 305 | self._calls.append('tearDown') | 308 | self._calls.append('tearDown') |
230 | 306 | TestCase.tearDown(self) | 309 | TestCase.tearDown(self) |
231 | @@ -400,13 +403,29 @@ | |||
232 | 400 | self.assertRaises( | 403 | self.assertRaises( |
233 | 401 | KeyboardInterrupt, self.test.run, self.logging_result) | 404 | KeyboardInterrupt, self.test.run, self.logging_result) |
234 | 402 | 405 | ||
242 | 403 | def test_multipleErrorsReported(self): | 406 | def test_multipleCleanupErrorsReported(self): |
243 | 404 | # Errors from all failing cleanups are reported. | 407 | # Errors from all failing cleanups are reported as separate backtraces. |
244 | 405 | self.test.addCleanup(lambda: 1/0) | 408 | self.test.addCleanup(lambda: 1/0) |
245 | 406 | self.test.addCleanup(lambda: 1/0) | 409 | self.test.addCleanup(lambda: 1/0) |
246 | 407 | self.test.run(self.logging_result) | 410 | self.logging_result = ExtendedTestResult() |
247 | 408 | self.assertErrorLogEqual( | 411 | self.test.run(self.logging_result) |
248 | 409 | ['startTest', 'addError', 'addError', 'stopTest']) | 412 | self.assertEqual(['startTest', 'addError', 'stopTest'], |
249 | 413 | [event[0] for event in self.logging_result._events]) | ||
250 | 414 | self.assertEqual(set(['traceback', 'traceback-1']), | ||
251 | 415 | set(self.logging_result._events[1][2].keys())) | ||
252 | 416 | |||
253 | 417 | def test_multipleErrorsCoreAndCleanupReported(self): | ||
254 | 418 | # Errors from all failing cleanups are reported, with stopTest, | ||
255 | 419 | # startTest inserted. | ||
256 | 420 | self.test = TestAddCleanup.LoggingTest('brokenTest') | ||
257 | 421 | self.test.addCleanup(lambda: 1/0) | ||
258 | 422 | self.test.addCleanup(lambda: 1/0) | ||
259 | 423 | self.logging_result = ExtendedTestResult() | ||
260 | 424 | self.test.run(self.logging_result) | ||
261 | 425 | self.assertEqual(['startTest', 'addError', 'stopTest'], | ||
262 | 426 | [event[0] for event in self.logging_result._events]) | ||
263 | 427 | self.assertEqual(set(['traceback', 'traceback-1', 'traceback-2']), | ||
264 | 428 | set(self.logging_result._events[1][2].keys())) | ||
265 | 410 | 429 | ||
266 | 411 | 430 | ||
267 | 412 | class TestWithDetails(TestCase): | 431 | class TestWithDetails(TestCase): |
jml says ok,