Merge lp:~jml/testtools/gassy-failure-660852 into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 203
Proposed branch: lp:~jml/testtools/gassy-failure-660852
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 662 lines (+269/-99)
8 files modified
NEWS (+3/-0)
doc/for-test-authors.rst (+3/-7)
testtools/content_type.py (+7/-1)
testtools/matchers.py (+9/-2)
testtools/testresult/real.py (+53/-16)
testtools/tests/test_content_type.py (+10/-0)
testtools/tests/test_distutilscmd.py (+2/-2)
testtools/tests/test_testresult.py (+182/-71)
To merge this branch: bzr merge lp:~jml/testtools/gassy-failure-660852
Reviewer Review Type Date Requested Status
Robert Collins Needs Information
Martin Packman Approve
Review via email: mp+66470@code.launchpad.net

Commit message

Change the way we format details so that normal test case failures are more succinct.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

 * Change content_type to have a better repr()
 * Remove the "Text attachment" boilerplate text
 * Group empty attachments together
 * Make sure the traceback is at the bottom and make it not look like an attachment
 * Change the way text attachments look. Use curly brace quoting rather than dashes

Since the current system wasn't great for trailing whitespace in attachments, I didn't bother making it stand out with this system.

The main principles are to reduce duplication, to make it easy to see the most important information and to not hide data (with exception of whitespace mentioned above).

Fixing the excess levels in tracebacks is out of scope for this branch.

Tangentially, this branch also changes the behaviour of Equals() so my brain wouldn't explode during testing.

Revision history for this message
Jonathan Lange (jml) wrote :

Should also mention that I'm not happy with the mechanism for singling out the "special" traceback. I am happy with the result.

Revision history for this message
Martin Packman (gz) wrote :

New format for output looks like a good improvement, and the issues raised on IRC have been resolve.

review: Approve
Revision history for this message
Jonathan Lange (jml) wrote :

This isn't merged. I forgot how to use Bazaar.

211. By Jonathan Lange

Oh man I hope this works.

Revision history for this message
Robert Collins (lifeless) wrote :

Why are you removing MatchesListwise etc from matchers.__all__

review: Needs Information
Revision history for this message
Jonathan Lange (jml) wrote :

Because I failed to use Bazaar properly. Any other comments?

212. By Jonathan Lange

Move the "Run tests" line closer to OK

213. By Jonathan Lange

Restore matchers.

Revision history for this message
Robert Collins (lifeless) wrote :

20 -* All public matchers are now in ``testtools.matchers.__all__``.
21 - (Jonathan Lange, #784859)

That still looks accidental?

This looks ok though there is one pseudo-bug with it

def test_foo(self):
    self.addDetail('traceback', utf8_text('my server crashed'))
    self.fail('foo')

will specialcase the server crash not the failure. I think thats not what you intended.

214. By Jonathan Lange

Merge trunk

215. By Jonathan Lange

Manually merge in the diff.

Revision history for this message
Jonathan Lange (jml) wrote :

NEWS merges correctly for me.

The example test you give actually completely masks the 'my server crash' detail, both in trunk and in my branch. As such, since my branch causes no functional regression, I'm going to merge it in.

Revision history for this message
Martin Packman (gz) wrote :

Something went wrong with the merge here, I get an old test failure from testtools.tests.test_testresult.TestTestResult.test_traceback_formatting which was previously fixed but now has the blame pointing at r182.2.29 "Oh man I hope this works."

Revision history for this message
Jonathan Lange (jml) wrote :

I can't reproduce that failure.

Revision history for this message
Jonathan Lange (jml) wrote :

Not a merge problem, an OS-specific test failure. Have fixed, verified w/ mgz and pushed.

Revision history for this message
Martin Packman (gz) wrote :

Not the merge, just a very similar problem to the one that was fixed in r50 and a Martin unable to read the diff...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2011-07-04 18:05:16 +0000
3+++ NEWS 2011-07-06 23:11:28 +0000
4@@ -16,6 +16,9 @@
5 * Correctly display non-ASCII unicode output on terminals that claim to have a
6 unicode encoding. (Martin [gz], #804122)
7
8+* Less boilerplate displayed in test failures and errors.
9+ (Jonathan Lange, #660852)
10+
11 * New convenience assertions, ``assertIsNone`` and ``assertIsNotNone``.
12 (Christian Kampka)
13
14
15=== modified file 'doc/for-test-authors.rst'
16--- doc/for-test-authors.rst 2011-06-30 16:57:12 +0000
17+++ doc/for-test-authors.rst 2011-07-06 23:11:28 +0000
18@@ -728,12 +728,8 @@
19 ======================================================================
20 ERROR: exampletest.TestSomething.test_thingy
21 ----------------------------------------------------------------------
22- Text attachment: arbitrary-color-name
23- ------------
24- blue
25- ------------
26- Text attachment: traceback
27- ------------
28+ arbitrary-color-name: {{{blue}}}
29+
30 Traceback (most recent call last):
31 ...
32 File "exampletest.py", line 8, in test_thingy
33@@ -742,7 +738,7 @@
34 ------------
35 Ran 1 test in 0.030s
36
37-As you can see, the detail is included as a "Text attachment", here saying
38+As you can see, the detail is included as an attachment, here saying
39 that our arbitrary-color-name is "blue".
40
41
42
43=== modified file 'testtools/content_type.py'
44--- testtools/content_type.py 2011-06-30 16:57:12 +0000
45+++ testtools/content_type.py 2011-07-06 23:11:28 +0000
46@@ -27,7 +27,13 @@
47 return self.__dict__ == other.__dict__
48
49 def __repr__(self):
50- return "%s/%s params=%s" % (self.type, self.subtype, self.parameters)
51+ if self.parameters:
52+ params = '; '
53+ params += ', '.join(
54+ '%s="%s"' % (k, v) for k, v in self.parameters.items())
55+ else:
56+ params = ''
57+ return "%s/%s%s" % (self.type, self.subtype, params)
58
59
60 UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'})
61
62=== modified file 'testtools/matchers.py'
63--- testtools/matchers.py 2011-07-01 18:06:47 +0000
64+++ testtools/matchers.py 2011-07-06 23:11:28 +0000
65@@ -261,13 +261,20 @@
66 self._mismatch_string = mismatch_string
67 self.other = other
68
69+ def _format(self, thing):
70+ # Blocks of text with newlines are formatted as triple-quote
71+ # strings. Everything else is pretty-printed.
72+ if istext(thing) and '\n' in thing:
73+ return '"""\\\n%s"""' % (thing,)
74+ return pformat(thing)
75+
76 def describe(self):
77 left = repr(self.expected)
78 right = repr(self.other)
79 if len(left) + len(right) > 70:
80 return "%s:\nreference = %s\nactual = %s\n" % (
81- self._mismatch_string, pformat(self.expected),
82- pformat(self.other))
83+ self._mismatch_string, self._format(self.expected),
84+ self._format(self.other))
85 else:
86 return "%s %s %s" % (left, self._mismatch_string,right)
87
88
89=== modified file 'testtools/testresult/real.py'
90--- testtools/testresult/real.py 2011-06-30 16:57:12 +0000
91+++ testtools/testresult/real.py 2011-07-06 23:11:28 +0000
92@@ -158,7 +158,7 @@
93 """Convert an error in exc_info form or a contents dict to a string."""
94 if err is not None:
95 return self._exc_info_to_unicode(err, test)
96- return _details_to_str(details)
97+ return _details_to_str(details, special='traceback')
98
99 def _now(self):
100 """Return the current 'test time'.
101@@ -310,7 +310,7 @@
102 self.stream.write(
103 "%sUNEXPECTED SUCCESS: %s\n%s" % (
104 self.sep1, test.id(), self.sep2))
105- self.stream.write("Ran %d test%s in %.3fs\n\n" %
106+ self.stream.write("\nRan %d test%s in %.3fs\n" %
107 (self.testsRun, plural,
108 self._delta_to_float(stop - self.__start)))
109 if self.wasSuccessful():
110@@ -520,8 +520,10 @@
111
112 def _details_to_exc_info(self, details):
113 """Convert a details dict to an exc_info tuple."""
114- return (_StringException,
115- _StringException(_details_to_str(details)), None)
116+ return (
117+ _StringException,
118+ _StringException(_details_to_str(details, special='traceback')),
119+ None)
120
121 def done(self):
122 try:
123@@ -603,19 +605,54 @@
124 return False
125
126
127-def _details_to_str(details):
128- """Convert a details dict to a string."""
129- chars = []
130+def _format_text_attachment(name, text):
131+ if '\n' in text:
132+ return "%s: {{{\n%s\n}}}\n" % (name, text)
133+ return "%s: {{{%s}}}" % (name, text)
134+
135+
136+def _details_to_str(details, special=None):
137+ """Convert a details dict to a string.
138+
139+ :param details: A dictionary mapping short names to ``Content`` objects.
140+ :param special: If specified, an attachment that should have special
141+ attention drawn to it. The primary attachment. Normally it's the
142+ traceback that caused the test to fail.
143+ :return: A formatted string that can be included in text test results.
144+ """
145+ empty_attachments = []
146+ binary_attachments = []
147+ text_attachments = []
148+ special_content = None
149 # sorted is for testing, may want to remove that and use a dict
150 # subclass with defined order for items instead.
151 for key, content in sorted(details.items()):
152 if content.content_type.type != 'text':
153- chars.append('Binary content: %s\n' % key)
154- continue
155- chars.append('Text attachment: %s\n' % key)
156- chars.append('------------\n')
157- chars.extend(content.iter_text())
158- if not chars[-1].endswith('\n'):
159- chars.append('\n')
160- chars.append('------------\n')
161- return _u('').join(chars)
162+ binary_attachments.append((key, content.content_type))
163+ continue
164+ text = _u('').join(content.iter_text()).strip()
165+ if not text:
166+ empty_attachments.append(key)
167+ continue
168+ # We want the 'special' attachment to be at the bottom.
169+ if key == special:
170+ special_content = '%s\n' % (text,)
171+ continue
172+ text_attachments.append(_format_text_attachment(key, text))
173+ if text_attachments and not text_attachments[-1].endswith('\n'):
174+ text_attachments.append('')
175+ if special_content:
176+ text_attachments.append(special_content)
177+ lines = []
178+ if binary_attachments:
179+ lines.append('Binary content:\n')
180+ for name, content_type in binary_attachments:
181+ lines.append(' %s (%s)\n' % (name, content_type))
182+ if empty_attachments:
183+ lines.append('Empty attachments:\n')
184+ for name in empty_attachments:
185+ lines.append(' %s\n' % (name,))
186+ if (binary_attachments or empty_attachments) and text_attachments:
187+ lines.append('\n')
188+ lines.append('\n'.join(text_attachments))
189+ return _u('').join(lines)
190
191=== modified file 'testtools/tests/test_content_type.py'
192--- testtools/tests/test_content_type.py 2011-06-30 16:57:12 +0000
193+++ testtools/tests/test_content_type.py 2011-07-06 23:11:28 +0000
194@@ -31,6 +31,16 @@
195 self.assertTrue(content_type1.__eq__(content_type2))
196 self.assertFalse(content_type1.__eq__(content_type3))
197
198+ def test_basic_repr(self):
199+ content_type = ContentType('text', 'plain')
200+ self.assertThat(repr(content_type), Equals('text/plain'))
201+
202+ def test_extended_repr(self):
203+ content_type = ContentType(
204+ 'text', 'plain', {'foo': 'bar', 'baz': 'qux'})
205+ self.assertThat(
206+ repr(content_type), Equals('text/plain; foo="bar", baz="qux"'))
207+
208
209 class TestBuiltinContentTypes(TestCase):
210
211
212=== modified file 'testtools/tests/test_distutilscmd.py'
213--- testtools/tests/test_distutilscmd.py 2011-02-14 14:53:41 +0000
214+++ testtools/tests/test_distutilscmd.py 2011-07-06 23:11:28 +0000
215@@ -59,8 +59,8 @@
216 cmd.runner.stdout = stream
217 dist.run_command('test')
218 self.assertEqual("""Tests running...
219+
220 Ran 2 tests in 0.000s
221-
222 OK
223 """, stream.getvalue())
224
225@@ -79,8 +79,8 @@
226 cmd.runner.stdout = stream
227 dist.run_command('test')
228 self.assertEqual("""Tests running...
229+
230 Ran 2 tests in 0.000s
231-
232 OK
233 """, stream.getvalue())
234
235
236=== modified file 'testtools/tests/test_testresult.py'
237--- testtools/tests/test_testresult.py 2011-06-30 16:57:12 +0000
238+++ testtools/tests/test_testresult.py 2011-07-06 23:11:28 +0000
239@@ -31,10 +31,15 @@
240 str_is_unicode,
241 StringIO,
242 )
243-from testtools.content import Content
244+from testtools.content import (
245+ Content,
246+ content_from_stream,
247+ text_content,
248+ )
249 from testtools.content_type import ContentType, UTF8_TEXT
250 from testtools.matchers import (
251 DocTestMatches,
252+ Equals,
253 MatchesException,
254 Raises,
255 )
256@@ -45,7 +50,45 @@
257 ExtendedTestResult,
258 an_exc_info
259 )
260-from testtools.testresult.real import utc
261+from testtools.testresult.real import (
262+ _details_to_str,
263+ utc,
264+ )
265+
266+
267+def make_erroring_test():
268+ class Test(TestCase):
269+ def error(self):
270+ 1/0
271+ return Test("error")
272+
273+
274+def make_failing_test():
275+ class Test(TestCase):
276+ def failed(self):
277+ self.fail("yo!")
278+ return Test("failed")
279+
280+
281+def make_unexpectedly_successful_test():
282+ class Test(TestCase):
283+ def succeeded(self):
284+ self.expectFailure("yo!", lambda: None)
285+ return Test("succeeded")
286+
287+
288+def make_test():
289+ class Test(TestCase):
290+ def test(self):
291+ pass
292+ return Test("test")
293+
294+
295+def make_exception_info(exceptionFactory, *args, **kwargs):
296+ try:
297+ raise exceptionFactory(*args, **kwargs)
298+ except:
299+ return sys.exc_info()
300
301
302 class Python26Contract(object):
303@@ -188,7 +231,7 @@
304
305 class StartTestRunContract(FallbackContract):
306 """Defines the contract for testtools policy choices.
307-
308+
309 That is things which are not simply extensions to unittest but choices we
310 have made differently.
311 """
312@@ -326,21 +369,29 @@
313 result.time(now)
314 self.assertEqual(now, result._now())
315
316-
317-class TestWithFakeExceptions(TestCase):
318-
319- def makeExceptionInfo(self, exceptionFactory, *args, **kwargs):
320- try:
321- raise exceptionFactory(*args, **kwargs)
322- except:
323- return sys.exc_info()
324-
325-
326-class TestMultiTestResult(TestWithFakeExceptions):
327+ def test_traceback_formatting(self):
328+ result = self.makeResult()
329+ test = make_erroring_test()
330+ test.run(result)
331+ self.assertThat(
332+ result.errors[0][1],
333+ DocTestMatches(
334+ 'Traceback (most recent call last):\n'
335+ ' File "testtools/runtest.py", line ..., in _run_user\n'
336+ ' return fn(*args, **kwargs)\n'
337+ ' File "testtools/testcase.py", line ..., in _run_test_method\n'
338+ ' return self._get_test_method()()\n'
339+ ' File "testtools/tests/test_testresult.py", line ..., in error\n'
340+ ' 1/0\n'
341+ 'ZeroDivisionError: ...\n',
342+ doctest.ELLIPSIS))
343+
344+
345+class TestMultiTestResult(TestCase):
346 """Tests for 'MultiTestResult'."""
347
348 def setUp(self):
349- TestWithFakeExceptions.setUp(self)
350+ super(TestMultiTestResult, self).setUp()
351 self.result1 = LoggingResult([])
352 self.result2 = LoggingResult([])
353 self.multiResult = MultiTestResult(self.result1, self.result2)
354@@ -389,14 +440,14 @@
355 def test_addFailure(self):
356 # Calling `addFailure` on a `MultiTestResult` calls `addFailure` on
357 # all its `TestResult`s.
358- exc_info = self.makeExceptionInfo(AssertionError, 'failure')
359+ exc_info = make_exception_info(AssertionError, 'failure')
360 self.multiResult.addFailure(self, exc_info)
361 self.assertResultLogsEqual([('addFailure', self, exc_info)])
362
363 def test_addError(self):
364 # Calling `addError` on a `MultiTestResult` calls `addError` on all
365 # its `TestResult`s.
366- exc_info = self.makeExceptionInfo(RuntimeError, 'error')
367+ exc_info = make_exception_info(RuntimeError, 'error')
368 self.multiResult.addError(self, exc_info)
369 self.assertResultLogsEqual([('addError', self, exc_info)])
370
371@@ -436,30 +487,6 @@
372 super(TestTextTestResult, self).setUp()
373 self.result = TextTestResult(StringIO())
374
375- def make_erroring_test(self):
376- class Test(TestCase):
377- def error(self):
378- 1/0
379- return Test("error")
380-
381- def make_failing_test(self):
382- class Test(TestCase):
383- def failed(self):
384- self.fail("yo!")
385- return Test("failed")
386-
387- def make_unexpectedly_successful_test(self):
388- class Test(TestCase):
389- def succeeded(self):
390- self.expectFailure("yo!", lambda: None)
391- return Test("succeeded")
392-
393- def make_test(self):
394- class Test(TestCase):
395- def test(self):
396- pass
397- return Test("test")
398-
399 def getvalue(self):
400 return self.result.stream.getvalue()
401
402@@ -475,7 +502,7 @@
403 self.assertEqual("Tests running...\n", self.getvalue())
404
405 def test_stopTestRun_count_many(self):
406- test = self.make_test()
407+ test = make_test()
408 self.result.startTestRun()
409 self.result.startTest(test)
410 self.result.stopTest(test)
411@@ -484,27 +511,27 @@
412 self.result.stream = StringIO()
413 self.result.stopTestRun()
414 self.assertThat(self.getvalue(),
415- DocTestMatches("Ran 2 tests in ...s\n...", doctest.ELLIPSIS))
416+ DocTestMatches("\nRan 2 tests in ...s\n...", doctest.ELLIPSIS))
417
418 def test_stopTestRun_count_single(self):
419- test = self.make_test()
420+ test = make_test()
421 self.result.startTestRun()
422 self.result.startTest(test)
423 self.result.stopTest(test)
424 self.reset_output()
425 self.result.stopTestRun()
426 self.assertThat(self.getvalue(),
427- DocTestMatches("Ran 1 test in ...s\n\nOK\n", doctest.ELLIPSIS))
428+ DocTestMatches("\nRan 1 test in ...s\nOK\n", doctest.ELLIPSIS))
429
430 def test_stopTestRun_count_zero(self):
431 self.result.startTestRun()
432 self.reset_output()
433 self.result.stopTestRun()
434 self.assertThat(self.getvalue(),
435- DocTestMatches("Ran 0 tests in ...s\n\nOK\n", doctest.ELLIPSIS))
436+ DocTestMatches("\nRan 0 tests in ...s\nOK\n", doctest.ELLIPSIS))
437
438 def test_stopTestRun_current_time(self):
439- test = self.make_test()
440+ test = make_test()
441 now = datetime.datetime.now(utc)
442 self.result.time(now)
443 self.result.startTestRun()
444@@ -521,45 +548,43 @@
445 self.result.startTestRun()
446 self.result.stopTestRun()
447 self.assertThat(self.getvalue(),
448- DocTestMatches("...\n\nOK\n", doctest.ELLIPSIS))
449+ DocTestMatches("...\nOK\n", doctest.ELLIPSIS))
450
451 def test_stopTestRun_not_successful_failure(self):
452- test = self.make_failing_test()
453+ test = make_failing_test()
454 self.result.startTestRun()
455 test.run(self.result)
456 self.result.stopTestRun()
457 self.assertThat(self.getvalue(),
458- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
459+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
460
461 def test_stopTestRun_not_successful_error(self):
462- test = self.make_erroring_test()
463+ test = make_erroring_test()
464 self.result.startTestRun()
465 test.run(self.result)
466 self.result.stopTestRun()
467 self.assertThat(self.getvalue(),
468- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
469+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
470
471 def test_stopTestRun_not_successful_unexpected_success(self):
472- test = self.make_unexpectedly_successful_test()
473+ test = make_unexpectedly_successful_test()
474 self.result.startTestRun()
475 test.run(self.result)
476 self.result.stopTestRun()
477 self.assertThat(self.getvalue(),
478- DocTestMatches("...\n\nFAILED (failures=1)\n", doctest.ELLIPSIS))
479+ DocTestMatches("...\nFAILED (failures=1)\n", doctest.ELLIPSIS))
480
481 def test_stopTestRun_shows_details(self):
482 self.result.startTestRun()
483- self.make_erroring_test().run(self.result)
484- self.make_unexpectedly_successful_test().run(self.result)
485- self.make_failing_test().run(self.result)
486+ make_erroring_test().run(self.result)
487+ make_unexpectedly_successful_test().run(self.result)
488+ make_failing_test().run(self.result)
489 self.reset_output()
490 self.result.stopTestRun()
491 self.assertThat(self.getvalue(),
492 DocTestMatches("""...======================================================================
493 ERROR: testtools.tests.test_testresult.Test.error
494 ----------------------------------------------------------------------
495-Text attachment: traceback
496-------------
497 Traceback (most recent call last):
498 File "...testtools...runtest.py", line ..., in _run_user...
499 return fn(*args, **kwargs)
500@@ -568,12 +593,9 @@
501 File "...testtools...tests...test_testresult.py", line ..., in error
502 1/0
503 ZeroDivisionError:... divi... by zero...
504-------------
505 ======================================================================
506 FAIL: testtools.tests.test_testresult.Test.failed
507 ----------------------------------------------------------------------
508-Text attachment: traceback
509-------------
510 Traceback (most recent call last):
511 File "...testtools...runtest.py", line ..., in _run_user...
512 return fn(*args, **kwargs)
513@@ -582,18 +604,17 @@
514 File "...testtools...tests...test_testresult.py", line ..., in failed
515 self.fail("yo!")
516 AssertionError: yo!
517-------------
518 ======================================================================
519 UNEXPECTED SUCCESS: testtools.tests.test_testresult.Test.succeeded
520 ----------------------------------------------------------------------
521 ...""", doctest.ELLIPSIS | doctest.REPORT_NDIFF))
522
523
524-class TestThreadSafeForwardingResult(TestWithFakeExceptions):
525+class TestThreadSafeForwardingResult(TestCase):
526 """Tests for `TestThreadSafeForwardingResult`."""
527
528 def setUp(self):
529- TestWithFakeExceptions.setUp(self)
530+ super(TestThreadSafeForwardingResult, self).setUp()
531 self.result_semaphore = threading.Semaphore(1)
532 self.target = LoggingResult([])
533 self.result1 = ThreadsafeForwardingResult(self.target,
534@@ -622,14 +643,14 @@
535
536 def test_forwarding_methods(self):
537 # error, failure, skip and success are forwarded in batches.
538- exc_info1 = self.makeExceptionInfo(RuntimeError, 'error')
539+ exc_info1 = make_exception_info(RuntimeError, 'error')
540 starttime1 = datetime.datetime.utcfromtimestamp(1.489)
541 endtime1 = datetime.datetime.utcfromtimestamp(51.476)
542 self.result1.time(starttime1)
543 self.result1.startTest(self)
544 self.result1.time(endtime1)
545 self.result1.addError(self, exc_info1)
546- exc_info2 = self.makeExceptionInfo(AssertionError, 'failure')
547+ exc_info2 = make_exception_info(AssertionError, 'failure')
548 starttime2 = datetime.datetime.utcfromtimestamp(2.489)
549 endtime2 = datetime.datetime.utcfromtimestamp(3.476)
550 self.result1.time(starttime2)
551@@ -706,10 +727,19 @@
552 details = {'text 1': Content(ContentType('text', 'plain'), text1),
553 'text 2': Content(ContentType('text', 'strange'), text2),
554 'bin 1': Content(ContentType('application', 'binary'), bin1)}
555- return (details, "Binary content: bin 1\n"
556- "Text attachment: text 1\n------------\n1\n2\n"
557- "------------\nText attachment: text 2\n------------\n"
558- "3\n4\n------------\n")
559+ return (details,
560+ ("Binary content:\n"
561+ " bin 1 (application/binary)\n"
562+ "\n"
563+ "text 1: {{{\n"
564+ "1\n"
565+ "2\n"
566+ "}}}\n"
567+ "\n"
568+ "text 2: {{{\n"
569+ "3\n"
570+ "4\n"
571+ "}}}\n"))
572
573 def check_outcome_details_to_exec_info(self, outcome, expected=None):
574 """Call an outcome with a details dict to be made into exc_info."""
575@@ -1367,6 +1397,87 @@
576 return text.encode("utf-8")
577
578
579+class TestDetailsToStr(TestCase):
580+
581+ def test_no_details(self):
582+ string = _details_to_str({})
583+ self.assertThat(string, Equals(''))
584+
585+ def test_binary_content(self):
586+ content = content_from_stream(
587+ StringIO('foo'), content_type=ContentType('image', 'jpeg'))
588+ string = _details_to_str({'attachment': content})
589+ self.assertThat(
590+ string, Equals("""\
591+Binary content:
592+ attachment (image/jpeg)
593+"""))
594+
595+ def test_single_line_content(self):
596+ content = text_content('foo')
597+ string = _details_to_str({'attachment': content})
598+ self.assertThat(string, Equals('attachment: {{{foo}}}\n'))
599+
600+ def test_multi_line_text_content(self):
601+ content = text_content('foo\nbar\nbaz')
602+ string = _details_to_str({'attachment': content})
603+ self.assertThat(string, Equals('attachment: {{{\nfoo\nbar\nbaz\n}}}\n'))
604+
605+ def test_special_text_content(self):
606+ content = text_content('foo')
607+ string = _details_to_str({'attachment': content}, special='attachment')
608+ self.assertThat(string, Equals('foo\n'))
609+
610+ def test_multiple_text_content(self):
611+ string = _details_to_str(
612+ {'attachment': text_content('foo\nfoo'),
613+ 'attachment-1': text_content('bar\nbar')})
614+ self.assertThat(
615+ string, Equals('attachment: {{{\n'
616+ 'foo\n'
617+ 'foo\n'
618+ '}}}\n'
619+ '\n'
620+ 'attachment-1: {{{\n'
621+ 'bar\n'
622+ 'bar\n'
623+ '}}}\n'))
624+
625+ def test_empty_attachment(self):
626+ string = _details_to_str({'attachment': text_content('')})
627+ self.assertThat(
628+ string, Equals("""\
629+Empty attachments:
630+ attachment
631+"""))
632+
633+ def test_lots_of_different_attachments(self):
634+ jpg = lambda x: content_from_stream(
635+ StringIO(x), ContentType('image', 'jpeg'))
636+ attachments = {
637+ 'attachment': text_content('foo'),
638+ 'attachment-1': text_content('traceback'),
639+ 'attachment-2': jpg('pic1'),
640+ 'attachment-3': text_content('bar'),
641+ 'attachment-4': text_content(''),
642+ 'attachment-5': jpg('pic2'),
643+ }
644+ string = _details_to_str(attachments, special='attachment-1')
645+ self.assertThat(
646+ string, Equals("""\
647+Binary content:
648+ attachment-2 (image/jpeg)
649+ attachment-5 (image/jpeg)
650+Empty attachments:
651+ attachment-4
652+
653+attachment: {{{foo}}}
654+attachment-3: {{{bar}}}
655+
656+traceback
657+"""))
658+
659+
660 def test_suite():
661 from unittest import TestLoader
662 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches