8b3c04a4aa
* add NamedTemporaryFile() wrapper * use byte strings for binary data in tests * tox.ini: run test_splice on Python 3.4 and Python 3.5 Change-Id: I042d739fbf29433733bf0c2154749bc90b210416
325 lines
10 KiB
Python
325 lines
10 KiB
Python
# Copyright (c) 2014 OpenStack Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
'''Tests for `swift.common.splice`'''
|
|
|
|
import os
|
|
import errno
|
|
import ctypes
|
|
import logging
|
|
import tempfile
|
|
import unittest
|
|
import contextlib
|
|
import re
|
|
|
|
import mock
|
|
import nose
|
|
import six
|
|
|
|
from swift.common.splice import splice, tee
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def NamedTemporaryFile():
|
|
'''Wrapper to tempfile.NamedTemporaryFile() disabling bufferring.
|
|
|
|
The wrapper is used to support Python 2 and Python 3 in the same
|
|
code base.
|
|
'''
|
|
|
|
if six.PY3:
|
|
return tempfile.NamedTemporaryFile(buffering=0)
|
|
else:
|
|
return tempfile.NamedTemporaryFile(bufsize=0)
|
|
|
|
|
|
def safe_close(fd):
|
|
'''Close a file descriptor, ignoring any exceptions'''
|
|
|
|
try:
|
|
os.close(fd)
|
|
except Exception:
|
|
LOGGER.exception('Error while closing FD')
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def pipe():
|
|
'''Context-manager providing 2 ends of a pipe, closing them at exit'''
|
|
|
|
fds = os.pipe()
|
|
|
|
try:
|
|
yield fds
|
|
finally:
|
|
safe_close(fds[0])
|
|
safe_close(fds[1])
|
|
|
|
|
|
class TestSplice(unittest.TestCase):
|
|
'''Tests for `splice`'''
|
|
|
|
def setUp(self):
|
|
if not splice.available:
|
|
raise nose.SkipTest('splice not available')
|
|
|
|
def test_flags(self):
|
|
'''Test flag attribute availability'''
|
|
|
|
self.assertTrue(hasattr(splice, 'SPLICE_F_MOVE'))
|
|
self.assertTrue(hasattr(splice, 'SPLICE_F_NONBLOCK'))
|
|
self.assertTrue(hasattr(splice, 'SPLICE_F_MORE'))
|
|
self.assertTrue(hasattr(splice, 'SPLICE_F_GIFT'))
|
|
|
|
@mock.patch('swift.common.splice.splice._c_splice', None)
|
|
def test_available(self):
|
|
'''Test `available` attribute correctness'''
|
|
|
|
self.assertFalse(splice.available)
|
|
|
|
def test_splice_pipe_to_pipe(self):
|
|
'''Test `splice` from a pipe to a pipe'''
|
|
|
|
with pipe() as (p1a, p1b):
|
|
with pipe() as (p2a, p2b):
|
|
os.write(p1b, b'abcdef')
|
|
res = splice(p1a, None, p2b, None, 3, 0)
|
|
self.assertEqual(res, (3, None, None))
|
|
self.assertEqual(os.read(p2a, 3), b'abc')
|
|
self.assertEqual(os.read(p1a, 3), b'def')
|
|
|
|
def test_splice_file_to_pipe(self):
|
|
'''Test `splice` from a file to a pipe'''
|
|
|
|
with NamedTemporaryFile() as fd:
|
|
with pipe() as (pa, pb):
|
|
fd.write(b'abcdef')
|
|
fd.seek(0, os.SEEK_SET)
|
|
|
|
res = splice(fd, None, pb, None, 3, 0)
|
|
self.assertEqual(res, (3, None, None))
|
|
# `fd.tell()` isn't updated...
|
|
self.assertEqual(os.lseek(fd.fileno(), 0, os.SEEK_CUR), 3)
|
|
|
|
fd.seek(0, os.SEEK_SET)
|
|
res = splice(fd, 3, pb, None, 3, 0)
|
|
self.assertEqual(res, (3, 6, None))
|
|
self.assertEqual(os.lseek(fd.fileno(), 0, os.SEEK_CUR), 0)
|
|
|
|
self.assertEqual(os.read(pa, 6), b'abcdef')
|
|
|
|
def test_splice_pipe_to_file(self):
|
|
'''Test `splice` from a pipe to a file'''
|
|
|
|
with NamedTemporaryFile() as fd:
|
|
with pipe() as (pa, pb):
|
|
os.write(pb, b'abcdef')
|
|
|
|
res = splice(pa, None, fd, None, 3, 0)
|
|
self.assertEqual(res, (3, None, None))
|
|
self.assertEqual(fd.tell(), 3)
|
|
|
|
fd.seek(0, os.SEEK_SET)
|
|
|
|
res = splice(pa, None, fd, 3, 3, 0)
|
|
self.assertEqual(res, (3, None, 6))
|
|
self.assertEqual(fd.tell(), 0)
|
|
|
|
self.assertEqual(fd.read(6), b'abcdef')
|
|
|
|
@mock.patch.object(splice, '_c_splice')
|
|
def test_fileno(self, mock_splice):
|
|
'''Test handling of file-descriptors'''
|
|
|
|
splice(1, None, 2, None, 3, 0)
|
|
self.assertEqual(mock_splice.call_args,
|
|
((1, None, 2, None, 3, 0), {}))
|
|
|
|
mock_splice.reset_mock()
|
|
|
|
with open('/dev/zero', 'r') as fd:
|
|
splice(fd, None, fd, None, 3, 0)
|
|
self.assertEqual(mock_splice.call_args,
|
|
((fd.fileno(), None, fd.fileno(), None, 3, 0),
|
|
{}))
|
|
|
|
@mock.patch.object(splice, '_c_splice')
|
|
def test_flags_list(self, mock_splice):
|
|
'''Test handling of flag lists'''
|
|
|
|
splice(1, None, 2, None, 3,
|
|
[splice.SPLICE_F_MOVE, splice.SPLICE_F_NONBLOCK])
|
|
|
|
flags = splice.SPLICE_F_MOVE | splice.SPLICE_F_NONBLOCK
|
|
self.assertEqual(mock_splice.call_args,
|
|
((1, None, 2, None, 3, flags), {}))
|
|
|
|
mock_splice.reset_mock()
|
|
|
|
splice(1, None, 2, None, 3, [])
|
|
self.assertEqual(mock_splice.call_args,
|
|
((1, None, 2, None, 3, 0), {}))
|
|
|
|
def test_errno(self):
|
|
'''Test handling of failures'''
|
|
|
|
# Invoke EBADF by using a read-only FD as fd_out
|
|
with open('/dev/null', 'r') as fd:
|
|
err = errno.EBADF
|
|
msg = r'\[Errno %d\] splice: %s' % (err, os.strerror(err))
|
|
try:
|
|
splice(fd, None, fd, None, 3, 0)
|
|
except IOError as e:
|
|
self.assertTrue(re.match(msg, str(e)))
|
|
else:
|
|
self.fail('Expected IOError was not raised')
|
|
|
|
self.assertEqual(ctypes.get_errno(), 0)
|
|
|
|
@mock.patch('swift.common.splice.splice._c_splice', None)
|
|
def test_unavailable(self):
|
|
'''Test exception when unavailable'''
|
|
|
|
self.assertRaises(EnvironmentError, splice, 1, None, 2, None, 2, 0)
|
|
|
|
def test_unavailable_in_libc(self):
|
|
'''Test `available` attribute when `libc` has no `splice` support'''
|
|
|
|
class LibC(object):
|
|
'''A fake `libc` object tracking `splice` attribute access'''
|
|
|
|
def __init__(self):
|
|
self.splice_retrieved = False
|
|
|
|
@property
|
|
def splice(self):
|
|
self.splice_retrieved = True
|
|
raise AttributeError
|
|
|
|
libc = LibC()
|
|
mock_cdll = mock.Mock(return_value=libc)
|
|
|
|
with mock.patch('ctypes.CDLL', new=mock_cdll):
|
|
# Force re-construction of a `Splice` instance
|
|
# Something you're not supposed to do in actual code
|
|
new_splice = type(splice)()
|
|
self.assertFalse(new_splice.available)
|
|
|
|
libc_name = ctypes.util.find_library('c')
|
|
|
|
mock_cdll.assert_called_once_with(libc_name, use_errno=True)
|
|
self.assertTrue(libc.splice_retrieved)
|
|
|
|
|
|
class TestTee(unittest.TestCase):
|
|
'''Tests for `tee`'''
|
|
|
|
def setUp(self):
|
|
if not tee.available:
|
|
raise nose.SkipTest('tee not available')
|
|
|
|
@mock.patch('swift.common.splice.tee._c_tee', None)
|
|
def test_available(self):
|
|
'''Test `available` attribute correctness'''
|
|
|
|
self.assertFalse(tee.available)
|
|
|
|
def test_tee_pipe_to_pipe(self):
|
|
'''Test `tee` from a pipe to a pipe'''
|
|
|
|
with pipe() as (p1a, p1b):
|
|
with pipe() as (p2a, p2b):
|
|
os.write(p1b, b'abcdef')
|
|
res = tee(p1a, p2b, 3, 0)
|
|
self.assertEqual(res, 3)
|
|
self.assertEqual(os.read(p2a, 3), b'abc')
|
|
self.assertEqual(os.read(p1a, 6), b'abcdef')
|
|
|
|
@mock.patch.object(tee, '_c_tee')
|
|
def test_fileno(self, mock_tee):
|
|
'''Test handling of file-descriptors'''
|
|
|
|
with pipe() as (pa, pb):
|
|
tee(pa, pb, 3, 0)
|
|
self.assertEqual(mock_tee.call_args, ((pa, pb, 3, 0), {}))
|
|
|
|
mock_tee.reset_mock()
|
|
|
|
tee(os.fdopen(pa, 'r'), os.fdopen(pb, 'w'), 3, 0)
|
|
self.assertEqual(mock_tee.call_args, ((pa, pb, 3, 0), {}))
|
|
|
|
@mock.patch.object(tee, '_c_tee')
|
|
def test_flags_list(self, mock_tee):
|
|
'''Test handling of flag lists'''
|
|
|
|
tee(1, 2, 3, [splice.SPLICE_F_MOVE | splice.SPLICE_F_NONBLOCK])
|
|
flags = splice.SPLICE_F_MOVE | splice.SPLICE_F_NONBLOCK
|
|
self.assertEqual(mock_tee.call_args, ((1, 2, 3, flags), {}))
|
|
|
|
mock_tee.reset_mock()
|
|
|
|
tee(1, 2, 3, [])
|
|
self.assertEqual(mock_tee.call_args, ((1, 2, 3, 0), {}))
|
|
|
|
def test_errno(self):
|
|
'''Test handling of failures'''
|
|
|
|
# Invoke EBADF by using a read-only FD as fd_out
|
|
with open('/dev/null', 'r') as fd:
|
|
err = errno.EBADF
|
|
msg = r'\[Errno %d\] tee: %s' % (err, os.strerror(err))
|
|
try:
|
|
tee(fd, fd, 3, 0)
|
|
except IOError as e:
|
|
self.assertTrue(re.match(msg, str(e)))
|
|
else:
|
|
self.fail('Expected IOError was not raised')
|
|
|
|
self.assertEqual(ctypes.get_errno(), 0)
|
|
|
|
@mock.patch('swift.common.splice.tee._c_tee', None)
|
|
def test_unavailable(self):
|
|
'''Test exception when unavailable'''
|
|
|
|
self.assertRaises(EnvironmentError, tee, 1, 2, 2, 0)
|
|
|
|
def test_unavailable_in_libc(self):
|
|
'''Test `available` attribute when `libc` has no `tee` support'''
|
|
|
|
class LibC(object):
|
|
'''A fake `libc` object tracking `tee` attribute access'''
|
|
|
|
def __init__(self):
|
|
self.tee_retrieved = False
|
|
|
|
@property
|
|
def tee(self):
|
|
self.tee_retrieved = True
|
|
raise AttributeError
|
|
|
|
libc = LibC()
|
|
mock_cdll = mock.Mock(return_value=libc)
|
|
|
|
with mock.patch('ctypes.CDLL', new=mock_cdll):
|
|
# Force re-construction of a `Tee` instance
|
|
# Something you're not supposed to do in actual code
|
|
new_tee = type(tee)()
|
|
self.assertFalse(new_tee.available)
|
|
|
|
libc_name = ctypes.util.find_library('c')
|
|
|
|
mock_cdll.assert_called_once_with(libc_name, use_errno=True)
|
|
self.assertTrue(libc.tee_retrieved)
|