Merge lp:~gz/testtools/unicode_tracebacks_501166 into lp:~testtools-committers/testtools/trunk

Proposed by Martin Packman on 2010-05-26
Status: Superseded
Proposed branch: lp:~gz/testtools/unicode_tracebacks_501166
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 1010 lines (+763/-25) (has conflicts)
11 files modified
testtools/__init__.py (+1/-1)
testtools/compat.py (+186/-15)
testtools/content.py (+3/-3)
testtools/run.py (+2/-2)
testtools/testcase.py (+1/-1)
testtools/testresult/real.py (+30/-1)
testtools/tests/__init__.py (+2/-0)
testtools/tests/test_compat.py (+239/-0)
testtools/tests/test_content.py (+1/-1)
testtools/tests/test_testresult.py (+285/-1)
testtools/testsuite.py (+13/-0)
Text conflict in testtools/testresult/real.py
To merge this branch: bzr merge lp:~gz/testtools/unicode_tracebacks_501166
Reviewer Review Type Date Requested Status
Robert Collins 2010-05-26 Needs Fixing on 2010-05-31
testtools developers preliminary 2010-05-26 Pending
Review via email: mp+26094@code.launchpad.net

This proposal has been superseded by a proposal from 2010-06-18.

Description of the Change

Proposed just to get this on the radar and solicit some early feedback. Needs a fair bit more work (and testing, I haven't looked at Python 3 yet) before it could be merged. Am happy to be told to move around code, rewrite tests, whatever.

As described in the bug, testtools on Python 2 currently falls over if any part of a traceback is not ascii. 90% of the problem is just the stringification of exception instances, but to be properly robust against funny inputs other elements of the traceback need to be correctly decoded as well.

The aim of the branch is to swap out the TestResult traceback formatting code on Python 2 only, and make it behave pretty much as Python 3 already does. I did have some additional changes with added improvements to the presentation, but have removed most of them for the moment.

Problem code is scattered across unittest, traceback and lineache which leaves a choice of three unpalatable options:
* Duplicating a lot of code from the standard library to change a few lines
* Invasive monkey patching of core modules
* Crazy and not terribly succinct hackery
I have avoided monkey patching, but much of the added code is not very nice.

Brief overview of the diff:

<testtools/content.py>
Make sure unicode route is used (needing to create a dummy TestResult instance here feels like an abstraction leak)

<testtools/run.py>
Make stub runner use unicode route

<testtools/testresult/real.py>
On Python implementations where str is bytes make traceback formatting use a unicode returning alternative to the standard traceback functions, and a couple of other changes to stay on unicode route

<testtools/utils.py>
Substantive changes here. The various new functions essentially only matter for Python 2 so seemed next to _u and _b might be the right place.
* unicode_output_stream - helper to make it easy to treat a stream as writing unicode.
* _detect_encoding and _get_source_encoding - handling Python source encoding.
* _get_exception_encoding and _exception_to_text - stringifying exception instances.
* _format_exc_info - overall traceback and exception formatting as unicode.

<testtools/tests/__init__.py>
Add new test module to suite.

<testtools/tests/test_testresult.py>
End-to-end testing of encoding handling and formatting. Rather ad-hoc setup at the moment, but the intention in the test cases themselves is about right.

<testtools/tests/test_utils.py>
Unit testing of added functions in utils that don't depend too much on the environment. Currently just the decoding of lines from Python source files. The other functions could be tested here, but because the type and how it is handled throughout the package matters, they all need to be tested as a whole anyway.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

Thank you very much for doing this! I appreciate its early days, so I'll simply brain dump what I see and we can filter it in future passes.

I think

38 - result = TextTestResult(sys.stdout)
39 + result = TextTestResult(unicode_output_stream(sys.stdout))
40 result.startTestRun()

Needs a comment (such as 'the testtools TextTestResult outputs unicode, and sys.stdout is usually a bytestream not an encoded stream'). Writing this makes me think 'why doesn't TextTestResult take care of this for users of the API ?

I don't really like testtools.utils; I'd consider putting stuff in small dedicated modules rather than a catchall; not mandatory - what do you think? E.g. we could move _u and _b to testtools.sequencetypes. Or something.

+ if sys.version_info > (3, 0) or sys.platform == "cli":
This is repeated; helper function time.

+ # Python 3 and IronPython strings are unicode, use parent class method
Perhaps "traceback strings are unicode, ..."

The deep magic is a little mind bending at first encounter, perhaps a couple of hints in there to aid understanding in new readers. Also, if it was a helper you wouldn't need the manual del, which is otherwise pretty opaque.

Minor nit: PEP8 got changed strangely; please don't use leading empty lines in docstrings.
+class TestNonAsciiResults(TestCase):
+ """
+ Test all kinds of tracebacks are cleanly interpreted as unicode
+
->
+class TestNonAsciiResults(TestCase):
+ """Test all kinds of tracebacks are cleanly interpreted as unicode

The _run helper you have suggests the api for testtools.run isn't powerful enough/easy enough yet. Please consider improving that rather than reimplementing (even though its tiny code-wise, principle applies here)

_run_external_case wants to be two things:
 - create a module returning info on it with a cleanup method
 - use that info to run the test

whats different between rmtempdir and shutil.rmtree(ignore_errors=True) ?

rather than using a list of sample texts, consider subclasses that set _sample_text - this is closer to using multiply_tests, and gives cleaner info about things that go wrong as well as simpler test code (no loops needed). Or you could even just use testscenarios and multiply_tests.

I agree about better fidelity on the testing; consider an RE Matcher, or something along those lines.

Please file a bug about this - it seems unrelated to unicode:
# GZ 2010-05-25: Seems this breaks testtools internals.

Rather than assertIn, the DocTestMatches might be closer fidelity, or make an RE or similar one.

Only one line of VWS here please (PEP8 :P)
397 +import traceback
398 +
399 +
400 +import testtools

TestGetSourceEncoding would benefit from being able to create a module as per the split up run helper suggested above.

And to come back to utils - it really likes like we want an encodings or stringencodings or something module. If thats all that is in utils at the moment, just rename utils; otherwise put your new stuff in a different file. Or whatever feels right.

This looks pretty good; I'd be happy to land it with the above things done - its nearly *there*.

Thanks again,
Rob

review: Needs Fixing
Martin Packman (gz) wrote :
Download full text (6.0 KiB)

> I think
>
> 38 - result = TextTestResult(sys.stdout)
> 39 + result = TextTestResult(unicode_output_stream(sys.stdout))
> 40 result.startTestRun()
>
> Needs a comment (such as 'the testtools TextTestResult outputs unicode, and
> sys.stdout is usually a bytestream not an encoded stream'). Writing this makes
> me think 'why doesn't TextTestResult take care of this for users of the API ?

I think this is a weakness with the Runner code really, it's rather light on useful logic, and half the module is under `if __name__ == '__main__':` so none of it's reusable. And there's still bug 501174 too.

> I don't really like testtools.utils; I'd consider putting stuff in small
> dedicated modules rather than a catchall; not mandatory - what do you think?
> E.g. we could move _u and _b to testtools.sequencetypes. Or something.

Well, these are mostly compatibility hacks rather than useful tools, and a bunch of them should never run on some versions of Python. A better name than 'utils' would be good.

> + if sys.version_info > (3, 0) or sys.platform == "cli":
> This is repeated; helper function time.

I'm still not sure on the best spelling of this check, it might be more readable as `if str is unicode:` but I need to check all of these conditionals actually do the right thing with Python 3 and the 2to3 conversion script.

> + # Python 3 and IronPython strings are unicode, use parent class
> method
> Perhaps "traceback strings are unicode, ..."

Well, as in the str type is unicode, but yeah, this cares about what the traceback module returns.

> The deep magic is a little mind bending at first encounter, perhaps a couple
> of hints in there to aid understanding in new readers. Also, if it was a
> helper you wouldn't need the manual del, which is otherwise pretty opaque.

It's unfortunately not the kind of thing that's reusable, so a helper may not help much. The good news is if we do change the stack stripping logic we'll be ripping this out anyway.

Perhaps just copying the function straight from unittest would be less painful anyway. One of the things I thought might need checking is what is okay to copy things into the MIT-licensed testtools from unittest and the rest of the Python standard library. Writing this sort of patch means the code is inevitably some sort of derivative, even without verbatim copying of functions.

> The _run helper you have suggests the api for testtools.run isn't powerful
> enough/easy enough yet. Please consider improving that rather than
> reimplementing (even though its tiny code-wise, principle applies here)

Yep. Was trying to avoid rolling that issue into this one though, as it felt more rewrite to me than fix.

> _run_external_case wants to be two things:
> - create a module returning info on it with a cleanup method
> - use that info to run the test

Yeah, wants teasing out into a couple of smaller, more logical helpers.

> whats different between rmtempdir and shutil.rmtree(ignore_errors=True) ?

Nothing useful currently, I wrote it because I thought I'd have to work around a problem with unicode on byte based filesystems (as per posix) and bytes on unicode based f...

Read more...

Robert Collins (lifeless) wrote :

briefly, and I know I'm missing bits.

grab me on IRC for the matcher

yes, split utils into the iter and the encoding stuff

For the rest, do what you think makes the most sense ;)

-Rob

67. By Martin Packman on 2010-06-04

PEP 8 style fixes raised in review

68. By Martin Packman on 2010-06-11

Fix a bunch of boneheaded errors in TestNonAsciiResults

69. By Martin Packman on 2010-06-11

Merge trunk to pick up fixes for IronPython and Python 3

70. By Martin Packman on 2010-06-14

Changes to make tests work on Python 3, and a short term hack to make output robust

71. By Martin Packman on 2010-06-14

Make final problem test for Python 3 run rather than skip

72. By Martin Packman on 2010-06-14

Rearrange some TestNonAsciiResults helpers

73. By Martin Packman on 2010-06-14

Use flag in utils to record whether the str type is unicode rather than checking version and platform in multiple places

74. By Martin Packman on 2010-06-14

Work around a couple of quirks in non-mainstream Python implementations

75. By Martin Packman on 2010-06-15

Revert work around for IronPython coding declaration as it breaks Python 2

76. By Martin Packman on 2010-06-15

Poke encoding specific code in TestNonAsciiResults some more

77. By Martin Packman on 2010-06-15

Fix test_traceback_rechecks_encoding which got broken when adding Python 3 support

78. By Martin Packman on 2010-06-15

Found `ascii` function in Python 3 which saves some reimplementation

79. By Martin Packman on 2010-06-15

Unbreak test_non_ascii_dirname from IronPython returning filesystem encoding as None

80. By Martin Packman on 2010-06-15

Revert no longer needed cosmetic changes to test_content

81. By Martin Packman on 2010-06-15

Sanitise unicode_output_stream a bit and add some tests, needs more work

82. By Martin Packman on 2010-06-18

Make Jython skip unicode filesystem tests correctly again

83. By Martin Packman on 2010-06-18

Fix and test a bug with encoding of SyntaxError lines

84. By Martin Packman on 2010-06-18

Move most of utils module to compat and put iterate_tests in testsuite module

85. By Martin Packman on 2010-06-20

Trivial issues seen when scanning the diff for review

86. By Martin Packman on 2010-06-20

getlocale can return None if no locale is set, default to ascii

87. By Martin Packman on 2010-06-20

Allow detect encoding tests to work when a Python implementation may object to compiling them

88. By Martin Packman on 2010-06-20

Allow for mangling in shift_jis test if the exception encoding is incompatible

89. By Martin Packman on 2010-06-20

Document in tests the problems caused by CPython patch for issue #1031213

90. By Martin Packman on 2010-06-22

Avoid problem introduced with fix to Python bug #1031213 by ignoring the SyntaxError line and rereading the file instead

91. By Martin Packman on 2010-06-22

Change a test from using iso-8859-7 to iso-8859-5 which doesn't happen to have the test character on the same codepoint as iso-8859-1

92. By Martin Packman on 2010-06-22

Don't encode output as UTF-8 for TestNonAsciiResults

93. By Martin Packman on 2010-06-22

Make StringException.__str__ return UTF-8 rather than ascii/replace to prepare for merge

94. By Martin Packman on 2010-06-22

Merge trunk to resolve conflict in _StringException stringifying methods

95. By Martin Packman on 2010-06-22

Add NEWS for unicode tracebacks on Python 2

96. By Martin Packman on 2010-06-22

Unbreak test_assertion_text_shift_jis for Pythons with a unicode str type

97. By Martin Packman on 2010-06-22

Move Python 2 _StringException argument type check to __init__ from stringify methods

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/__init__.py'
2--- testtools/__init__.py 2010-05-13 12:39:18 +0000
3+++ testtools/__init__.py 2010-06-18 21:35:41 +0000
4@@ -40,8 +40,8 @@
5 )
6 from testtools.testsuite import (
7 ConcurrentTestSuite,
8+ iterate_tests,
9 )
10-from testtools.utils import iterate_tests
11
12 # same format as sys.version_info: "A tuple containing the five components of
13 # the version number: major, minor, micro, releaselevel, and serial. All
14
15=== renamed file 'testtools/utils.py' => 'testtools/compat.py'
16--- testtools/utils.py 2010-06-11 00:04:30 +0000
17+++ testtools/compat.py 2010-06-18 21:35:41 +0000
18@@ -1,13 +1,21 @@
19-# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
20+# Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
21
22 """Utilities for dealing with stuff in unittest."""
23
24
25+import codecs
26+import linecache
27+import locale
28+import os
29+import re
30 import sys
31+import traceback
32
33 __metaclass__ = type
34 __all__ = [
35- 'iterate_tests',
36+ 'advance_iterator',
37+ 'str_is_unicode',
38+ 'unicode_output_stream',
39 ]
40
41
42@@ -15,6 +23,7 @@
43 def _u(s):
44 """Replacement for u'some string' in Python 3."""
45 return s
46+ _r = ascii
47 def _b(s):
48 """A byte literal."""
49 return s.encode("latin-1")
50@@ -23,9 +32,13 @@
51 return isinstance(x, str)
52 def classtypes():
53 return (type,)
54+ str_is_unicode = True
55 else:
56 def _u(s):
57- return unicode(s, "latin-1")
58+ """Use _u('\u1234') over u'\u1234' to avoid Python 3 syntax error"""
59+ return (s.replace("\\", "\\\\").replace("\\\\u", "\\u")
60+ .replace("\\\\U", "\\U").decode("unicode-escape"))
61+ _r = repr
62 def _b(s):
63 return s
64 advance_iterator = lambda it: it.next()
65@@ -34,15 +47,173 @@
66 def classtypes():
67 import types
68 return (type, types.ClassType)
69-
70-
71-def iterate_tests(test_suite_or_case):
72- """Iterate through all of the test cases in 'test_suite_or_case'."""
73- try:
74- suite = iter(test_suite_or_case)
75- except TypeError:
76- yield test_suite_or_case
77- else:
78- for test in suite:
79- for subtest in iterate_tests(test):
80- yield subtest
81+ str_is_unicode = sys.platform == "cli"
82+
83+
84+def unicode_output_stream(stream):
85+ """Get wrapper for given stream that writes any unicode without exception
86+
87+ Characters that can't be coerced to the encoding of the stream, or 'ascii'
88+ if valid encoding is not found, will be replaced. The original stream may
89+ be returned in situations where a wrapper is determined unneeded.
90+
91+ The wrapper only allows unicode to be written, not non-ascii bytestrings,
92+ which is a good thing to ensure sanity and sanitation.
93+ """
94+ if sys.platform == "cli":
95+ # Best to never encode before writing in IronPython
96+ return stream
97+ try:
98+ writer = codecs.getwriter(stream.encoding or "")
99+ except (AttributeError, LookupError):
100+ # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs
101+ # different handling as it doesn't want bytestrings
102+ return codecs.getwriter("ascii")(stream, "replace")
103+ if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
104+ # The current stream has a unicode encoding so no error handler is needed
105+ return stream
106+ if sys.version_info > (3, 0):
107+ # Python 3 doesn't seem to make this easy, handle a common case
108+ try:
109+ return stream.__class__(stream.buffer, stream.encoding, "replace",
110+ stream.newlines, stream.line_buffering)
111+ except AttributeError:
112+ pass
113+ return writer(stream, "replace")
114+
115+
116+# The default source encoding is actually "iso-8859-1" until Python 2.5 but
117+# using non-ascii causes a deprecation warning in 2.4 and it's cleaner to
118+# treat all versions the same way
119+_default_source_encoding = "ascii"
120+
121+# Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
122+_cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
123+
124+def _detect_encoding(lines):
125+ """Get the encoding of a Python source file from a list of lines as bytes
126+
127+ This function does less than tokenize.detect_encoding added in Python 3 as
128+ it does not attempt to raise a SyntaxError when the interpreter would, it
129+ just wants the encoding of a source file Python has already compiled and
130+ determined is valid.
131+ """
132+ if not lines:
133+ return _default_source_encoding
134+ if lines[0].startswith("\xef\xbb\xbf"):
135+ # Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError
136+ return "utf-8"
137+ # Only the first two lines of the source file are examined
138+ magic = _cookie_search("".join(lines[:2]))
139+ if magic is None:
140+ return _default_source_encoding
141+ encoding = magic.group(1)
142+ try:
143+ codecs.lookup(encoding)
144+ except LookupError:
145+ # Some codecs raise something other than LookupError if they don't
146+ # support the given error handler, but not the text ones that could
147+ # actually be used for Python source code
148+ return _default_source_encoding
149+ return encoding
150+
151+
152+class _EncodingTuple(tuple):
153+ """A tuple type that can have an encoding attribute smuggled on"""
154+
155+
156+def _get_source_encoding(filename):
157+ """Detect, cache and return the encoding of Python source at filename"""
158+ try:
159+ return linecache.cache[filename].encoding
160+ except (AttributeError, KeyError):
161+ encoding = _detect_encoding(linecache.getlines(filename))
162+ if filename in linecache.cache:
163+ newtuple = _EncodingTuple(linecache.cache[filename])
164+ newtuple.encoding = encoding
165+ linecache.cache[filename] = newtuple
166+ return encoding
167+
168+def _get_exception_encoding():
169+ """Return the encoding we expect messages from the OS to be encoded in"""
170+ if os.name == "nt":
171+ # GZ 2010-05-24: Really want the codepage number instead, the error
172+ # handling of standard codecs is more deterministic
173+ return "mbcs"
174+ # GZ 2010-05-23: We need this call to be after initialisation, but there's
175+ # no benefit in asking more than once as it's a global
176+ # setting that can change after the message is formatted.
177+ return locale.getlocale(locale.LC_MESSAGES)[1]
178+
179+def _exception_to_text(evalue):
180+ """Try hard to get a sensible text value out of an exception instance"""
181+ try:
182+ return unicode(evalue)
183+ except KeyboardInterrupt:
184+ raise
185+ except:
186+ pass
187+ try:
188+ return str(evalue).decode(_get_exception_encoding(), "replace")
189+ except KeyboardInterrupt:
190+ raise
191+ except:
192+ pass
193+ # Okay, out of ideas, let higher level handle it
194+ return None
195+
196+# GZ 2010-05-23: This function is huge and horrible and I welcome suggestions
197+# on the best way to break it up
198+def _format_exc_info(eclass, evalue, tb, limit=None):
199+ """Format a stack trace and the exception information as unicode
200+
201+ Compatibility function for Python 2 which ensures each component of a
202+ traceback is correctly decoded according to its origins.
203+
204+ Based on traceback.format_exception and related functions.
205+ """
206+ fs_enc = sys.getfilesystemencoding()
207+ if tb:
208+ list = ['Traceback (most recent call last):\n']
209+ extracted_list = []
210+ for filename, lineno, name, line in traceback.extract_tb(tb, limit):
211+ extracted_list.append((
212+ filename.decode(fs_enc, "replace"),
213+ lineno,
214+ name.decode("ascii", "replace"),
215+ line.decode(_get_source_encoding(filename), "replace")))
216+ list.extend(traceback.format_list(extracted_list))
217+ else:
218+ list = []
219+ if evalue is None:
220+ # Is a (deprecated) string exception
221+ list.append(sclass.decode("ascii", "replace"))
222+ elif isinstance(evalue, SyntaxError) and len(evalue.args) > 1:
223+ # Avoid duplicating the special formatting for SyntaxError here,
224+ # instead create a new instance with unicode filename and line
225+ # Potentially gives duff spacing, but that's a pre-existing issue
226+ filename, lineno, offset, line = evalue.args[1]
227+ if filename:
228+ filename = filename.decode(fs_enc, "replace")
229+ if line:
230+ # Errors during parsing give the line from buffer encoded as
231+ # latin-1 if the given coding or utf-8 for all other codings
232+ # Can't know which was used, so just try utf-8 first
233+ try:
234+ line = line.decode("utf-8")
235+ except UnicodeDecodeError:
236+ line = line.decode("latin-1")
237+ evalue = eclass(evalue.args[0], (filename, lineno, offset, line))
238+ list.extend(traceback.format_exception_only(eclass, evalue))
239+ else:
240+ sclass = eclass.__name__
241+ svalue = _exception_to_text(evalue)
242+ if svalue:
243+ list.append("%s: %s\n" % (sclass, svalue))
244+ elif svalue is None:
245+ # GZ 2010-05-24: Not a great fallback message, but keep for the
246+ # the same for compatibility for the moment
247+ list.append("%s: <unprintable %s object>\n" % (sclass, sclass))
248+ else:
249+ list.append("%s\n" % sclass)
250+ return list
251
252=== modified file 'testtools/content.py'
253--- testtools/content.py 2010-01-16 01:31:27 +0000
254+++ testtools/content.py 2010-06-18 21:35:41 +0000
255@@ -3,10 +3,10 @@
256 """Content - a MIME-like Content object."""
257
258 import codecs
259-from unittest import TestResult
260
261+from testtools.testresult import TestResult
262+from testtools.compat import _b
263 from testtools.content_type import ContentType
264-from testtools.utils import _b
265
266
267 class Content(object):
268@@ -86,6 +86,6 @@
269 content_type = ContentType('text', 'x-traceback',
270 {"language": "python", "charset": "utf8"})
271 self._result = TestResult()
272- value = self._result._exc_info_to_string(err, test)
273+ value = self._result._exc_info_to_unicode(err, test)
274 super(TracebackContent, self).__init__(
275 content_type, lambda: [value.encode("utf8")])
276
277=== modified file 'testtools/run.py'
278--- testtools/run.py 2010-06-13 22:12:17 +0000
279+++ testtools/run.py 2010-06-18 21:35:41 +0000
280@@ -13,7 +13,7 @@
281 import sys
282
283 from testtools import TextTestResult
284-from testtools.utils import classtypes, istext
285+from testtools.compat import classtypes, istext, unicode_output_stream
286
287
288 defaultTestLoader = unittest.defaultTestLoader
289@@ -36,7 +36,7 @@
290
291 def run(self, test):
292 "Run the given test case or test suite."
293- result = TextTestResult(sys.stdout)
294+ result = TextTestResult(unicode_output_stream(sys.stdout))
295 result.startTestRun()
296 try:
297 return test.run(result)
298
299=== modified file 'testtools/testcase.py'
300--- testtools/testcase.py 2010-06-13 05:15:20 +0000
301+++ testtools/testcase.py 2010-06-18 21:35:41 +0000
302@@ -22,9 +22,9 @@
303 import unittest
304
305 from testtools import content
306+from testtools.compat import advance_iterator
307 from testtools.runtest import RunTest
308 from testtools.testresult import TestResult
309-from testtools.utils import advance_iterator
310
311
312 try:
313
314=== modified file 'testtools/testresult/real.py'
315--- testtools/testresult/real.py 2010-06-11 01:08:23 +0000
316+++ testtools/testresult/real.py 2010-06-18 21:35:41 +0000
317@@ -11,8 +11,11 @@
318 ]
319
320 import datetime
321+import sys
322 import unittest
323
324+from testtools.compat import _format_exc_info, str_is_unicode
325+
326
327 class TestResult(unittest.TestResult):
328 """Subclass of unittest.TestResult extending the protocol for flexability.
329@@ -105,10 +108,27 @@
330 """Called when a test was expected to fail, but succeed."""
331 self.unexpectedSuccesses.append(test)
332
333+ if str_is_unicode:
334+ # Python 3 and IronPython strings are unicode, use parent class method
335+ _exc_info_to_unicode = unittest.TestResult._exc_info_to_string
336+ else:
337+ # For Python 2, need to decode components of traceback according to
338+ # their source, so can't use traceback.format_exception
339+ # Here follows a little deep magic to copy the existing method and
340+ # replace the formatter with one that returns unicode instead
341+ from types import FunctionType as __F, ModuleType as __M
342+ __f = unittest.TestResult._exc_info_to_string.im_func
343+ __g = dict(__f.func_globals)
344+ __m = __M("__fake_traceback")
345+ __m.format_exception = _format_exc_info
346+ __g["traceback"] = __m
347+ _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode")
348+ del __F, __M, __f, __g, __m
349+
350 def _err_details_to_string(self, test, err=None, details=None):
351 """Convert an error in exc_info form or a contents dict to a string."""
352 if err is not None:
353- return self._exc_info_to_string(err, test)
354+ return self._exc_info_to_unicode(err, test)
355 return _details_to_str(details)
356
357 def _now(self):
358@@ -511,6 +531,7 @@
359 def __hash__(self):
360 return id(self)
361
362+<<<<<<< TREE
363 def __str__(self):
364 """Stringify better than 2.x's default behaviour of ascii encoding."""
365 if type(self.args[0]) is str:
366@@ -521,6 +542,14 @@
367 if type(self.args[0]) is str:
368 return self.args[0].decode('utf8')
369 return self.args[0]
370+=======
371+ if not str_is_unicode:
372+ def __str__(self):
373+ return self.args[0].encode("ascii", "replace")
374+
375+ def __unicode__(self):
376+ return self.args[0]
377+>>>>>>> MERGE-SOURCE
378
379 def __eq__(self, other):
380 try:
381
382=== modified file 'testtools/tests/__init__.py'
383--- testtools/tests/__init__.py 2010-01-16 01:31:27 +0000
384+++ testtools/tests/__init__.py 2010-06-18 21:35:41 +0000
385@@ -4,6 +4,7 @@
386
387 import unittest
388 from testtools.tests import (
389+ test_compat,
390 test_content,
391 test_content_type,
392 test_matchers,
393@@ -17,6 +18,7 @@
394 def test_suite():
395 suites = []
396 modules = [
397+ test_compat,
398 test_content,
399 test_content_type,
400 test_matchers,
401
402=== added file 'testtools/tests/test_compat.py'
403--- testtools/tests/test_compat.py 1970-01-01 00:00:00 +0000
404+++ testtools/tests/test_compat.py 2010-06-18 21:35:41 +0000
405@@ -0,0 +1,239 @@
406+# Copyright (c) 2010 testtools developers. See LICENSE for details.
407+
408+"""Tests for miscellaneous compatibility functions"""
409+
410+import linecache
411+import os
412+import sys
413+import tempfile
414+import traceback
415+
416+import testtools
417+
418+from testtools.compat import (
419+ _b,
420+ _detect_encoding,
421+ _get_source_encoding,
422+ _u,
423+ unicode_output_stream,
424+ )
425+
426+
427+class TestDetectEncoding(testtools.TestCase):
428+ """Test detection of Python source encodings"""
429+
430+ def _check_encoding(self, expected, lines):
431+ """Check lines are valid Python and encoding is as expected"""
432+ compile(_b("".join(lines)), "<str>", "exec")
433+ encoding = _detect_encoding(lines)
434+ self.assertEqual(expected, encoding,
435+ "Encoding %r expected but got %r from lines %r" %
436+ (expected, encoding, lines))
437+
438+ def test_examples_from_pep(self):
439+ """Check the examples given in PEP 263 all work as specified
440+
441+ See 'Examples' section of <http://www.python.org/dev/peps/pep-0263/>
442+ """
443+ # With interpreter binary and using Emacs style file encoding comment:
444+ self._check_encoding("latin-1", (
445+ "#!/usr/bin/python\n",
446+ "# -*- coding: latin-1 -*-\n",
447+ "import os, sys\n"))
448+ self._check_encoding("iso-8859-15", (
449+ "#!/usr/bin/python\n",
450+ "# -*- coding: iso-8859-15 -*-\n",
451+ "import os, sys\n"))
452+ self._check_encoding("ascii", (
453+ "#!/usr/bin/python\n",
454+ "# -*- coding: ascii -*-\n",
455+ "import os, sys\n"))
456+ # Without interpreter line, using plain text:
457+ self._check_encoding("utf-8", (
458+ "# This Python file uses the following encoding: utf-8\n",
459+ "import os, sys\n"))
460+ # Text editors might have different ways of defining the file's
461+ # encoding, e.g.
462+ self._check_encoding("latin-1", (
463+ "#!/usr/local/bin/python\n",
464+ "# coding: latin-1\n",
465+ "import os, sys\n"))
466+ # Without encoding comment, Python's parser will assume ASCII text:
467+ self._check_encoding("ascii", (
468+ "#!/usr/local/bin/python\n",
469+ "import os, sys\n"))
470+ # Encoding comments which don't work:
471+ # Missing "coding:" prefix:
472+ self._check_encoding("ascii", (
473+ "#!/usr/local/bin/python\n",
474+ "# latin-1\n",
475+ "import os, sys\n"))
476+ # Encoding comment not on line 1 or 2:
477+ self._check_encoding("ascii", (
478+ "#!/usr/local/bin/python\n",
479+ "#\n",
480+ "# -*- coding: latin-1 -*-\n",
481+ "import os, sys\n"))
482+ # Unsupported encoding:
483+ # Correct behaviour is a SyntaxError, which we aren't testing for
484+ # and fails instead with MemoryError on Python 2.4
485+ # self._check_encoding("ascii", (
486+ # "#!/usr/local/bin/python\n",
487+ # "# -*- coding: utf-42 -*-\n",
488+ # "import os, sys\n"))
489+
490+ def test_bom(self):
491+ """Test the UTF-8 BOM counts as an encoding declaration"""
492+ self._check_encoding("utf-8", (
493+ "\xef\xbb\xbfimport sys\n",
494+ ))
495+ self._check_encoding("utf-8", (
496+ "\xef\xbb\xbf# File encoding: UTF-8\n",
497+ ))
498+ self._check_encoding("utf-8", (
499+ '\xef\xbb\xbf"""Module docstring\n',
500+ '\xef\xbb\xbfThat should just be a ZWNB"""\n'))
501+ self._check_encoding("latin-1", (
502+ '"""Is this coding: latin-1 or coding: utf-8 instead?\n',
503+ '\xef\xbb\xbfThose should be latin-1 bytes"""\n'))
504+ self._check_encoding("utf-8", (
505+ "\xef\xbb\xbf# Is the coding: utf-8 or coding: euc-jp instead?\n",
506+ '"""Module docstring say \xe2\x98\x86"""\n'))
507+
508+ def test_multiple_coding_comments(self):
509+ """Test only the first of multiple coding declarations counts"""
510+ self._check_encoding("iso-8859-1", (
511+ "# Is the coding: iso-8859-1\n",
512+ "# Or is it coding: iso-8859-2\n"))
513+ self._check_encoding("iso-8859-1", (
514+ "#!/usr/bin/python\n",
515+ "# Is the coding: iso-8859-1\n",
516+ "# Or is it coding: iso-8859-2\n"))
517+ self._check_encoding("iso-8859-1", (
518+ "# Is the coding: iso-8859-1 or coding: iso-8859-2\n",
519+ "# Or coding: iso-8859-3 or coding: iso-8859-4\n"))
520+ self._check_encoding("iso-8859-2", (
521+ "# Is the coding iso-8859-1 or coding: iso-8859-2\n",
522+ "# Spot the missing colon above\n"))
523+
524+
525+class TestGetSourceEncoding(testtools.TestCase):
526+ """Test reading and caching the encodings of source files"""
527+
528+ def setUp(self):
529+ testtools.TestCase.setUp(self)
530+ dir = tempfile.mkdtemp()
531+ self.addCleanup(os.rmdir, dir)
532+ self.filename = os.path.join(dir, self.id().rsplit(".", 1)[1] + ".py")
533+ self._written = False
534+
535+ def put_source(self, text):
536+ f = open(self.filename, "w")
537+ try:
538+ f.write(text)
539+ finally:
540+ f.close()
541+ if not self._written:
542+ self._written = True
543+ self.addCleanup(os.remove, self.filename)
544+ self.addCleanup(linecache.cache.pop, self.filename, None)
545+
546+ def test_nonexistant_file_as_ascii(self):
547+ """When file can't be found, the encoding should default to ascii"""
548+ self.assertEquals("ascii", _get_source_encoding(self.filename))
549+
550+ def test_encoding_is_cached(self):
551+ """The encoding should stay the same if the cache isn't invalidated"""
552+ self.put_source(
553+ "# coding: iso-8859-13\n"
554+ "import os\n")
555+ self.assertEquals("iso-8859-13", _get_source_encoding(self.filename))
556+ self.put_source(
557+ "# coding: rot-13\n"
558+ "vzcbeg bf\n")
559+ self.assertEquals("iso-8859-13", _get_source_encoding(self.filename))
560+
561+ def test_traceback_rechecks_encoding(self):
562+ """A traceback function checks the cache and resets the encoding"""
563+ self.put_source(
564+ "# coding: iso-8859-8\n"
565+ "import os\n")
566+ self.assertEquals("iso-8859-8", _get_source_encoding(self.filename))
567+ self.put_source(
568+ "# coding: utf-8\n"
569+ "import os\n")
570+ try:
571+ exec (compile("raise RuntimeError\n", self.filename, "exec"))
572+ except RuntimeError:
573+ traceback.extract_tb(sys.exc_info()[2])
574+ else:
575+ self.fail("RuntimeError not raised")
576+ self.assertEquals("utf-8", _get_source_encoding(self.filename))
577+
578+
579+class _FakeOutputStream(object):
580+ """A simple file-like object for testing"""
581+ def __init__(self):
582+ self.writelog = []
583+ def write(self, obj):
584+ self.writelog.append(obj)
585+
586+
587+class TestUnicodeOutputStream(testtools.TestCase):
588+ """Test wrapping output streams so they work with arbitrary unicode"""
589+ uni = _u("pa\u026a\u03b8\u0259n")
590+ def setUp(self):
591+ super(TestUnicodeOutputStream, self).setUp()
592+ if sys.platform == "cli":
593+ self.skip("IronPython shouldn't wrap streams to do encoding")
594+ def test_no_encoding_becomes_ascii(self):
595+ """A stream with no encoding attribute gets ascii/replace strings"""
596+ sout = _FakeOutputStream()
597+ unicode_output_stream(sout).write(self.uni)
598+ self.assertEqual([_b("pa???n")], sout.writelog)
599+ def test_encoding_as_none_becomes_ascii(self):
600+ """A stream with encoding value of None gets ascii/replace strings"""
601+ sout = _FakeOutputStream()
602+ sout.encoding = None
603+ unicode_output_stream(sout).write(self.uni)
604+ self.assertEqual([_b("pa???n")], sout.writelog)
605+ def test_bogus_encoding_becomes_ascii(self):
606+ """A stream with a bogus encoding gets ascii/replace strings"""
607+ sout = _FakeOutputStream()
608+ sout.encoding = "bogus"
609+ unicode_output_stream(sout).write(self.uni)
610+ self.assertEqual([_b("pa???n")], sout.writelog)
611+ def test_partial_encoding_replace(self):
612+ """A string which can be partly encoded correctly should be"""
613+ sout = _FakeOutputStream()
614+ sout.encoding = "iso-8859-7"
615+ unicode_output_stream(sout).write(self.uni)
616+ self.assertEqual([_b("pa?\xe8?n")], sout.writelog)
617+ def test_unicode_encodings_not_wrapped(self):
618+ """A unicode encoding is left unwrapped as needs no error handler"""
619+ sout = _FakeOutputStream()
620+ sout.encoding = "utf-8"
621+ self.assertIs(unicode_output_stream(sout), sout)
622+ sout = _FakeOutputStream()
623+ sout.encoding = "utf-16-be"
624+ self.assertIs(unicode_output_stream(sout), sout)
625+ def test_stringio(self):
626+ """A StringIO object should maybe get an ascii native str type"""
627+ try:
628+ from cStringIO import StringIO
629+ newio = False
630+ except ImportError:
631+ from io import StringIO
632+ newio = True
633+ sout = StringIO()
634+ soutwrapper = unicode_output_stream(sout)
635+ if newio:
636+ self.expectFailure("Python 3 StringIO expects bytes",
637+ self.assertRaises, TypeError, soutwrapper.write, self.uni)
638+ soutwrapper.write(self.uni)
639+ self.assertEqual("pa???n", sout.getvalue())
640+
641+
642+def test_suite():
643+ from unittest import TestLoader
644+ return TestLoader().loadTestsFromName(__name__)
645
646=== modified file 'testtools/tests/test_content.py'
647--- testtools/tests/test_content.py 2009-12-31 03:15:19 +0000
648+++ testtools/tests/test_content.py 2010-06-18 21:35:41 +0000
649@@ -1,9 +1,9 @@
650 # Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
651
652 import unittest
653+from testtools.compat import _u
654 from testtools.content import Content, TracebackContent
655 from testtools.content_type import ContentType
656-from testtools.utils import _u
657 from testtools.tests.helpers import an_exc_info
658
659
660
661=== modified file 'testtools/tests/test_testresult.py'
662--- testtools/tests/test_testresult.py 2010-06-10 19:00:59 +0000
663+++ testtools/tests/test_testresult.py 2010-06-18 21:35:41 +0000
664@@ -4,14 +4,19 @@
665
666 __metaclass__ = type
667
668+import codecs
669 import datetime
670 try:
671 from cStringIO import StringIO
672 except ImportError:
673 from io import StringIO
674 import doctest
675+import os
676+import shutil
677 import sys
678+import tempfile
679 import threading
680+import warnings
681
682 from testtools import (
683 ExtendedToOriginalDecorator,
684@@ -22,9 +27,16 @@
685 ThreadsafeForwardingResult,
686 testresult,
687 )
688+from testtools.compat import (
689+ _b,
690+ _get_exception_encoding,
691+ _r,
692+ _u,
693+ str_is_unicode,
694+ unicode_output_stream,
695+ )
696 from testtools.content import Content, ContentType
697 from testtools.matchers import DocTestMatches
698-from testtools.utils import _u, _b
699 from testtools.tests.helpers import (
700 LoggingResult,
701 Python26TestResult,
702@@ -802,6 +814,278 @@
703 self.assertEqual(2, self.converter.foo())
704
705
706+class TestNonAsciiResults(TestCase):
707+ """Test all kinds of tracebacks are cleanly interpreted as unicode
708+
709+ Currently only uses weak "contains" assertions, would be good to be much
710+ stricter about the expected output. This would add a few failures for the
711+ current release of IronPython for instance, which gets some traceback
712+ lines muddled.
713+ """
714+
715+ _sample_texts = (
716+ _u("pa\u026a\u03b8\u0259n"), # Unicode encodings only
717+ _u("\u5357\u7121"), # In ISO 2022 encodings
718+ _u("\xa7\xa7\xa7"), # In ISO 8859 encodings
719+ )
720+ # Everything but Jython shows syntax errors on the current character
721+ _error_on_character = os.name != "java"
722+
723+ def _run(self, stream, test):
724+ """Run the test, the same as in testtools.run but not to stdout"""
725+ if not str_is_unicode:
726+ stream = codecs.getwriter("UTF-8")(stream)
727+ result = TextTestResult(stream)
728+ result.startTestRun()
729+ try:
730+ return test.run(result)
731+ finally:
732+ result.stopTestRun()
733+
734+ def _write_module(self, name, encoding, contents):
735+ """Create Python module on disk with contents in given encoding"""
736+ f = codecs.open(os.path.join(self.dir, name + ".py"), "w", encoding)
737+ try:
738+ f.write(contents)
739+ finally:
740+ f.close()
741+
742+ def _test_external_case(self, testline, coding="ascii", modulelevel="",
743+ suffix=""):
744+ """Create and run a test case in a seperate module"""
745+ self._setup_external_case(testline, coding, modulelevel, suffix)
746+ return self._run_external_case()
747+
748+ def _setup_external_case(self, testline, coding="ascii", modulelevel="",
749+ suffix=""):
750+ """Create a test case in a seperate module"""
751+ _, prefix, self.modname = self.id().rsplit(".", 2)
752+ self.dir = tempfile.mkdtemp(prefix=prefix, suffix=suffix)
753+ self.addCleanup(shutil.rmtree, self.dir)
754+ try:
755+ # Need to pre-check that the coding is valid or codecs.open drops
756+ # the file without closing it which breaks non-refcounted pythons
757+ codecs.lookup(coding)
758+ except LookupError:
759+ self.skip("Encoding unsupported by implementation: %r" % coding)
760+ self._write_module(self.modname, coding,
761+ # Older Python 2 versions don't see a coding declaration in a
762+ # docstring so it has to be in a comment, but then we can't
763+ # workaround bug: <http://ironpython.codeplex.com/workitem/26940>
764+ "# coding: %s\n"
765+ "import testtools\n"
766+ "%s\n"
767+ "class Test(testtools.TestCase):\n"
768+ " def runTest(self):\n"
769+ " %s\n" % (coding, modulelevel, testline))
770+
771+ def _run_external_case(self):
772+ """Run the prepared test case in a seperate module"""
773+ sys.path.insert(0, self.dir)
774+ self.addCleanup(sys.path.remove, self.dir)
775+ module = __import__(self.modname)
776+ self.addCleanup(sys.modules.pop, self.modname)
777+ stream = StringIO()
778+ self._run(stream, module.Test())
779+ return stream.getvalue()
780+
781+ def _silence_deprecation_warnings(self):
782+ """Shut up DeprecationWarning for this test only"""
783+ warnings.simplefilter("ignore", DeprecationWarning)
784+ self.addCleanup(warnings.filters.remove, warnings.filters[0])
785+
786+ def _get_sample_text(self, encoding="unicode_internal"):
787+ if encoding is None and str_is_unicode:
788+ encoding = "unicode_internal"
789+ for u in self._sample_texts:
790+ try:
791+ b = u.encode(encoding)
792+ if u == b.decode(encoding):
793+ if str_is_unicode:
794+ return u, u
795+ return u, b
796+ except (LookupError, UnicodeError):
797+ pass
798+ self.skip("Could not find a sample text for encoding: %r" % encoding)
799+
800+ def _as_output(self, text):
801+ if str_is_unicode:
802+ return text
803+ return text.encode("UTF-8")
804+
805+ def test_non_ascii_failure_string(self):
806+ """Assertion contents can be non-ascii and should get decoded"""
807+ text, raw = self._get_sample_text(_get_exception_encoding())
808+ textoutput = self._test_external_case("self.fail(%s)" % _r(raw))
809+ self.assertIn(self._as_output(text), textoutput)
810+
811+ def test_control_characters_in_failure_string(self):
812+ """Control characters in assertions should be escaped"""
813+ textoutput = self._test_external_case("self.fail('\\a\\a\\a')")
814+ self.expectFailure("Defense against the beeping horror unimplemented",
815+ self.assertNotIn, self._as_output("\a\a\a"), textoutput)
816+ self.assertIn(self._as_output(_u("\uFFFD\uFFFD\uFFFD")), textoutput)
817+
818+ def test_os_error(self):
819+ """Locale error messages from the OS shouldn't break anything"""
820+ textoutput = self._test_external_case(
821+ modulelevel="import os",
822+ testline="os.mkdir('/')")
823+ if os.name != "nt" or sys.version_info < (2, 5):
824+ self.assertIn(self._as_output("OSError: "), textoutput)
825+ else:
826+ self.assertIn(self._as_output("WindowsError: "), textoutput)
827+
828+ def test_assertion_text_shift_jis(self):
829+ """A terminal raw backslash in an encoded string is weird but fine"""
830+ example_text = _u("\u5341")
831+ textoutput = self._test_external_case(
832+ coding="shift_jis",
833+ testline="self.fail('%s')" % example_text)
834+ self.assertIn(self._as_output("AssertionError: %s" % example_text),
835+ textoutput)
836+
837+ def test_file_comment_iso2022_jp(self):
838+ """Control character escapes must be preserved if valid encoding"""
839+ example_text, _ = self._get_sample_text("iso2022_jp")
840+ textoutput = self._test_external_case(
841+ coding="iso2022_jp",
842+ testline="self.fail('Simple') # %s" % example_text)
843+ self.assertIn(self._as_output(example_text), textoutput)
844+
845+ def test_unicode_exception(self):
846+ """Exceptions that can be formated losslessly as unicode should be"""
847+ example_text, _ = self._get_sample_text()
848+ exception_class = (
849+ "class FancyError(Exception):\n"
850+ # A __unicode__ method does nothing on py3k but the default works
851+ " def __unicode__(self):\n"
852+ " return self.args[0]\n")
853+ textoutput = self._test_external_case(
854+ modulelevel=exception_class,
855+ testline="raise FancyError(%s)" % _r(example_text))
856+ self.assertIn(self._as_output(example_text), textoutput)
857+
858+ def test_unprintable_exception(self):
859+ """A totally useless exception instance still prints something"""
860+ exception_class = (
861+ "class UnprintableError(Exception):\n"
862+ " def __str__(self):\n"
863+ " raise RuntimeError\n"
864+ " def __repr__(self):\n"
865+ " raise RuntimeError\n")
866+ textoutput = self._test_external_case(
867+ modulelevel=exception_class,
868+ testline="raise UnprintableError")
869+ self.assertIn(self._as_output(
870+ "UnprintableError: <unprintable UnprintableError object>\n"),
871+ textoutput)
872+
873+ # GZ 2010-05-25: Seems this breaks testtools internals.
874+ def _disabled_test_string_exception(self):
875+ """Raise a string rather than an exception instance if supported"""
876+ if sys.version_info > (2, 6):
877+ self.skip("No string exceptions in Python 2.6 or later")
878+ elif sys.version_info > (2, 5):
879+ self._silence_deprecation_warnings()
880+ textoutput = self._test_external_case(testline="raise 'plain str'")
881+ self.assertIn(self._as_output("\nplain str\n"), textoutput)
882+
883+ def test_non_ascii_dirname(self):
884+ """Script paths in the traceback can be non-ascii"""
885+ text, raw = self._get_sample_text(sys.getfilesystemencoding())
886+ textoutput = self._test_external_case(
887+ # Avoid bug in Python 3 by giving a unicode source encoding rather
888+ # than just ascii which raises a SyntaxError with no other details
889+ coding="utf-8",
890+ testline="self.fail('Simple')",
891+ suffix=raw)
892+ self.assertIn(self._as_output(text), textoutput)
893+
894+ def test_syntax_error(self):
895+ """Syntax errors should still have fancy special-case formatting"""
896+ textoutput = self._test_external_case("exec ('f(a, b c)')")
897+ self.assertIn(self._as_output(
898+ ' File "<string>", line 1\n'
899+ ' f(a, b c)\n'
900+ + ' ' * self._error_on_character +
901+ ' ^\n'
902+ 'SyntaxError: '
903+ ), textoutput)
904+
905+ def test_syntax_error_import_binary(self):
906+ """Importing a binary file shouldn't break SyntaxError formatting"""
907+ if sys.version_info < (2, 5):
908+ # Python 2.4 assumes the file is latin-1 and tells you off
909+ self._silence_deprecation_warnings()
910+ self._setup_external_case("import bad")
911+ f = open(os.path.join(self.dir, "bad.py"), "wb")
912+ try:
913+ f.write(_b("x\x9c\xcb*\xcd\xcb\x06\x00\x04R\x01\xb9"))
914+ finally:
915+ f.close()
916+ textoutput = self._run_external_case()
917+ self.assertIn(self._as_output("\nSyntaxError: "), textoutput)
918+
919+ def test_syntax_error_line_iso_8859_1(self):
920+ """Syntax error on a latin-1 line shows the line decoded"""
921+ text, raw = self._get_sample_text("iso-8859-1")
922+ textoutput = self._setup_external_case("import bad")
923+ self._write_module("bad", "iso-8859-1",
924+ "# coding: iso-8859-1\n$ = 0 # %s\n" % text)
925+ textoutput = self._run_external_case()
926+ self.assertIn(self._as_output(_u(
927+ #'bad.py", line 2\n'
928+ ' $ = 0 # %s\n'
929+ + ' ' * self._error_on_character +
930+ ' ^\n'
931+ 'SyntaxError: ') %
932+ (text,)), textoutput)
933+
934+ def test_syntax_error_line_iso_8859_7(self):
935+ """Syntax error on a iso-8859-7 line shows the line decoded"""
936+ text, raw = self._get_sample_text("iso-8859-7")
937+ textoutput = self._setup_external_case("import bad")
938+ self._write_module("bad", "iso-8859-7",
939+ "# coding: iso-8859-7\n%% = 0 # %s\n" % text)
940+ textoutput = self._run_external_case()
941+ self.assertIn(self._as_output(_u(
942+ #'bad.py", line 2\n'
943+ ' %% = 0 # %s\n'
944+ + ' ' * self._error_on_character +
945+ ' ^\n'
946+ 'SyntaxError: ') %
947+ (text,)), textoutput)
948+
949+ def test_syntax_error_line_utf_8(self):
950+ """Syntax error on a utf-8 line shows the line decoded"""
951+ text, raw = self._get_sample_text("utf-8")
952+ textoutput = self._setup_external_case("import bad")
953+ self._write_module("bad", "utf-8", _u("\ufeff^ = 0 # %s\n") % text)
954+ textoutput = self._run_external_case()
955+ self.assertIn(self._as_output(_u(
956+ 'bad.py", line 1\n'
957+ ' ^ = 0 # %s\n'
958+ + ' ' * self._error_on_character +
959+ ' ^\n'
960+ 'SyntaxError: ') %
961+ text), textoutput)
962+
963+
964+class TestNonAsciiResultsWithUnittest(TestNonAsciiResults):
965+ """Test that running under unittest produces clean ascii strings"""
966+
967+ from unittest import TextTestRunner as _Runner
968+
969+ def _run(self, stream, test):
970+ return self._Runner(stream).run(test)
971+
972+ def _as_output(self, text):
973+ if str_is_unicode:
974+ return text
975+ return text.encode("ascii", "replace")
976+
977+
978 def test_suite():
979 from unittest import TestLoader
980 return TestLoader().loadTestsFromName(__name__)
981
982=== modified file 'testtools/testsuite.py'
983--- testtools/testsuite.py 2010-06-10 19:01:06 +0000
984+++ testtools/testsuite.py 2010-06-18 21:35:41 +0000
985@@ -5,6 +5,7 @@
986 __metaclass__ = type
987 __all__ = [
988 'ConcurrentTestSuite',
989+ 'iterate_tests',
990 ]
991
992 try:
993@@ -17,6 +18,18 @@
994 import testtools
995
996
997+def iterate_tests(test_suite_or_case):
998+ """Iterate through all of the test cases in 'test_suite_or_case'."""
999+ try:
1000+ suite = iter(test_suite_or_case)
1001+ except TypeError:
1002+ yield test_suite_or_case
1003+ else:
1004+ for test in suite:
1005+ for subtest in iterate_tests(test):
1006+ yield subtest
1007+
1008+
1009 class ConcurrentTestSuite(unittest.TestSuite):
1010 """A TestSuite whose run() calls out to a concurrency strategy."""
1011

Subscribers

People subscribed via source and target branches