Add tests around the OSError retry logic This updates our retry logic to be more specific in catching OSErrors and it adds specific tests to show that it works and properly re-initializes the StyleGuide with the pep8.StandardReport class so we can fall back on serial behaviour gracefully. Closes #74
diff --git a/flake8/engine.py b/flake8/engine.py index 9cbdf6d..0f440c1 100644 --- a/flake8/engine.py +++ b/flake8/engine.py
@@ -120,7 +120,8 @@ ]) def __init__(self, **kwargs): - self._styleguide = NoQAStyleGuide(**kwargs) + # This allows us to inject a mocked StyleGuide in the tests. + self._styleguide = kwargs.pop('styleguide', NoQAStyleGuide(**kwargs)) @property def options(self): @@ -139,8 +140,8 @@ """ try: return func(*args, **kwargs) - except IOError as ioerr: - if ioerr.errno in self.serial_retry_errors: + except OSError as oserr: + if oserr.errno in self.serial_retry_errors: self.init_report(pep8.StandardReport) else: raise @@ -161,7 +162,7 @@ filename=filename, lines=lines, expected=expected, - line_offset=0, + line_offset=line_offset, )
diff --git a/flake8/tests/test_engine.py b/flake8/tests/test_engine.py index 60b388c..a6faab5 100644 --- a/flake8/tests/test_engine.py +++ b/flake8/tests/test_engine.py
@@ -1,5 +1,6 @@ from __future__ import with_statement +import errno import unittest try: from unittest import mock @@ -7,6 +8,7 @@ import mock # < PY33 from flake8 import engine, util, __version__, reporter +import pep8 class TestEngine(unittest.TestCase): @@ -112,5 +114,87 @@ assert 'X' not in sg.options.ignore +def oserror_generator(error_number, message='Ominous OSError message'): + def oserror_side_effect(*args, **kwargs): + if hasattr(oserror_side_effect, 'used'): + return + + oserror_side_effect.used = True + raise OSError(error_number, message) + + return oserror_side_effect + + +class TestStyleGuide(unittest.TestCase): + def setUp(self): + mocked_styleguide = mock.Mock(spec=engine.NoQAStyleGuide) + self.styleguide = engine.StyleGuide(styleguide=mocked_styleguide) + self.mocked_sg = mocked_styleguide + + def test_proxies_excluded(self): + self.styleguide.excluded('file.py', parent='.') + + self.mocked_sg.excluded.assert_called_once_with('file.py', parent='.') + + def test_proxies_init_report(self): + reporter = object() + self.styleguide.init_report(reporter) + + self.mocked_sg.init_report.assert_called_once_with(reporter) + + def test_proxies_check_files(self): + self.styleguide.check_files(['foo', 'bar']) + + self.mocked_sg.check_files.assert_called_once_with( + paths=['foo', 'bar'] + ) + + def test_proxies_input_file(self): + self.styleguide.input_file('file.py', + lines=[9, 10], + expected='foo', + line_offset=20) + + self.mocked_sg.input_file.assert_called_once_with(filename='file.py', + lines=[9, 10], + expected='foo', + line_offset=20) + + def test_check_files_retries_on_specific_OSErrors(self): + self.mocked_sg.check_files.side_effect = oserror_generator( + errno.ENOSPC, 'No space left on device' + ) + + self.styleguide.check_files(['foo', 'bar']) + + self.mocked_sg.init_report.assert_called_once_with(pep8.StandardReport) + + def test_input_file_retries_on_specific_OSErrors(self): + self.mocked_sg.input_file.side_effect = oserror_generator( + errno.ENOSPC, 'No space left on device' + ) + + self.styleguide.input_file('file.py') + + self.mocked_sg.init_report.assert_called_once_with(pep8.StandardReport) + + def test_check_files_reraises_unknown_OSErrors(self): + self.mocked_sg.check_files.side_effect = oserror_generator( + errno.EADDRINUSE, + 'lol why are we talking about binding to sockets' + ) + + self.assertRaises(OSError, self.styleguide.check_files, + ['foo', 'bar']) + + def test_input_file_reraises_unknown_OSErrors(self): + self.mocked_sg.input_file.side_effect = oserror_generator( + errno.EADDRINUSE, + 'lol why are we talking about binding to sockets' + ) + + self.assertRaises(OSError, self.styleguide.input_file, + ['foo', 'bar']) + if __name__ == '__main__': unittest.main()