diff --git a/flake8/reporter.py b/flake8/reporter.py index 5fc2743..1df3d9e 100644 --- a/flake8/reporter.py +++ b/flake8/reporter.py @@ -2,6 +2,7 @@ # Adapted from a contribution of Johan Dahlin import collections +import errno import re import sys try: @@ -18,6 +19,20 @@ class BaseQReport(pep8.BaseReport): """Base Queue Report.""" _loaded = False # Windows support + # Reasoning for ignored error numbers is in-line below + ignored_errors = set([ + # EPIPE: Added by sigmavirus24 + # > If output during processing is piped to something that may close + # > its own stdin before we've finished printing results, we need to + # > catch a Broken pipe error and continue on. + # > (See also: https://gitlab.com/pycqa/flake8/issues/69) + errno.EPIPE, + # NOTE(sigmavirus24): When adding to this list, include the reasoning + # on the lines before the error code and always append your error + # code. Further, please always add a trailing `,` to reduce the visual + # noise in diffs. + ]) + def __init__(self, options): assert options.jobs > 0 super(BaseQReport, self).__init__(options) @@ -74,6 +89,11 @@ class BaseQReport(pep8.BaseReport): self._process_main() except KeyboardInterrupt: pass + except IOError as ioerr: + # If we happen across an IOError that we aren't certain can/should + # be ignored, we should re-raise the exception. + if ioerr.errno not in self.ignored_errors: + raise finally: # ensure all output is flushed before main process continues sys.stdout.flush() diff --git a/flake8/tests/test_reporter.py b/flake8/tests/test_reporter.py new file mode 100644 index 0000000..f91bb52 --- /dev/null +++ b/flake8/tests/test_reporter.py @@ -0,0 +1,36 @@ +from __future__ import with_statement + +import errno +import unittest +try: + from unittest import mock +except ImportError: + import mock # < PY33 + +from flake8 import reporter + + +def ioerror_report_factory(errno_code): + class IOErrorBaseQReport(reporter.BaseQReport): + def _process_main(self): + raise IOError(errno_code, 'Fake bad pipe exception') + + options = mock.MagicMock() + options.jobs = 2 + return IOErrorBaseQReport(options) + + +class TestBaseQReport(unittest.TestCase): + def test_does_not_raise_a_bad_pipe_ioerror(self): + """Test that no EPIPE IOError exception is re-raised or leaked.""" + report = ioerror_report_factory(errno.EPIPE) + try: + report.process_main() + except IOError: + self.fail('BaseQReport.process_main raised an IOError for EPIPE' + ' but it should have caught this exception.') + + def test_raises_a_enoent_ioerror(self): + """Test that an ENOENT IOError exception is re-raised.""" + report = ioerror_report_factory(errno.ENOENT) + self.assertRaises(IOError, report.process_main)