Skip to content

Commit 7bf91e6

Browse files
committed
Add failing tests.
1 parent 35b1cc1 commit 7bf91e6

File tree

5 files changed

+217
-4
lines changed

5 files changed

+217
-4
lines changed

src/pip/_internal/utils/logging.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
_log_state.indentation = 0
2727

2828

29+
class BrokenStdoutLoggingError(Exception):
30+
"""
31+
Raised if BrokenPipeError occurs for the stdout stream while logging.
32+
"""
33+
pass
34+
35+
2936
@contextlib.contextmanager
3037
def indent_log(num=2):
3138
"""

src/pip/_internal/utils/misc.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,13 @@ def captured_stdout():
864864
return captured_output('stdout')
865865

866866

867+
def captured_stderr():
868+
"""
869+
See captured_stdout().
870+
"""
871+
return captured_output('stderr')
872+
873+
867874
class cached_property(object):
868875
"""A property that is only computed once per instance and then replaces
869876
itself with an ordinary attribute. Deleting the attribute resets the
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import subprocess
2+
import sys
3+
4+
import pytest
5+
6+
from pip._internal.utils.compat import WINDOWS
7+
8+
if sys.version_info < (3, 6):
9+
_BROKEN_STDOUT_RETURN_CODE = 1
10+
else:
11+
_BROKEN_STDOUT_RETURN_CODE = 120
12+
13+
14+
def setup_broken_stdout_test(args):
15+
proc = subprocess.Popen(
16+
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
17+
)
18+
# Call close() on stdout to cause a broken pipe.
19+
proc.stdout.close()
20+
# This line causes a timeout on Windows.
21+
returncode = proc.wait()
22+
stderr = proc.stderr.read().decode('utf-8')
23+
24+
if sys.version_info < (3, 5):
25+
# In Python 3.4, stderr starts instead with--
26+
# "DEPRECATION: Python 3.4 support has been deprecated..."
27+
assert 'ERROR: Pipe to stdout was broken' in stderr
28+
else:
29+
assert stderr.startswith('ERROR: Pipe to stdout was broken')
30+
31+
return stderr, returncode
32+
33+
34+
@pytest.mark.skipif(WINDOWS, reason="test times out on Windows")
35+
def test_broken_stdout_pipe():
36+
"""
37+
Test a broken pipe to stdout.
38+
"""
39+
stderr, returncode = setup_broken_stdout_test(['pip', 'list'])
40+
41+
# Check that no traceback occurs.
42+
assert 'raise BrokenStdoutLoggingError()' not in stderr
43+
assert stderr.count('Traceback') == 0
44+
45+
assert returncode == _BROKEN_STDOUT_RETURN_CODE
46+
47+
48+
@pytest.mark.skipif(WINDOWS, reason="test times out on Windows")
49+
def test_broken_stdout_pipe__verbose():
50+
"""
51+
Test a broken pipe to stdout with verbose logging enabled.
52+
"""
53+
stderr, returncode = setup_broken_stdout_test(['pip', '-v', 'list'])
54+
55+
# Check that a traceback occurs and that it occurs at most once.
56+
# We permit up to two because the exception can be chained.
57+
assert 'raise BrokenStdoutLoggingError()' in stderr
58+
assert 1 <= stderr.count('Traceback') <= 2
59+
60+
assert returncode == _BROKEN_STDOUT_RETURN_CODE

tests/unit/test_base_command.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import logging
22

33
from pip._internal.cli.base_command import Command
4+
from pip._internal.utils.logging import BrokenStdoutLoggingError
45

56

67
class FakeCommand(Command):
78
name = 'fake'
89
summary = name
910

10-
def __init__(self, error=False):
11-
self.error = error
11+
def __init__(self, run_func=None, error=False):
12+
if error:
13+
def run_func():
14+
raise SystemExit(1)
15+
16+
self.run_func = run_func
1217
super(FakeCommand, self).__init__()
1318

1419
def main(self, args):
@@ -17,8 +22,8 @@ def main(self, args):
1722

1823
def run(self, options, args):
1924
logging.getLogger("pip.tests").info("fake")
20-
if self.error:
21-
raise SystemExit(1)
25+
if self.run_func:
26+
return self.run_func()
2227

2328

2429
class FakeCommandWithUnicode(FakeCommand):
@@ -32,6 +37,40 @@ def run(self, options, args):
3237
)
3338

3439

40+
class TestCommand(object):
41+
42+
def call_main(self, capsys, args):
43+
"""
44+
Call command.main(), and return the command's stderr.
45+
"""
46+
def raise_broken_stdout():
47+
raise BrokenStdoutLoggingError()
48+
49+
cmd = FakeCommand(run_func=raise_broken_stdout)
50+
status = cmd.main(args)
51+
assert status == 1
52+
stderr = capsys.readouterr().err
53+
54+
return stderr
55+
56+
def test_raise_broken_stdout(self, capsys):
57+
"""
58+
Test raising BrokenStdoutLoggingError.
59+
"""
60+
stderr = self.call_main(capsys, [])
61+
62+
assert stderr.rstrip() == 'ERROR: Pipe to stdout was broken'
63+
64+
def test_raise_broken_stdout__debug_logging(self, capsys):
65+
"""
66+
Test raising BrokenStdoutLoggingError with debug logging enabled.
67+
"""
68+
stderr = self.call_main(capsys, ['-v'])
69+
70+
assert 'ERROR: Pipe to stdout was broken' in stderr
71+
assert 'Traceback (most recent call last):' in stderr
72+
73+
3574
class Test_base_command_logging(object):
3675
"""
3776
Test `pip.base_command.Command` setting up logging consumers based on

tests/unit/test_logging.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import errno
2+
import logging
3+
import sys
4+
5+
import pytest
6+
from mock import patch
7+
from pip._vendor.six import PY2
8+
9+
from pip._internal.utils.logging import (
10+
BrokenStdoutLoggingError, ColorizedStreamHandler,
11+
)
12+
from pip._internal.utils.misc import captured_stderr, captured_stdout
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
# This is a Python 2/3 compatibility helper.
18+
def _make_broken_pipe_error():
19+
"""
20+
Return an exception object for a broken pipe.
21+
"""
22+
if sys.version_info[0] == 2:
23+
# This is how BrokenPipeError shows up in Python 2.
24+
return IOError(errno.EPIPE, 'Broken pipe')
25+
26+
return BrokenPipeError() # noqa: F821
27+
28+
29+
class TestColorizedStreamHandler(object):
30+
31+
def _make_log_record(self):
32+
attrs = {
33+
'msg': 'my error',
34+
}
35+
record = logging.makeLogRecord(attrs)
36+
37+
return record
38+
39+
def test_broken_pipe_in_stderr_flush(self):
40+
"""
41+
Test sys.stderr.flush() raising BrokenPipeError.
42+
43+
This error should _not_ trigger an error in the logging framework.
44+
"""
45+
record = self._make_log_record()
46+
47+
with captured_stderr() as stderr:
48+
handler = ColorizedStreamHandler(stream=stderr)
49+
with patch('sys.stderr.flush') as mock_flush:
50+
mock_flush.side_effect = _make_broken_pipe_error()
51+
# The emit() call raises no exception.
52+
handler.emit(record)
53+
54+
err_text = stderr.getvalue()
55+
56+
assert err_text.startswith('my error')
57+
# Check that the logging framework tried to log the exception.
58+
if PY2:
59+
assert 'IOError: [Errno 32] Broken pipe' in err_text
60+
assert 'Logged from file' in err_text
61+
else:
62+
assert 'Logging error' in err_text
63+
assert 'BrokenPipeError' in err_text
64+
assert "Message: 'my error'" in err_text
65+
66+
def test_broken_pipe_in_stdout_write(self):
67+
"""
68+
Test sys.stdout.write() raising BrokenPipeError.
69+
70+
This error _should_ trigger an error in the logging framework.
71+
"""
72+
record = self._make_log_record()
73+
74+
with captured_stdout() as stdout:
75+
handler = ColorizedStreamHandler(stream=stdout)
76+
with patch('sys.stdout.write') as mock_write:
77+
mock_write.side_effect = _make_broken_pipe_error()
78+
with pytest.raises(BrokenStdoutLoggingError):
79+
handler.emit(record)
80+
81+
def test_broken_pipe_in_stdout_flush(self):
82+
"""
83+
Test sys.stdout.flush() raising BrokenPipeError.
84+
85+
This error _should_ trigger an error in the logging framework.
86+
"""
87+
record = self._make_log_record()
88+
89+
with captured_stdout() as stdout:
90+
handler = ColorizedStreamHandler(stream=stdout)
91+
with patch('sys.stdout.flush') as mock_flush:
92+
mock_flush.side_effect = _make_broken_pipe_error()
93+
with pytest.raises(BrokenStdoutLoggingError):
94+
handler.emit(record)
95+
96+
output = stdout.getvalue()
97+
98+
# Sanity check that the log record was written, since flush() happens
99+
# after write().
100+
assert output.startswith('my error')

0 commit comments

Comments
 (0)