from __future__ import print_function, absolute_import import functools import gc import io import os import sys import tempfile import unittest import gevent from gevent import fileobject from gevent._fileobjectcommon import OpenDescriptor from gevent._compat import PY2 from gevent._compat import PY3 from gevent._compat import text_type import gevent.testing as greentest from gevent.testing import sysinfo try: ResourceWarning except NameError: class ResourceWarning(Warning): "Python 2 fallback" def Writer(fobj, line): for character in line: fobj.write(character) fobj.flush() fobj.close() def close_fd_quietly(fd): try: os.close(fd) except (IOError, OSError): pass def skipUnlessWorksWithRegularFiles(func): @functools.wraps(func) def f(self): if not self.WORKS_WITH_REGULAR_FILES: self.skipTest("Doesn't work with regular files") func(self) return f class TestFileObjectBlock(greentest.TestCase): # serves as a base for the concurrent tests too WORKS_WITH_REGULAR_FILES = True def _getTargetClass(self): return fileobject.FileObjectBlock def _makeOne(self, *args, **kwargs): return self._getTargetClass()(*args, **kwargs) def _test_del(self, **kwargs): r, w = os.pipe() self.addCleanup(close_fd_quietly, r) self.addCleanup(close_fd_quietly, w) self._do_test_del((r, w), **kwargs) def _do_test_del(self, pipe, **kwargs): r, w = pipe s = self._makeOne(w, 'wb', **kwargs) s.write(b'x') try: s.flush() except IOError: # Sometimes seen on Windows/AppVeyor print("Failed flushing fileobject", repr(s), file=sys.stderr) import traceback traceback.print_exc() import warnings with warnings.catch_warnings(): warnings.simplefilter('ignore', ResourceWarning) # Deliberately getting ResourceWarning with FileObject(Thread) under Py3 del s gc.collect() # PyPy if kwargs.get("close", True): with self.assertRaises((OSError, IOError)): # expected, because FileObject already closed it os.close(w) else: os.close(w) with self._makeOne(r, 'rb') as fobj: self.assertEqual(fobj.read(), b'x') def test_del(self): # Close should be true by default self._test_del() def test_del_close(self): self._test_del(close=True) @skipUnlessWorksWithRegularFiles def test_seek(self): fileno, path = tempfile.mkstemp('.gevent.test__fileobject.test_seek') self.addCleanup(os.remove, path) s = b'a' * 1024 os.write(fileno, b'B' * 15) os.write(fileno, s) os.close(fileno) with open(path, 'rb') as f: f.seek(15) native_data = f.read(1024) with open(path, 'rb') as f_raw: f = self._makeOne(f_raw, 'rb', close=False) if PY3 or hasattr(f, 'seekable'): # On Python 3, all objects should have seekable. # On Python 2, only our custom objects do. self.assertTrue(f.seekable()) f.seek(15) self.assertEqual(15, f.tell()) # Note that a duplicate close() of the underlying # file descriptor can look like an OSError from this line # as we exit the with block fileobj_data = f.read(1024) self.assertEqual(native_data, s) self.assertEqual(native_data, fileobj_data) def __check_native_matches(self, byte_data, open_mode, meth='read', open_path=True, **open_kwargs): fileno, path = tempfile.mkstemp('.gevent_test_' + open_mode) self.addCleanup(os.remove, path) os.write(fileno, byte_data) os.close(fileno) with io.open(path, open_mode, **open_kwargs) as f: native_data = getattr(f, meth)() if open_path: with self._makeOne(path, open_mode, **open_kwargs) as f: gevent_data = getattr(f, meth)() else: # Note that we don't use ``io.open()`` for the raw file, # on Python 2. We want 'r' to mean what the usual call to open() means. opener = io.open if PY3 else open with opener(path, open_mode, **open_kwargs) as raw: with self._makeOne(raw) as f: gevent_data = getattr(f, meth)() self.assertEqual(native_data, gevent_data) return gevent_data @skipUnlessWorksWithRegularFiles def test_str_default_to_native(self): # With no 'b' or 't' given, read and write native str. gevent_data = self.__check_native_matches(b'abcdefg', 'r') self.assertIsInstance(gevent_data, str) @skipUnlessWorksWithRegularFiles def test_text_encoding(self): gevent_data = self.__check_native_matches( u'\N{SNOWMAN}'.encode('utf-8'), 'r+', buffering=5, encoding='utf-8' ) self.assertIsInstance(gevent_data, text_type) @skipUnlessWorksWithRegularFiles def test_does_not_leak_on_exception(self): # If an exception occurs during opening, # everything still gets cleaned up. pass @skipUnlessWorksWithRegularFiles def test_rbU_produces_bytes_readline(self): # Including U in rb still produces bytes. # Note that the universal newline behaviour is # essentially ignored in explicit bytes mode. gevent_data = self.__check_native_matches( b'line1\nline2\r\nline3\rlastline\n\n', 'rbU', meth='readlines', ) self.assertIsInstance(gevent_data[0], bytes) self.assertEqual(len(gevent_data), 4) @skipUnlessWorksWithRegularFiles def test_rU_produces_native(self): gevent_data = self.__check_native_matches( b'line1\nline2\r\nline3\rlastline\n\n', 'rU', meth='readlines', ) self.assertIsInstance(gevent_data[0], str) @skipUnlessWorksWithRegularFiles def test_r_readline_produces_native(self): gevent_data = self.__check_native_matches( b'line1\n', 'r', meth='readline', ) self.assertIsInstance(gevent_data, str) @skipUnlessWorksWithRegularFiles def test_r_readline_on_fobject_produces_native(self): gevent_data = self.__check_native_matches( b'line1\n', 'r', meth='readline', open_path=False, ) self.assertIsInstance(gevent_data, str) def test_close_pipe(self): # Issue #190, 203 r, w = os.pipe() x = self._makeOne(r) y = self._makeOne(w, 'w') x.close() y.close() class ConcurrentFileObjectMixin(object): # Additional tests for fileobjects that cooperate # and we have full control of the implementation def test_read1_binary_present(self): # Issue #840 r, w = os.pipe() reader = self._makeOne(r, 'rb') self._close_on_teardown(reader) writer = self._makeOne(w, 'w') self._close_on_teardown(writer) self.assertTrue(hasattr(reader, 'read1'), dir(reader)) def test_read1_text_not_present(self): # Only defined for binary. r, w = os.pipe() reader = self._makeOne(r, 'rt') self._close_on_teardown(reader) self.addCleanup(os.close, w) self.assertFalse(hasattr(reader, 'read1'), dir(reader)) def test_read1_default(self): # If just 'r' is given, whether it has one or not # depends on if we're Python 2 or 3. r, w = os.pipe() self.addCleanup(os.close, w) reader = self._makeOne(r) self._close_on_teardown(reader) self.assertEqual(PY2, hasattr(reader, 'read1')) def test_bufsize_0(self): # Issue #840 r, w = os.pipe() x = self._makeOne(r, 'rb', bufsize=0) y = self._makeOne(w, 'wb', bufsize=0) self._close_on_teardown(x) self._close_on_teardown(y) y.write(b'a') b = x.read(1) self.assertEqual(b, b'a') y.writelines([b'2']) b = x.read(1) self.assertEqual(b, b'2') def test_newlines(self): import warnings r, w = os.pipe() lines = [b'line1\n', b'line2\r', b'line3\r\n', b'line4\r\nline5', b'\nline6'] g = gevent.spawn(Writer, self._makeOne(w, 'wb'), lines) try: with warnings.catch_warnings(): warnings.simplefilter('ignore', DeprecationWarning) # U is deprecated in Python 3, shows up on FileObjectThread fobj = self._makeOne(r, 'rU') result = fobj.read() fobj.close() self.assertEqual('line1\nline2\nline3\nline4\nline5\nline6', result) finally: g.kill() class TestFileObjectThread(ConcurrentFileObjectMixin, TestFileObjectBlock): def _getTargetClass(self): return fileobject.FileObjectThread def test_del_noclose(self): # In the past, we used os.fdopen() when given a file descriptor, # and that has a destructor that can't be bypassed, so # close=false wasn't allowed. Now that we do everything with the # io module, it is allowed. self._test_del(close=False) # We don't test this with FileObjectThread. Sometimes the # visibility of the 'close' operation, which happens in a # background thread, doesn't make it to the foreground # thread in a timely fashion, leading to 'os.close(4) must # not succeed' in test_del_close. We have the same thing # with flushing and closing in test_newlines. Both of # these are most commonly (only?) observed on Py27/64-bit. # They also appear on 64-bit 3.6 with libuv def test_del(self): raise unittest.SkipTest("Race conditions") def test_del_close(self): raise unittest.SkipTest("Race conditions") @unittest.skipUnless( hasattr(fileobject, 'FileObjectPosix'), "Needs FileObjectPosix" ) class TestFileObjectPosix(ConcurrentFileObjectMixin, TestFileObjectBlock): if sysinfo.LIBUV and sysinfo.LINUX: # On Linux, initializing the watcher for a regular # file results in libuv raising EPERM. But that works # fine on other platforms. WORKS_WITH_REGULAR_FILES = False def _getTargetClass(self): return fileobject.FileObjectPosix def test_seek_raises_ioerror(self): # https://github.com/gevent/gevent/issues/1323 # Get a non-seekable file descriptor r, w = os.pipe() self.addCleanup(close_fd_quietly, r) self.addCleanup(close_fd_quietly, w) with self.assertRaises(OSError) as ctx: os.lseek(r, 0, os.SEEK_SET) os_ex = ctx.exception with self.assertRaises(IOError) as ctx: f = self._makeOne(r, 'r', close=False) # Seek directly using the underlying GreenFileDescriptorIO; # the buffer may do different things, depending # on the version of Python (especially 3.7+) f.fileio.seek(0) io_ex = ctx.exception self.assertEqual(io_ex.errno, os_ex.errno) self.assertEqual(io_ex.strerror, os_ex.strerror) self.assertEqual(io_ex.args, os_ex.args) self.assertEqual(str(io_ex), str(os_ex)) class TestTextMode(unittest.TestCase): def test_default_mode_writes_linesep(self): # See https://github.com/gevent/gevent/issues/1282 # libuv 1.x interferes with the default line mode on # Windows. # First, make sure we initialize gevent gevent.get_hub() fileno, path = tempfile.mkstemp('.gevent.test__fileobject.test_default') self.addCleanup(os.remove, path) os.close(fileno) with open(path, "w") as f: f.write("\n") with open(path, "rb") as f: data = f.read() self.assertEqual(data, os.linesep.encode('ascii')) class TestOpenDescriptor(greentest.TestCase): def _makeOne(self, *args, **kwargs): return OpenDescriptor(*args, **kwargs) def _check(self, regex, kind, *args, **kwargs): with self.assertRaisesRegex(kind, regex): self._makeOne(*args, **kwargs) case = lambda re, **kwargs: (re, TypeError, kwargs) vase = lambda re, **kwargs: (re, ValueError, kwargs) CASES = ( case('mode', mode=42), case('buffering', buffering='nope'), case('encoding', encoding=42), case('errors', errors=42), vase('mode', mode='aoeug'), vase('mode U cannot be combined', mode='wU'), vase('text and binary', mode='rtb'), vase('append mode at once', mode='rw'), vase('exactly one', mode='+'), vase('take an encoding', mode='rb', encoding='ascii'), vase('take an errors', mode='rb', errors='strict'), vase('take a newline', mode='rb', newline='\n'), ) def pop(): for regex, kind, kwargs in TestOpenDescriptor.CASES: setattr( TestOpenDescriptor, 'test_' + regex, lambda self, _re=regex, _kind=kind, _kw=kwargs: self._check(_re, _kind, 1, **_kw) ) pop() if __name__ == '__main__': greentest.main()