Issue 10882: add os.sendfile(). (patch provided by Ross Lagerwall)
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 544eee1..59a2d63 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -15,6 +15,13 @@
 import contextlib
 import mmap
 import uuid
+import asyncore
+import asynchat
+import socket
+try:
+    import threading
+except ImportError:
+    threading = None
 
 # Detect whether we're on a Linux system that uses the (now outdated
 # and unmaintained) linuxthreads threading library.  There's an issue
@@ -1261,6 +1268,251 @@
         self.assertNotEqual(len(user_name), 0)
 
 
+class SendfileTestServer(asyncore.dispatcher, threading.Thread):
+
+    class Handler(asynchat.async_chat):
+
+        def __init__(self, conn):
+            asynchat.async_chat.__init__(self, conn)
+            self.in_buffer = []
+            self.closed = False
+            self.push(b"220 ready\r\n")
+
+        def handle_read(self):
+            data = self.recv(4096)
+            self.in_buffer.append(data)
+
+        def get_data(self):
+            return b''.join(self.in_buffer)
+
+        def handle_close(self):
+            self.close()
+            self.closed = True
+
+        def handle_error(self):
+            raise
+
+    def __init__(self, address):
+        threading.Thread.__init__(self)
+        asyncore.dispatcher.__init__(self)
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.bind(address)
+        self.listen(5)
+        self.host, self.port = self.socket.getsockname()[:2]
+        self.handler_instance = None
+        self._active = False
+        self._active_lock = threading.Lock()
+
+    # --- public API
+
+    @property
+    def running(self):
+        return self._active
+
+    def start(self):
+        assert not self.running
+        self.__flag = threading.Event()
+        threading.Thread.start(self)
+        self.__flag.wait()
+
+    def stop(self):
+        assert self.running
+        self._active = False
+        self.join()
+
+    def wait(self):
+        # wait for handler connection to be closed, then stop the server
+        while not getattr(self.handler_instance, "closed", True):
+            time.sleep(0.001)
+        self.stop()
+
+    # --- internals
+
+    def run(self):
+        self._active = True
+        self.__flag.set()
+        while self._active and asyncore.socket_map:
+            self._active_lock.acquire()
+            asyncore.loop(timeout=0.001, count=1)
+            self._active_lock.release()
+        asyncore.close_all()
+
+    def handle_accept(self):
+        conn, addr = self.accept()
+        self.handler_instance = self.Handler(conn)
+
+    def handle_connect(self):
+        self.close()
+    handle_read = handle_connect
+
+    def writable(self):
+        return 0
+
+    def handle_error(self):
+        raise
+
+
+@unittest.skipUnless(hasattr(os, 'sendfile'), "test needs os.sendfile()")
+class TestSendfile(unittest.TestCase):
+
+    DATA = b"12345abcde" * 1024 * 1024  # 10 Mb
+    SUPPORT_HEADERS_TRAILERS = not sys.platform.startswith("linux") and \
+                               not sys.platform.startswith("solaris")
+
+    @classmethod
+    def setUpClass(cls):
+        with open(support.TESTFN, "wb") as f:
+            f.write(cls.DATA)
+
+    @classmethod
+    def tearDownClass(cls):
+        support.unlink(support.TESTFN)
+
+    def setUp(self):
+        self.server = SendfileTestServer((support.HOST, 0))
+        self.server.start()
+        self.client = socket.socket()
+        self.client.connect((self.server.host, self.server.port))
+        self.client.settimeout(1)
+        # synchronize by waiting for "220 ready" response
+        self.client.recv(1024)
+        self.sockno = self.client.fileno()
+        self.file = open(support.TESTFN, 'rb')
+        self.fileno = self.file.fileno()
+
+    def tearDown(self):
+        self.file.close()
+        self.client.close()
+        if self.server.running:
+            self.server.stop()
+
+    def sendfile_wrapper(self, sock, file, offset, nbytes, headers=[], trailers=[]):
+        """A higher level wrapper representing how an application is
+        supposed to use sendfile().
+        """
+        while 1:
+            try:
+                if self.SUPPORT_HEADERS_TRAILERS:
+                    return os.sendfile(sock, file, offset, nbytes, headers,
+                                       trailers)
+                else:
+                    return os.sendfile(sock, file, offset, nbytes)
+            except OSError as err:
+                if err.errno == errno.ECONNRESET:
+                    # disconnected
+                    raise
+                elif err.errno in (errno.EAGAIN, errno.EBUSY):
+                    # we have to retry send data
+                    continue
+                else:
+                    raise
+
+    def test_send_whole_file(self):
+        # normal send
+        total_sent = 0
+        offset = 0
+        nbytes = 4096
+        while 1:
+            sent = self.sendfile_wrapper(self.sockno, self.fileno, offset, nbytes)
+            if sent == 0:
+                break
+            offset += sent
+            total_sent += sent
+            self.assertTrue(sent <= nbytes)
+            self.assertEqual(offset, total_sent)
+
+        self.assertEqual(total_sent, len(self.DATA))
+        self.client.close()
+        self.server.wait()
+        data = self.server.handler_instance.get_data()
+        self.assertEqual(hash(data), hash(self.DATA))
+
+    def test_send_at_certain_offset(self):
+        # start sending a file at a certain offset
+        total_sent = 0
+        offset = len(self.DATA) / 2
+        nbytes = 4096
+        while 1:
+            sent = self.sendfile_wrapper(self.sockno, self.fileno, offset, nbytes)
+            if sent == 0:
+                break
+            offset += sent
+            total_sent += sent
+            self.assertTrue(sent <= nbytes)
+
+        self.client.close()
+        self.server.wait()
+        data = self.server.handler_instance.get_data()
+        expected = self.DATA[int(len(self.DATA) / 2):]
+        self.assertEqual(total_sent, len(expected))
+        self.assertEqual(hash(data), hash(expected))
+
+    def test_offset_overflow(self):
+        # specify an offset > file size
+        offset = len(self.DATA) + 4096
+        sent = os.sendfile(self.sockno, self.fileno, offset, 4096)
+        self.assertEqual(sent, 0)
+        self.client.close()
+        self.server.wait()
+        data = self.server.handler_instance.get_data()
+        self.assertEqual(data, b'')
+
+    def test_invalid_offset(self):
+        with self.assertRaises(OSError) as cm:
+            os.sendfile(self.sockno, self.fileno, -1, 4096)
+        self.assertEqual(cm.exception.errno, errno.EINVAL)
+
+    # --- headers / trailers tests
+
+    if SUPPORT_HEADERS_TRAILERS:
+
+        def test_headers(self):
+            total_sent = 0
+            sent = os.sendfile(self.sockno, self.fileno, 0, 4096,
+                               headers=[b"x" * 512])
+            total_sent += sent
+            offset = 4096
+            nbytes = 4096
+            while 1:
+                sent = self.sendfile_wrapper(self.sockno, self.fileno,
+                                                     offset, nbytes)
+                if sent == 0:
+                    break
+                total_sent += sent
+                offset += sent
+
+            expected_data = b"x" * 512 + self.DATA
+            self.assertEqual(total_sent, len(expected_data))
+            self.client.close()
+            self.server.wait()
+            data = self.server.handler_instance.get_data()
+            self.assertEqual(hash(data), hash(expected_data))
+
+        def test_trailers(self):
+            TESTFN2 = support.TESTFN + "2"
+            f = open(TESTFN2, 'wb')
+            f.write(b"abcde")
+            f.close()
+            f = open(TESTFN2, 'rb')
+            try:
+                os.sendfile(self.sockno, f.fileno(), 0, 4096, trailers=[b"12345"])
+                self.client.close()
+                self.server.wait()
+                data = self.server.handler_instance.get_data()
+                self.assertEqual(data, b"abcde12345")
+            finally:
+                os.remove(TESTFN2)
+
+        if hasattr(os, "SF_NODISKIO"):
+            def test_flags(self):
+                try:
+                    os.sendfile(self.sockno, self.fileno, 0, 4096,
+                                flags=os.SF_NODISKIO)
+                except OSError as err:
+                    if err.errno not in (errno.EBUSY, errno.EAGAIN):
+                        raise
+
+
 def test_main():
     support.run_unittest(
         FileTests,
@@ -1281,6 +1533,7 @@
         PidTests,
         LoginTests,
         LinkTests,
+        TestSendfile,
     )
 
 if __name__ == "__main__":