Merge lp:~jml/testtools/stack-fixes into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 233
Proposed branch: lp:~jml/testtools/stack-fixes
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 119 lines (+61/-16)
2 files modified
testtools/testresult/real.py (+41/-16)
testtools/tests/test_testresult.py (+20/-0)
To merge this branch: bzr merge lp:~jml/testtools/stack-fixes
Reviewer Review Type Date Requested Status
testtools committers Pending
Review via email: mp+81332@code.launchpad.net

Description of the change

Hello,

This is the first in a series of branches that are aimed at improving our traceback support. This one fixes bug 854769, where we are showing too many levels of stack when we are hiding our stack levels.

The problem was being caused by TestResult requiring an exact type match with failureException to trigger some of the stack hiding. When we switched to raising MismatchErrors, we lost the stack hiding for assertions.

The implementation approach has been to copy over the implementation of _exc_info_to_string and adapt it for our needs. I talked this over with mgz, who explained again the funky monkey patching that's in trunk now, and who agreed it would be a sane approach.

Thanks,
jml

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/testresult/real.py'
2--- testtools/testresult/real.py 2011-11-02 14:15:01 +0000
3+++ testtools/testresult/real.py 2011-11-04 21:23:24 +0000
4@@ -12,6 +12,7 @@
5
6 import datetime
7 import sys
8+import traceback
9 import unittest
10
11 from testtools.compat import all, _format_exc_info, str_is_unicode, _u
12@@ -35,6 +36,9 @@
13
14 utc = UTC()
15
16+STDOUT_LINE = '\nStdout:\n%s'
17+STDERR_LINE = '\nStderr:\n%s'
18+
19
20 class TestResult(unittest.TestResult):
21 """Subclass of unittest.TestResult extending the protocol for flexability.
22@@ -137,22 +141,43 @@
23 """
24 return not (self.errors or self.failures or self.unexpectedSuccesses)
25
26- if str_is_unicode:
27- # Python 3 and IronPython strings are unicode, use parent class method
28- _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
29- else:
30- # For Python 2, need to decode components of traceback according to
31- # their source, so can't use traceback.format_exception
32- # Here follows a little deep magic to copy the existing method and
33- # replace the formatter with one that returns unicode instead
34- from types import FunctionType as __F, ModuleType as __M
35- __f = unittest.TestResult._exc_info_to_string.im_func
36- __g = dict(__f.func_globals)
37- __m = __M("__fake_traceback")
38- __m.format_exception = _format_exc_info
39- __g["traceback"] = __m
40- _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode")
41- del __F, __M, __f, __g, __m
42+ def _exc_info_to_unicode(self, err, test):
43+ """Converts a sys.exc_info()-style tuple of values into a string.
44+
45+ Copied from Python 2.7's unittest.TestResult._exc_info_to_string.
46+ """
47+ exctype, value, tb = err
48+ # Skip test runner traceback levels
49+ while tb and self._is_relevant_tb_level(tb):
50+ tb = tb.tb_next
51+
52+ # testtools customization. When str is unicode (e.g. IronPython,
53+ # Python 3), traceback.format_exception returns unicode. For Python 2,
54+ # it returns bytes. We need to guarantee unicode.
55+ if str_is_unicode:
56+ format_exception = traceback.format_exception
57+ else:
58+ format_exception = _format_exc_info
59+
60+ if test.failureException and isinstance(value, test.failureException):
61+ # Skip assert*() traceback levels
62+ length = self._count_relevant_tb_levels(tb)
63+ msgLines = format_exception(exctype, value, tb, length)
64+ else:
65+ msgLines = format_exception(exctype, value, tb)
66+
67+ if getattr(self, 'buffer', None):
68+ output = sys.stdout.getvalue()
69+ error = sys.stderr.getvalue()
70+ if output:
71+ if not output.endswith('\n'):
72+ output += '\n'
73+ msgLines.append(STDOUT_LINE % output)
74+ if error:
75+ if not error.endswith('\n'):
76+ error += '\n'
77+ msgLines.append(STDERR_LINE % error)
78+ return ''.join(msgLines)
79
80 def _err_details_to_string(self, test, err=None, details=None):
81 """Convert an error in exc_info form or a contents dict to a string."""
82
83=== modified file 'testtools/tests/test_testresult.py'
84--- testtools/tests/test_testresult.py 2011-11-02 14:15:01 +0000
85+++ testtools/tests/test_testresult.py 2011-11-04 21:23:24 +0000
86@@ -74,6 +74,13 @@
87 return Test("failed")
88
89
90+def make_mismatching_test():
91+ class Test(TestCase):
92+ def mismatch(self):
93+ self.assertEqual(1, 2)
94+ return Test("mismatch")
95+
96+
97 def make_unexpectedly_successful_test():
98 class Test(TestCase):
99 def succeeded(self):
100@@ -416,6 +423,19 @@
101 'ZeroDivisionError: ...\n',
102 doctest.ELLIPSIS))
103
104+ def test_traceback_formatting_with_stack_hidden_mismatch(self):
105+ result = self.makeResult()
106+ test = make_mismatching_test()
107+ run_with_stack_hidden(True, test.run, result)
108+ self.assertThat(
109+ result.failures[0][1],
110+ DocTestMatches(
111+ 'Traceback (most recent call last):\n'
112+ ' File "...testtools...tests...test_testresult.py", line ..., in mismatch\n'
113+ ' self.assertEqual(1, 2)\n'
114+ '...MismatchError: 1 != 2\n',
115+ doctest.ELLIPSIS))
116+
117
118 class TestMultiTestResult(TestCase):
119 """Tests for 'MultiTestResult'."""

Subscribers

People subscribed via source and target branches