Skip to content

Commit 677ca89

Browse files
committed
Make sure output_stream can handle non-utf8 bytes
This is needed to safely output raw subunit v2 streams.
1 parent 4fc3813 commit 677ca89

File tree

5 files changed

+44
-16
lines changed

5 files changed

+44
-16
lines changed

NEWS

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ CHANGES
1717
* When list-tests encounters an error, a much clearer response will
1818
now be shown. (Robert Collins, #1271133)
1919

20+
INTERNALS
21+
---------
22+
23+
* ``UI.output_stream`` is now tested for handling of non-utf8 bytestreams.
24+
(Robert Collins)
25+
2026
0.0.18
2127
++++++
2228

testrepository/tests/test_setup.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_bdist(self):
3535
proc = subprocess.Popen([sys.executable, path, 'bdist'],
3636
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3737
stderr=subprocess.STDOUT, universal_newlines=True)
38-
output, _ = proc.communicate()
38+
output, err = proc.communicate()
3939
self.assertThat(output, MatchesAny(
4040
# win32
4141
DocTestMatches("""...
@@ -48,4 +48,5 @@ def test_bdist(self):
4848
...bin/testr ...
4949
""", doctest.ELLIPSIS)
5050
))
51-
self.assertEqual(0, proc.returncode)
51+
self.assertEqual(0, proc.returncode,
52+
"Setup failed out=%r err=%r" % (output, err))

testrepository/tests/test_ui.py

+5
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ def test_output_stream(self):
119119
ui = self.get_test_ui()
120120
ui.output_stream(BytesIO())
121121

122+
def test_output_stream_non_utf8(self):
123+
# When the stream has non-utf8 bytes it still outputs correctly.
124+
ui = self.get_test_ui()
125+
ui.output_stream(BytesIO(_b('\xfa')))
126+
122127
def test_output_table(self):
123128
# output_table shows a table.
124129
ui = self.get_test_ui()

testrepository/tests/ui/test_cli.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ def test_outputs_error_string(self):
109109
except Exception:
110110
err_tuple = sys.exc_info()
111111
expected = str(err_tuple[1]) + '\n'
112-
stdout = StringIO()
112+
bytestream = BytesIO()
113+
stdout = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
113114
stdin = StringIO()
114115
stderr = StringIO()
115116
ui = cli.UI([], stdin, stdout, stderr)
@@ -128,6 +129,9 @@ def test_error_enters_pdb_when_TESTR_PDB_set(self):
128129
<BLANKLINE>
129130
fooo
130131
""")
132+
# This should be a BytesIO + Textwrapper, but pdb on 2.7 writes bytes
133+
# - this code is the most pragmatic to test on 2.6 and up, and 3.2 and
134+
# up.
131135
stdout = StringIO()
132136
stdin = StringIO(_u('c\n'))
133137
stderr = StringIO()
@@ -196,7 +200,8 @@ def test_outputs_summary_to_stdout(self):
196200
ui._stdout.buffer.getvalue())
197201

198202
def test_parse_error_goes_to_stderr(self):
199-
stdout = StringIO()
203+
bytestream = BytesIO()
204+
stdout = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
200205
stdin = StringIO()
201206
stderr = StringIO()
202207
ui = cli.UI(['one'], stdin, stdout, stderr)
@@ -206,7 +211,8 @@ def test_parse_error_goes_to_stderr(self):
206211
self.assertEqual("Could not find command 'one'.\n", stderr.getvalue())
207212

208213
def test_parse_excess_goes_to_stderr(self):
209-
stdout = StringIO()
214+
bytestream = BytesIO()
215+
stdout = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
210216
stdin = StringIO()
211217
stderr = StringIO()
212218
ui = cli.UI(['one'], stdin, stdout, stderr)
@@ -248,7 +254,8 @@ def test_run_subunit_option(self):
248254
self.assertEqual(True, ui.options.subunit)
249255

250256
def test_dash_dash_help_shows_help(self):
251-
stdout = StringIO()
257+
bytestream = BytesIO()
258+
stdout = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
252259
stdin = StringIO()
253260
stderr = StringIO()
254261
ui = cli.UI(['--help'], stdin, stdout, stderr)
@@ -263,7 +270,7 @@ def test_dash_dash_help_shows_help(self):
263270
self.assertThat(exc_info, MatchesException(SystemExit(0)))
264271
else:
265272
self.fail('ui.set_command did not raise')
266-
self.assertThat(stdout.getvalue(),
273+
self.assertThat(bytestream.getvalue().decode('utf8'),
267274
DocTestMatches("""Usage: run.py bar [options] foo
268275
...
269276
A command that can be run...
@@ -352,9 +359,11 @@ def make_result(self, stream=None, argv=None, filter_tags=None):
352359
def test_initial_stream(self):
353360
# CLITestResult.__init__ does not do anything to the stream it is
354361
# given.
355-
stream = StringIO()
356-
cli.CLITestResult(cli.UI(None, None, None, None), stream, lambda: None)
357-
self.assertEqual('', stream.getvalue())
362+
bytestream = BytesIO()
363+
stream = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
364+
ui = cli.UI(None, None, None, None)
365+
cli.CLITestResult(ui, stream, lambda: None)
366+
self.assertEqual(_b(''), bytestream.getvalue())
358367

359368
def test_format_error(self):
360369
# CLITestResult formats errors by giving them a big fat line, a title
@@ -376,7 +385,8 @@ def test_format_error_includes_tags(self):
376385
def test_addFail_outputs_error(self):
377386
# CLITestResult.status test_status='fail' outputs the given error
378387
# immediately to the stream.
379-
stream = StringIO()
388+
bytestream = BytesIO()
389+
stream = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
380390
result = self.make_result(stream)[0]
381391
error = self.make_exc_info()
382392
error_text = 'foo\nbar\n'
@@ -385,7 +395,7 @@ def test_addFail_outputs_error(self):
385395
file_name='traceback', mime_type='text/plain;charset=utf8',
386396
file_bytes=error_text.encode('utf8'))
387397
self.assertThat(
388-
stream.getvalue(),
398+
bytestream.getvalue().decode('utf8'),
389399
DocTestMatches(result._format_error('FAIL', self, error_text)))
390400

391401
def test_addFailure_handles_string_encoding(self):
@@ -412,7 +422,8 @@ def test_subunit_output(self):
412422
self.assertEqual(b'', bytestream.getvalue())
413423

414424
def test_make_result_tag_filter(self):
415-
stream = StringIO()
425+
bytestream = BytesIO()
426+
stream = TextIOWrapper(bytestream, 'utf8', line_buffering=True)
416427
result, summary = self.make_result(
417428
stream, filter_tags=set(['worker-0']))
418429
# Generate a bunch of results with tags in the same events that
@@ -438,5 +449,5 @@ def test_make_result_tag_filter(self):
438449
----------------------------------------------------------------------
439450
Ran 1 tests
440451
FAILED (id=None, failures=1, skips=1)
441-
""", stream.getvalue())
452+
""", bytestream.getvalue().decode('utf8'))
442453

testrepository/ui/cli.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def __init__(self, argv, stdin, stdout, stderr):
9393
self._stdin = stdin
9494
self._stdout = stdout
9595
self._stderr = stderr
96+
self._binary_stdout = None
9697

9798
def _iter_streams(self, stream_type):
9899
# Only the first stream declared in a command can be accepted at the
@@ -155,13 +156,17 @@ def output_rest(self, rest_string):
155156
self._stdout.write(_u('\n'))
156157

157158
def output_stream(self, stream):
159+
if not self._binary_stdout:
160+
self._binary_stdout = subunit.make_stream_binary(self._stdout)
158161
contents = stream.read(65536)
159162
assert type(contents) is bytes, \
160163
"Bad stream contents %r" % type(contents)
161-
# Outputs bytes, treat them as utf8. Probably needs fixing.
164+
# If there are unflushed bytes in the text wrapper, we need to sync..
165+
self._stdout.flush()
162166
while contents:
163-
self._stdout.write(contents.decode('utf8'))
167+
self._binary_stdout.write(contents)
164168
contents = stream.read(65536)
169+
self._binary_stdout.flush()
165170

166171
def output_table(self, table):
167172
# stringify

0 commit comments

Comments
 (0)