Merge lp:~jml/testtools/gassy-failure-660852 into lp:~testtools-committers/testtools/trunk
- gassy-failure-660852
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
Jonathan Lange (jml) wrote : | # |
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.
Martin Packman (gz) wrote : | # |
New format for output looks like a good improvement, and the issues raised on IRC have been resolve.
Jonathan Lange (jml) wrote : | # |
This isn't merged. I forgot how to use Bazaar.
- 211. By Jonathan Lange
-
Oh man I hope this works.
Robert Collins (lifeless) wrote : | # |
Why are you removing MatchesListwise etc from matchers.__all__
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.
Robert Collins (lifeless) wrote : | # |
20 -* All public matchers are now in ``testtools.
21 - (Jonathan Lange, #784859)
That still looks accidental?
This looks ok though there is one pseudo-bug with it
def test_foo(self):
self.
self.
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.
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.
Martin Packman (gz) wrote : | # |
Something went wrong with the merge here, I get an old test failure from testtools.
Jonathan Lange (jml) wrote : | # |
I can't reproduce that failure.
Jonathan Lange (jml) wrote : | # |
Not a merge problem, an OS-specific test failure. Have fixed, verified w/ mgz and pushed.
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
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__) |
* 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.