Skip to content

Commit 6ebfcdd

Browse files
ikedamdnozay
authored andcommitted
Use own buffering mechanism to capture stdout / stderr for XML reports
1 parent 2713535 commit 6ebfcdd

File tree

3 files changed

+149
-19
lines changed

3 files changed

+149
-19
lines changed

tests/testsuite.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
"""Executable module to test unittest-xml-reporting.
55
"""
6+
import contextlib
7+
import io
68
import sys
79

810
from xmlrunner.unittest import unittest
@@ -55,6 +57,22 @@ def test_doctest_example(self):
5557
'name="twice"'.encode('utf8'), output)
5658

5759

60+
@contextlib.contextmanager
61+
def capture_stdout_stderr():
62+
"""
63+
context manager to capture stdout and stderr
64+
"""
65+
orig_stdout = sys.stdout
66+
orig_stderr = sys.stderr
67+
sys.stdout = StringIO()
68+
sys.stderr = StringIO()
69+
try:
70+
yield (sys.stdout, sys.stderr)
71+
finally:
72+
sys.stdout = orig_stdout
73+
sys.stderr = orig_stderr
74+
75+
5876
class XMLTestRunnerTestCase(unittest.TestCase):
5977
"""
6078
XMLTestRunner test case.
@@ -102,6 +120,9 @@ def test_runner_buffer_output_fail(self):
102120
print('should be printed')
103121
self.fail('expected to fail')
104122

123+
def test_output(self):
124+
print('test message')
125+
105126
def test_non_ascii_runner_buffer_output_fail(self):
106127
print(u'Where is the café ?')
107128
self.fail(u'The café could not be found')
@@ -270,7 +291,25 @@ def test_xmlrunner_non_ascii_failures(self):
270291
runner = xmlrunner.XMLTestRunner(
271292
stream=self.stream, output=outdir, verbosity=self.verbosity,
272293
**self.runner_kwargs)
273-
runner.run(suite)
294+
295+
# allow output non-ascii letters to stdout
296+
orig_stdout = sys.stdout
297+
if getattr(sys.stdout, 'buffer', None):
298+
# Python3
299+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
300+
else:
301+
# Python2
302+
import codecs
303+
sys.stdout = codecs.getwriter("utf-8")(sys.stdout)
304+
305+
try:
306+
runner.run(suite)
307+
finally:
308+
if getattr(sys.stdout, 'buffer', None):
309+
# Python3
310+
# Not to be closed when TextIOWrapper is disposed.
311+
sys.stdout.detach()
312+
sys.stdout = orig_stdout
274313
outdir.seek(0)
275314
output = outdir.read()
276315
self.assertIn(
@@ -296,10 +335,52 @@ def test_xmlrunner_buffer_output_pass(self):
296335
def test_xmlrunner_buffer_output_fail(self):
297336
suite = unittest.TestSuite()
298337
suite.addTest(self.DummyTest('test_runner_buffer_output_fail'))
338+
# --buffer option
339+
self.runner_kwargs['buffer'] = True
299340
self._test_xmlrunner(suite)
300341
testsuite_output = self.stream.getvalue()
301342
self.assertIn('should be printed', testsuite_output)
302343

344+
def test_xmlrunner_output_without_buffer(self):
345+
suite = unittest.TestSuite()
346+
suite.addTest(self.DummyTest('test_output'))
347+
with capture_stdout_stderr() as r:
348+
self._test_xmlrunner(suite)
349+
output_from_test = r[0].getvalue()
350+
self.assertIn('test message', output_from_test)
351+
352+
def test_xmlrunner_output_with_buffer(self):
353+
suite = unittest.TestSuite()
354+
suite.addTest(self.DummyTest('test_output'))
355+
# --buffer option
356+
self.runner_kwargs['buffer'] = True
357+
with capture_stdout_stderr() as r:
358+
self._test_xmlrunner(suite)
359+
output_from_test = r[0].getvalue()
360+
self.assertNotIn('test message', output_from_test)
361+
362+
def test_xmlrunner_stdout_stderr_recovered_without_buffer(self):
363+
orig_stdout = sys.stdout
364+
orig_stderr = sys.stderr
365+
suite = unittest.TestSuite()
366+
suite.addTest(self.DummyTest('test_pass'))
367+
self._test_xmlrunner(suite)
368+
self.assertIs(orig_stdout, sys.stdout)
369+
self.assertIs(orig_stderr, sys.stderr)
370+
371+
def test_xmlrunner_stdout_stderr_recovered_with_buffer(self):
372+
orig_stdout = sys.stdout
373+
orig_stderr = sys.stderr
374+
suite = unittest.TestSuite()
375+
suite.addTest(self.DummyTest('test_pass'))
376+
# --buffer option
377+
self.runner_kwargs['buffer'] = True
378+
self._test_xmlrunner(suite)
379+
self.assertIs(orig_stdout, sys.stdout)
380+
self.assertIs(orig_stderr, sys.stderr)
381+
suite = unittest.TestSuite()
382+
suite.addTest(self.DummyTest('test_pass'))
383+
303384
@unittest.skipIf(not hasattr(unittest.TestCase, 'subTest'),
304385
'unittest.TestCase.subTest not present.')
305386
def test_unittest_subTest_fail(self):

xmlrunner/result.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
import io
23
import os
34
import sys
45
import datetime
@@ -82,6 +83,37 @@ def testcase_name(test_method):
8283
return result
8384

8485

86+
class _DuplicateWriter(io.RawIOBase):
87+
"""
88+
Duplicate output from the first handle to the second handle
89+
90+
The second handle is expected to be a StringIO and not to block.
91+
"""
92+
93+
def __init__(self, first, second):
94+
super(_DuplicateWriter, self).__init__()
95+
self._first = first
96+
self._second = second
97+
98+
def flush(self):
99+
self._first.flush()
100+
self._second.flush()
101+
102+
def writable(self):
103+
return True
104+
105+
def writelines(self, lines):
106+
self._first.writelines(lines)
107+
self._second.writelines(lines)
108+
109+
def write(self, b):
110+
len = self._first.write(b)
111+
112+
# expected to always succeed to write
113+
self._second.write(b[:len])
114+
return len
115+
116+
85117
class _TestInfo(object):
86118
"""
87119
This class keeps useful information about the execution of a
@@ -152,9 +184,12 @@ class _XMLTestResult(_TextTestResult):
152184
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1,
153185
elapsed_times=True, properties=None, infoclass=None):
154186
_TextTestResult.__init__(self, stream, descriptions, verbosity)
155-
self.buffer = True # we are capturing test output
156187
self._stdout_data = None
157188
self._stderr_data = None
189+
self._stdout_capture = StringIO()
190+
self.__stdout_saved = None
191+
self._stderr_capture = StringIO()
192+
self.__stderr_saved = None
158193
self.successes = []
159194
self.callback = None
160195
self.elapsed_times = elapsed_times
@@ -202,14 +237,35 @@ def startTest(self, test):
202237
self.stream.write(' ' + self.getDescription(test))
203238
self.stream.write(" ... ")
204239

240+
def _setupStdout(self):
241+
"""
242+
Capture stdout / stderr by replacing sys.stdout / sys.stderr
243+
"""
244+
super(_XMLTestResult, self)._setupStdout()
245+
self.__stdout_saved = sys.stdout
246+
sys.stdout = _DuplicateWriter(sys.stdout, self._stdout_capture)
247+
self.__stderr_saved = sys.stderr
248+
sys.stderr = _DuplicateWriter(sys.stderr, self._stderr_capture)
249+
250+
def _restoreStdout(self):
251+
"""
252+
Stop capturing stdout / stderr and recover sys.stdout / sys.stderr
253+
"""
254+
if self.__stdout_saved:
255+
sys.stdout = self.__stdout_saved
256+
self.__stdout_saved = None
257+
if self.__stderr_saved:
258+
sys.stderr = self.__stderr_saved
259+
self.__stderr_saved = None
260+
self._stdout_capture.seek(0)
261+
self._stdout_capture.truncate()
262+
self._stderr_capture.seek(0)
263+
self._stderr_capture.truncate()
264+
super(_XMLTestResult, self)._restoreStdout()
265+
205266
def _save_output_data(self):
206-
# Only try to get sys.stdout and sys.sterr as they not be
207-
# StringIO yet, e.g. when test fails during __call__
208-
try:
209-
self._stdout_data = sys.stdout.getvalue()
210-
self._stderr_data = sys.stderr.getvalue()
211-
except AttributeError:
212-
pass
267+
self._stdout_data = self._stdout_capture.getvalue()
268+
self._stderr_data = self._stderr_capture.getvalue()
213269

214270
def stopTest(self, test):
215271
"""
@@ -526,16 +582,8 @@ def _exc_info_to_string(self, err, test):
526582
msgLines = traceback.format_exception(exctype, value, tb)
527583

528584
if self.buffer:
529-
# Only try to get sys.stdout and sys.sterr as they not be
530-
# StringIO yet, e.g. when test fails during __call__
531-
try:
532-
output = sys.stdout.getvalue()
533-
except AttributeError:
534-
output = None
535-
try:
536-
error = sys.stderr.getvalue()
537-
except AttributeError:
538-
error = None
585+
output = self._stdout_capture.getvalue()
586+
error = self._stdout_capture.getvalue()
539587
if output:
540588
if not output.endswith('\n'):
541589
output += '\n'

xmlrunner/runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def run(self, test):
5252
# Prepare the test execution
5353
result = self._make_result()
5454
result.failfast = self.failfast
55+
result.buffer = self.buffer
5556
if hasattr(test, 'properties'):
5657
# junit testsuite properties
5758
result.properties = test.properties

0 commit comments

Comments
 (0)