Merge lp:~bac/zope.testing/1012171 into lp:~launchpad/zope.testing/3.9.4-fork

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: 43
Merged at revision: 41
Proposed branch: lp:~bac/zope.testing/1012171
Merge into: lp:~launchpad/zope.testing/3.9.4-fork
Diff against target: 257 lines (+129/-9)
5 files modified
.bzrignore (+1/-0)
setup.py (+1/-1)
src/zope/testing/testrunner/formatter.py (+30/-2)
src/zope/testing/testrunner/options.py (+4/-2)
src/zope/testing/testrunner/test_subunit.py (+93/-4)
To merge this branch: bzr merge lp:~bac/zope.testing/1012171
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+111093@code.launchpad.net

Commit message

Include data written to stdout or stderr by tests into the subunit output stream.

Description of the change

Include data written to stdout or stderr by tests into the subunit output stream.

Test:

bin/test -vvt test_subunit

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This looks great.

I'm disappointed in the hoops you had to jump through to build the content.Content objects.

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

This:
+ if msg:
+ details['STDOUT:'] = content.Content(
+ self.PLAIN_TEXT, partial(lambda x: x, msg))

Would be simpler as:
if msg:
    details['STDOUT:'] = content.text_content(msg)

(assuming msg is a unicode object, not a bytestring). If msg is a
bytestring, then:
if msg:
    details['STDOUT:'] = content.Content(content_type.UTF8_TEXT, lambda:[msg])

would be appropriate.

As it is, you're generating content objects that iterate bytes of
length 1, which will be pathologically slow, so you should change this
one way or another. Happy to chat on IRC or wherever if you need more
pointers.

-Rob

Revision history for this message
Gavin Panella (allenap) wrote :

On 19 June 2012 21:46, Robert Collins <email address hidden> wrote:
...
> (assuming msg is a unicode object, not a bytestring). If msg is a
> bytestring, then:
> if msg:
>    details['STDOUT:'] = content.Content(content_type.UTF8_TEXT, lambda:[msg])

This will keep a reference to msg in the enclosing scope. A quick look
at the code shows that msg is changed later in the function, so this
will break, hence the need for the partial() I think.

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

On Wed, Jun 20, 2012 at 10:28 AM, Gavin Panella
<email address hidden> wrote:
> On 19 June 2012 21:46, Robert Collins <email address hidden> wrote:
> ...
>> (assuming msg is a unicode object, not a bytestring). If msg is a
>> bytestring, then:
>> if msg:
>>    details['STDOUT:'] = content.Content(content_type.UTF8_TEXT, lambda:[msg])
>
> This will keep a reference to msg in the enclosing scope. A quick look
> at the code shows that msg is changed later in the function, so this
> will break, hence the need for the partial() I think.

Oh! So, I'd wrap the two lines in a helper, that will break the
scoping problem and be shorter overall:

def attach(label, msg):
    if msg:
        details[label] = content.Content(content_type.UTF8_TEXT, lambda:[msg])
attach('STDOUT:, _get_new_stream_output(sys.stdout))
attach('STDERR:, _get_new_stream_output(sys.stderr))

5 lines vs 6, and less partial gymnastics ;)

-Rob

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2012-06-11 18:30:09 +0000
+++ .bzrignore 2012-06-19 19:31:21 +0000
@@ -7,3 +7,4 @@
7Session.vim7Session.vim
8dist8dist
9tags9tags
10.emacs.desktop
1011
=== modified file 'setup.py'
--- setup.py 2012-06-14 16:56:56 +0000
+++ setup.py 2012-06-19 19:31:21 +0000
@@ -85,7 +85,7 @@
8585
86setup(86setup(
87 name='zope.testing',87 name='zope.testing',
88 version = '3.9.4-p13',88 version = '3.9.4-p14',
89 url='http://pypi.python.org/pypi/zope.testing',89 url='http://pypi.python.org/pypi/zope.testing',
90 license='ZPL 2.1',90 license='ZPL 2.1',
91 description='Zope testing framework, including the testrunner script.',91 description='Zope testing framework, including the testrunner script.',
9292
=== modified file 'src/zope/testing/testrunner/formatter.py'
--- src/zope/testing/testrunner/formatter.py 2012-06-12 19:23:43 +0000
+++ src/zope/testing/testrunner/formatter.py 2012-06-19 19:31:21 +0000
@@ -24,6 +24,7 @@
24import traceback24import traceback
2525
26from datetime import datetime, timedelta26from datetime import datetime, timedelta
27from functools import partial
2728
28from zope.testing.exceptions import DocTestFailureException29from zope.testing.exceptions import DocTestFailureException
2930
@@ -713,6 +714,7 @@
713 TAG_THREADS = 'zope:threads'714 TAG_THREADS = 'zope:threads'
714 TAG_REFCOUNTS = 'zope:refcounts'715 TAG_REFCOUNTS = 'zope:refcounts'
715716
717
716 def __init__(self, options, stream=None):718 def __init__(self, options, stream=None):
717 if subunit is None:719 if subunit is None:
718 raise Exception("Requires subunit 0.0.5 or better")720 raise Exception("Requires subunit 0.0.5 or better")
@@ -907,7 +909,9 @@
907 def test_success(self, test, seconds):909 def test_success(self, test, seconds):
908 if self._time_tests:910 if self._time_tests:
909 self._emit_timestamp()911 self._emit_timestamp()
910 self._subunit.addSuccess(test)912 details = {}
913 self._add_test_output(details)
914 self._subunit.addSuccess(test, details=details)
911915
912 def import_errors(self, import_errors):916 def import_errors(self, import_errors):
913 """Report test-module import errors (if any)."""917 """Report test-module import errors (if any)."""
@@ -943,7 +947,29 @@
943947
944 return {948 return {
945 'traceback': content.Content(949 'traceback': content.Content(
946 self.TRACEBACK_CONTENT_TYPE, lambda: [unicode_tb.encode('utf8')])}950 self.TRACEBACK_CONTENT_TYPE,
951 lambda: [unicode_tb.encode('utf8')])}
952
953 def _get_new_stream_output(self, stream):
954 """Get the stream output written since the last time."""
955 try:
956 stream.truncate()
957 msg = stream.getvalue()
958 stream.seek(0)
959 except (AttributeError, IOError):
960 msg = None
961 return msg
962
963 def _add_test_output(self, details):
964 """If tests write data to stdout or stderr, add it to the details."""
965 msg = self._get_new_stream_output(sys.stdout)
966 if msg:
967 details['STDOUT:'] = content.Content(
968 self.PLAIN_TEXT, partial(lambda x: x, msg))
969 msg = self._get_new_stream_output(sys.stderr)
970 if msg:
971 details['STDERR:'] = content.Content(
972 self.PLAIN_TEXT, partial(lambda x: x, msg))
947973
948 def test_error(self, test, seconds, exc_info):974 def test_error(self, test, seconds, exc_info):
949 """Report that an error occurred while running a test.975 """Report that an error occurred while running a test.
@@ -955,6 +981,7 @@
955 if self._time_tests:981 if self._time_tests:
956 self._emit_timestamp()982 self._emit_timestamp()
957 details = self._exc_info_to_details(exc_info)983 details = self._exc_info_to_details(exc_info)
984 self._add_test_output(details)
958 self._subunit.addError(test, details=details)985 self._subunit.addError(test, details=details)
959986
960 def test_failure(self, test, seconds, exc_info):987 def test_failure(self, test, seconds, exc_info):
@@ -967,6 +994,7 @@
967 if self._time_tests:994 if self._time_tests:
968 self._emit_timestamp()995 self._emit_timestamp()
969 details = self._exc_info_to_details(exc_info)996 details = self._exc_info_to_details(exc_info)
997 self._add_test_output(details)
970 self._subunit.addFailure(test, details=details)998 self._subunit.addFailure(test, details=details)
971999
972 def profiler_stats(self, stats):1000 def profiler_stats(self, stats):
9731001
=== modified file 'src/zope/testing/testrunner/options.py'
--- src/zope/testing/testrunner/options.py 2012-06-14 17:27:43 +0000
+++ src/zope/testing/testrunner/options.py 2012-06-19 19:31:21 +0000
@@ -19,6 +19,7 @@
19import optparse19import optparse
20import re20import re
21import os21import os
22from StringIO import StringIO
22import sys23import sys
2324
24import pkg_resources25import pkg_resources
@@ -511,6 +512,7 @@
511 return (lambda s: not pattern(s))512 return (lambda s: not pattern(s))
512 return re.compile(pattern).search513 return re.compile(pattern).search
513514
515
514def merge_options(options, defaults):516def merge_options(options, defaults):
515 odict = options.__dict__517 odict = options.__dict__
516 for name, value in defaults.__dict__.items():518 for name, value in defaults.__dict__.items():
@@ -601,12 +603,12 @@
601 streams_munged = True603 streams_munged = True
602 # Replace stdout (and possibly stderr) with a file-like black hole604 # Replace stdout (and possibly stderr) with a file-like black hole
603 # so the subunit stream does not get corrupted by test output.605 # so the subunit stream does not get corrupted by test output.
604 sys.stdout = NullStream()606 sys.stdout = StringIO()
605 # If we are running in a subprocess then the test runner code will607 # If we are running in a subprocess then the test runner code will
606 # use stderr as a communication channel back to the spawning608 # use stderr as a communication channel back to the spawning
607 # process so we shouldn't touch it.609 # process so we shouldn't touch it.
608 if '--resume-layer' not in sys.argv:610 if '--resume-layer' not in sys.argv:
609 sys.__stderr__ = sys.stderr = NullStream()611 sys.__stderr__ = sys.stderr = StringIO()
610 # Send subunit output to the real stdout.612 # Send subunit output to the real stdout.
611 options.output = SubunitOutputFormatter(options, sys.__stdout__)613 options.output = SubunitOutputFormatter(options, sys.__stdout__)
612614
613615
=== modified file 'src/zope/testing/testrunner/test_subunit.py'
--- src/zope/testing/testrunner/test_subunit.py 2012-05-30 11:59:41 +0000
+++ src/zope/testing/testrunner/test_subunit.py 2012-06-19 19:31:21 +0000
@@ -18,10 +18,14 @@
18import sys18import sys
19import unittest19import unittest
20import formatter20import formatter
21import subunit
22from StringIO import StringIO21from StringIO import StringIO
2322
2423
24class FormatterOptions:
25 """Simple options class for use with formatter."""
26 verbose=False
27
28
25class TestSubunitTracebackPrinting(unittest.TestCase):29class TestSubunitTracebackPrinting(unittest.TestCase):
2630
27 def makeByteStringFailure(self, text, encoding):31 def makeByteStringFailure(self, text, encoding):
@@ -35,8 +39,6 @@
35 return sys.exc_info()39 return sys.exc_info()
3640
37 def setUp(self):41 def setUp(self):
38 class FormatterOptions:
39 verbose=False
40 options = FormatterOptions()42 options = FormatterOptions()
4143
42 self.output = StringIO()44 self.output = StringIO()
@@ -56,4 +58,91 @@
56 def test_print_failure_containing_latin1_bytestrings(self):58 def test_print_failure_containing_latin1_bytestrings(self):
57 exc_info = self.makeByteStringFailure(unichr(241), 'latin1')59 exc_info = self.makeByteStringFailure(unichr(241), 'latin1')
58 self.subunit_formatter.test_failure(self, 0, exc_info)60 self.subunit_formatter.test_failure(self, 0, exc_info)
59 assert "AssertionError: \xef\xbf\xbd0" in self.output.getvalue()
60\ No newline at end of file61\ No newline at end of file
62 self.assertIn("\xef\xbf\xbd", self.output.getvalue())
63
64
65class TestSubunitStreamReporting(unittest.TestCase):
66 """Test capture of stdout and stderr.
67
68 The testrunner sets up fake I/O streams for the tests to use for
69 sys.stdout and sys.stderr. Anything written to those streams is
70 captured and added as part of the subunit multi-part details output.
71 """
72 def setFakeStreams(self):
73 sys.stdout = StringIO()
74 sys.stderr = StringIO()
75
76 def restoreStreams(self):
77 sys.stdout = sys.__stdout__
78 sys.stderr = sys.__stderr__
79
80 def makeExcInfo(self):
81 try:
82 assert False
83 except:
84 return sys.exc_info()
85
86 def setUp(self):
87 class FormatterOptions:
88 verbose = False
89 options = FormatterOptions()
90
91 self.output = StringIO()
92 self.setFakeStreams()
93 self.subunit_formatter = formatter.SubunitOutputFormatter(
94 options, stream=self.output)
95 #self.subunit_formatter._set_stream_positions()
96
97 def tearDown(self):
98 self.restoreStreams()
99
100 def test_stream_success(self):
101 sys.stdout.write("Output written to stdout\n")
102 sys.stderr.write("Output written to stderr\n")
103 fake_test = formatter.FakeTest('stdout_test')
104 self.subunit_formatter.test_success(fake_test, 10)
105 self.restoreStreams()
106 self.assertIn('STDOUT:', self.output.getvalue())
107 self.assertIn('STDERR:', self.output.getvalue())
108
109
110 def test_stream_error(self):
111 sys.stdout.write("Output written to stdout\n")
112 sys.stderr.write("Output written to stderr\n")
113 fake_test = formatter.FakeTest('error_test')
114 exc_info = self.makeExcInfo()
115 self.subunit_formatter.test_error(fake_test, 10, exc_info)
116 self.restoreStreams()
117 self.assertIn('STDOUT:', self.output.getvalue())
118 self.assertIn('STDERR:', self.output.getvalue())
119
120 def test_stream_failure(self):
121 sys.stdout.write("Output written to stdout\n")
122 sys.stderr.write("Output written to stderr\n")
123 fake_test = formatter.FakeTest('error_test')
124 exc_info = self.makeExcInfo()
125 self.subunit_formatter.test_failure(fake_test, 10, exc_info)
126 self.restoreStreams()
127 self.assertIn('STDOUT:', self.output.getvalue())
128 self.assertIn('STDERR:', self.output.getvalue())
129
130 def test_multiple_messages(self):
131 # Only never-reported messages should be seen.
132 fake_test = formatter.FakeTest('stdout_test')
133 sys.stdout.write("First message written to stdout\n")
134 sys.stderr.write("First message written to stderr\n")
135 self.subunit_formatter.test_success(fake_test, 10)
136 self.assertIn(
137 "First message written to stdout", self.output.getvalue())
138 self.assertIn(
139 "First message written to stderr", self.output.getvalue())
140 self.output.seek(0)
141 self.output.truncate()
142 sys.stdout.write("Second message written to stdout\n")
143 sys.stderr.write("Second message written to stderr\n")
144 self.subunit_formatter.test_success(fake_test, 10)
145 self.restoreStreams()
146 self.assertNotIn(
147 "First message written to stdout", self.output.getvalue())
148 self.assertNotIn(
149 "First message written to stderr", self.output.getvalue())

Subscribers

People subscribed via source and target branches