예제 #1
0
    def createSystem(self):
        """
        Create and return a brand new L{MemoryIOSystem}.

        The L{MemoryIOSystem} knows how to open new tunnel devices.

        @return: The newly created I/O system object.
        @rtype: L{MemoryIOSystem}
        """
        system = MemoryIOSystem()
        system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
        return system
예제 #2
0
 def setUp(self):
     """
     Create an in-memory I/O system and set up a L{TuntapPort} against it.
     """
     self.name = b"tun0"
     self.system = MemoryIOSystem()
     self.system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
     self.protocol = self.factory.buildProtocol(
         TunnelAddress(self.helper.TUNNEL_TYPE, self.name))
     self.reactor = FSSetClock()
     self.port = TuntapPort(
         self.name, self.protocol, reactor=self.reactor, system=self.system)
예제 #3
0
    def createSystem(self):
        """
        Create and return a brand new L{MemoryIOSystem}.

        The L{MemoryIOSystem} knows how to open new tunnel devices.

        @return: The newly created I/O system object.
        @rtype: L{MemoryIOSystem}
        """
        system = MemoryIOSystem()
        system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
        return system
예제 #4
0
 def test_blockingRead(self):
     """
     Blocking reads are not implemented by L{Tunnel.read}.  Attempting one
     results in L{NotImplementedError} being raised.
     """
     tunnel = Tunnel(MemoryIOSystem(), os.O_RDONLY, None)
     self.assertRaises(NotImplementedError, tunnel.read, 1024)
예제 #5
0
 def setUp(self):
     """
     Create an in-memory I/O system and set up a L{TuntapPort} against it.
     """
     self.name = b"tun0"
     self.system = MemoryIOSystem()
     self.system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
     self.protocol = self.factory.buildProtocol(TunnelAddress(self.helper.TUNNEL_TYPE, self.name))
     self.reactor = FSSetClock()
     self.port = TuntapPort(self.name, self.protocol, reactor=self.reactor, system=self.system)
예제 #6
0
class TunnelTestsMixin:
    """
    A mixin defining tests for L{TuntapPort}.

    These tests run against L{MemoryIOSystem} (proven equivalent to the real
    thing by the tests above) to avoid performing any real I/O.
    """
    def setUp(self):
        """
        Create an in-memory I/O system and set up a L{TuntapPort} against it.
        """
        self.name = b"tun0"
        self.system = MemoryIOSystem()
        self.system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
        self.protocol = self.factory.buildProtocol(
            TunnelAddress(self.helper.TUNNEL_TYPE, self.name))
        self.reactor = FSSetClock()
        self.port = TuntapPort(
            self.name, self.protocol, reactor=self.reactor, system=self.system)


    def _tunnelTypeOnly(self, flags):
        """
        Mask off any flags except for L{TunnelType.IFF_TUN} and
        L{TunnelType.IFF_TAP}.

        @param flags: Flags from L{TunnelType} to mask.
        @type flags: L{FlagConstant}

        @return: The flags given by C{flags} except the two type flags.
        @rtype: L{FlagConstant}
        """
        return flags & (TunnelFlags.IFF_TUN | TunnelFlags.IFF_TAP)


    def test_startListeningOpensDevice(self):
        """
        L{TuntapPort.startListening} opens the tunnel factory character special
        device C{"/dev/net/tun"} and configures it as a I{tun} tunnel.
        """
        system = self.system
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)

        expected = (
            system.O_RDWR | system.O_CLOEXEC | system.O_NONBLOCK,
            b"tun0" + b"\x00" * (_IFNAMSIZ - len(b"tun0")),
            self.port.interface, False, True)
        actual = (
            tunnel.openFlags,
            tunnel.requestedName,
            tunnel.name, tunnel.blocking, tunnel.closeOnExec)
        self.assertEqual(expected, actual)


    def test_startListeningSetsConnected(self):
        """
        L{TuntapPort.startListening} sets C{connected} on the port object to
        C{True}.
        """
        self.port.startListening()
        self.assertTrue(self.port.connected)


    def test_startListeningConnectsProtocol(self):
        """
        L{TuntapPort.startListening} calls C{makeConnection} on the protocol
        the port was initialized with, passing the port as an argument.
        """
        self.port.startListening()
        self.assertIs(self.port, self.protocol.transport)


    def test_startListeningStartsReading(self):
        """
        L{TuntapPort.startListening} passes the port instance to the reactor's
        C{addReader} method to begin watching the port's file descriptor for
        data to read.
        """
        self.port.startListening()
        self.assertIn(self.port, self.reactor.getReaders())


    def test_startListeningHandlesOpenFailure(self):
        """
        L{TuntapPort.startListening} raises L{CannotListenError} if opening the
        tunnel factory character special device fails.
        """
        self.system.permissions.remove('open')
        self.assertRaises(CannotListenError, self.port.startListening)


    def test_startListeningHandlesConfigureFailure(self):
        """
        L{TuntapPort.startListening} raises L{CannotListenError} if the
        C{ioctl} call to configure the tunnel device fails.
        """
        self.system.permissions.remove('ioctl')
        self.assertRaises(CannotListenError, self.port.startListening)


    def _stopPort(self, port):
        """
        Verify that the C{stopListening} method of an L{IListeningPort} removes
        that port from the reactor's "readers" set and also that the
        L{Deferred} returned by that method fires with L{None}.

        @param port: The port object to stop.
        @type port: L{IListeningPort} provider
        """
        stopped = port.stopListening()
        self.assertNotIn(port, self.reactor.getReaders())
        # An unfortunate implementation detail
        self.reactor.advance(0)
        self.assertIsNone(self.successResultOf(stopped))


    def test_stopListeningStopsReading(self):
        """
        L{TuntapPort.stopListening} returns a L{Deferred} which fires after the
        port has been removed from the reactor's reader list by passing it to
        the reactor's C{removeReader} method.
        """
        self.port.startListening()
        fileno = self.port.fileno()
        self._stopPort(self.port)

        self.assertNotIn(fileno, self.system._openFiles)


    def test_stopListeningUnsetsConnected(self):
        """
        After the L{Deferred} returned by L{TuntapPort.stopListening} fires,
        the C{connected} attribute of the port object is set to C{False}.
        """
        self.port.startListening()
        self._stopPort(self.port)
        self.assertFalse(self.port.connected)


    def test_stopListeningStopsProtocol(self):
        """
        L{TuntapPort.stopListening} calls C{doStop} on the protocol the port
        was initialized with.
        """
        self.port.startListening()
        self._stopPort(self.port)
        self.assertIsNone(self.protocol.transport)


    def test_stopListeningWhenStopped(self):
        """
        L{TuntapPort.stopListening} returns a L{Deferred} which succeeds
        immediately if it is called when the port is not listening.
        """
        stopped = self.port.stopListening()
        self.assertIsNone(self.successResultOf(stopped))


    def test_multipleStopListening(self):
        """
        It is safe and a no-op to call L{TuntapPort.stopListening} more than
        once with no intervening L{TuntapPort.startListening} call.
        """
        self.port.startListening()
        self.port.stopListening()
        second = self.port.stopListening()
        self.reactor.advance(0)
        self.assertIsNone(self.successResultOf(second))


    def test_loseConnection(self):
        """
        L{TuntapPort.loseConnection} stops the port and is deprecated.
        """
        self.port.startListening()

        self.port.loseConnection()
        # An unfortunate implementation detail
        self.reactor.advance(0)

        self.assertFalse(self.port.connected)
        warnings = self.flushWarnings([self.test_loseConnection])
        self.assertEqual(DeprecationWarning, warnings[0]['category'])
        self.assertEqual(
            "twisted.pair.tuntap.TuntapPort.loseConnection was deprecated "
            "in Twisted 14.0.0; please use twisted.pair.tuntap.TuntapPort."
            "stopListening instead",
            warnings[0]['message'])
        self.assertEqual(1, len(warnings))


    def _stopsReadingTest(self, style):
        """
        Test that L{TuntapPort.doRead} has no side-effects under a certain
        exception condition.

        @param style: An exception instance to arrange for the (python wrapper
            around the) underlying platform I{read} call to fail with.

        @raise C{self.failureException}: If there are any observable
            side-effects.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.nonBlockingExceptionStyle = style
        self.port.doRead()
        self.assertEqual([], self.protocol.received)


    def test_eagainStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EAGAIN} errno from a C{read}
        call, it returns.
        """
        self._stopsReadingTest(Tunnel.EAGAIN_STYLE)


    def test_ewouldblockStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EWOULDBLOCK} errno from a
        C{read} call, it returns.
        """
        self._stopsReadingTest(Tunnel.EWOULDBLOCK_STYLE)


    def test_eintrblockStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EINTR} errno from a C{read}
        call, it returns.
        """
        self._stopsReadingTest(Tunnel.EINTR_STYLE)


    def test_unhandledReadError(self):
        """
        If L{Tuntap.doRead} encounters any exception other than one explicitly
        handled by the code, the exception propagates to the caller.
        """
        class UnexpectedException(Exception):
            pass

        self.assertRaises(
            UnexpectedException,
            self._stopsReadingTest, UnexpectedException())


    def test_unhandledEnvironmentReadError(self):
        """
        Just like C{test_unhandledReadError}, but for the case where the
        exception that is not explicitly handled happens to be of type
        C{EnvironmentError} (C{OSError} or C{IOError}).
        """
        self.assertRaises(
            IOError,
            self._stopsReadingTest, IOError(EPERM, "Operation not permitted"))


    def test_doReadSmallDatagram(self):
        """
        L{TuntapPort.doRead} reads a datagram of fewer than
        C{TuntapPort.maxPacketSize} from the port's file descriptor and passes
        it to its protocol's C{datagramReceived} method.
        """
        datagram = b'x' * (self.port.maxPacketSize - 1)
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.append(datagram)
        self.port.doRead()
        self.assertEqual([datagram], self.protocol.received)


    def test_doReadLargeDatagram(self):
        """
        L{TuntapPort.doRead} reads the first part of a datagram of more than
        C{TuntapPort.maxPacketSize} from the port's file descriptor and passes
        the truncated data to its protocol's C{datagramReceived} method.
        """
        datagram = b'x' * self.port.maxPacketSize
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.append(datagram + b'y')
        self.port.doRead()
        self.assertEqual([datagram], self.protocol.received)


    def test_doReadSeveralDatagrams(self):
        """
        L{TuntapPort.doRead} reads several datagrams, of up to
        C{TuntapPort.maxThroughput} bytes total, before returning.
        """
        values = cycle(iterbytes(b'abcdefghijklmnopqrstuvwxyz'))
        total = 0
        datagrams = []
        while total < self.port.maxThroughput:
            datagrams.append(next(values) * self.port.maxPacketSize)
            total += self.port.maxPacketSize

        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.extend(datagrams)
        tunnel.readBuffer.append(b'excessive datagram, not to be read')

        self.port.doRead()
        self.assertEqual(datagrams, self.protocol.received)


    def _datagramReceivedException(self):
        """
        Deliver some data to a L{TuntapPort} hooked up to an application
        protocol that raises an exception from its C{datagramReceived} method.

        @return: Whatever L{AttributeError} exceptions are logged.
        """
        self.port.startListening()
        self.system.getTunnel(self.port).readBuffer.append(b"ping")

        # Break the application logic
        self.protocol.received = None

        self.port.doRead()
        return self.flushLoggedErrors(AttributeError)


    def test_datagramReceivedException(self):
        """
        If the protocol's C{datagramReceived} method raises an exception, the
        exception is logged.
        """
        errors = self._datagramReceivedException()
        self.assertEqual(1, len(errors))


    def test_datagramReceivedExceptionIdentifiesProtocol(self):
        """
        The exception raised by C{datagramReceived} is logged with a message
        identifying the offending protocol.
        """
        messages = []
        addObserver(messages.append)
        self.addCleanup(removeObserver, messages.append)
        self._datagramReceivedException()
        error = next(m for m in messages if m['isError'])
        message = textFromEventDict(error)
        self.assertEqual(
            "Unhandled exception from %s.datagramReceived" % (
                fullyQualifiedName(self.protocol.__class__),),
            message.splitlines()[0])


    def test_write(self):
        """
        L{TuntapPort.write} sends a datagram into the tunnel.
        """
        datagram = b"a b c d e f g"
        self.port.startListening()
        self.port.write(datagram)
        self.assertEqual(
            self.system.getTunnel(self.port).writeBuffer,
            deque([datagram]))


    def test_interruptedWrite(self):
        """
        If the platform write call is interrupted (causing the Python wrapper
        to raise C{IOError} with errno set to C{EINTR}), the write is re-tried.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.pendingSignals.append(SIGINT)
        self.port.write(b"hello, world")
        self.assertEqual(deque([b"hello, world"]), tunnel.writeBuffer)


    def test_unhandledWriteError(self):
        """
        Any exception raised by the underlying write call, except for EINTR, is
        propagated to the caller.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        self.assertRaises(
            IOError,
            self.port.write, b"x" * tunnel.SEND_BUFFER_SIZE + b"y")


    def test_writeSequence(self):
        """
        L{TuntapPort.writeSequence} sends a datagram into the tunnel by
        concatenating the byte strings in the list passed to it.
        """
        datagram = [b"a", b"b", b"c", b"d"]
        self.port.startListening()
        self.port.writeSequence(datagram)
        self.assertEqual(
            self.system.getTunnel(self.port).writeBuffer,
            deque([b"".join(datagram)]))


    def test_getHost(self):
        """
        L{TuntapPort.getHost} returns a L{TunnelAddress} including the tunnel's
        type and name.
        """
        self.port.startListening()
        address = self.port.getHost()
        self.assertEqual(
            TunnelAddress(
                self._tunnelTypeOnly(self.helper.TUNNEL_TYPE),
                self.system.getTunnel(self.port).name),
            address)


    def test_listeningString(self):
        """
        The string representation of a L{TuntapPort} instance includes the
        tunnel type and interface and the protocol associated with the port.
        """
        self.port.startListening()
        self.assertRegex(str(self.port),
                         fullyQualifiedName(self.protocol.__class__))

        expected = " listening on %s/%s>" % (
            self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name,
            self.system.getTunnel(self.port).name)
        self.assertTrue(str(self.port).find(expected) != -1)


    def test_unlisteningString(self):
        """
        The string representation of a L{TuntapPort} instance includes the
        tunnel type and interface and the protocol associated with the port.
        """
        self.assertRegex(str(self.port),
                         fullyQualifiedName(self.protocol.__class__))

        expected = " not listening on %s/%s>" % (
            self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name,
            self.name)
        self.assertTrue(str(self.port).find(expected) != -1)


    def test_logPrefix(self):
        """
        L{TuntapPort.logPrefix} returns a string identifying the application
        protocol and the type of tunnel.
        """
        self.assertEqual(
            "%s (%s)" % (
                self.protocol.__class__.__name__,
                self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name),
            self.port.logPrefix())
예제 #7
0
class TunnelTestsMixin(object):
    """
    A mixin defining tests for L{TuntapPort}.

    These tests run against L{MemoryIOSystem} (proven equivalent to the real
    thing by the tests above) to avoid performing any real I/O.
    """
    def setUp(self):
        """
        Create an in-memory I/O system and set up a L{TuntapPort} against it.
        """
        self.name = b"tun0"
        self.system = MemoryIOSystem()
        self.system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel)
        self.protocol = self.factory.buildProtocol(
            TunnelAddress(self.helper.TUNNEL_TYPE, self.name))
        self.reactor = FSSetClock()
        self.port = TuntapPort(
            self.name, self.protocol, reactor=self.reactor, system=self.system)


    def _tunnelTypeOnly(self, flags):
        """
        Mask off any flags except for L{TunnelType.IFF_TUN} and
        L{TunnelType.IFF_TAP}.

        @param flags: Flags from L{TunnelType} to mask.
        @type flags: L{FlagConstant}

        @return: The flags given by C{flags} except the two type flags.
        @rtype: L{FlagConstant}
        """
        return flags & (TunnelFlags.IFF_TUN | TunnelFlags.IFF_TAP)


    def test_startListeningOpensDevice(self):
        """
        L{TuntapPort.startListening} opens the tunnel factory character special
        device C{"/dev/net/tun"} and configures it as a I{tun} tunnel.
        """
        system = self.system
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)

        expected = (
            system.O_RDWR | system.O_CLOEXEC | system.O_NONBLOCK,
            b"tun0" + "\x00" * (_IFNAMSIZ - len(b"tun0")),
            self.port.interface, False, True)
        actual = (
            tunnel.openFlags,
            tunnel.requestedName,
            tunnel.name, tunnel.blocking, tunnel.closeOnExec)
        self.assertEqual(expected, actual)


    def test_startListeningSetsConnected(self):
        """
        L{TuntapPort.startListening} sets C{connected} on the port object to
        C{True}.
        """
        self.port.startListening()
        self.assertTrue(self.port.connected)


    def test_startListeningConnectsProtocol(self):
        """
        L{TuntapPort.startListening} calls C{makeConnection} on the protocol
        the port was initialized with, passing the port as an argument.
        """
        self.port.startListening()
        self.assertIs(self.port, self.protocol.transport)


    def test_startListeningStartsReading(self):
        """
        L{TuntapPort.startListening} passes the port instance to the reactor's
        C{addReader} method to begin watching the port's file descriptor for
        data to read.
        """
        self.port.startListening()
        self.assertIn(self.port, self.reactor.getReaders())


    def test_startListeningHandlesOpenFailure(self):
        """
        L{TuntapPort.startListening} raises L{CannotListenError} if opening the
        tunnel factory character special device fails.
        """
        self.system.permissions.remove('open')
        self.assertRaises(CannotListenError, self.port.startListening)


    def test_startListeningHandlesConfigureFailure(self):
        """
        L{TuntapPort.startListening} raises L{CannotListenError} if the
        C{ioctl} call to configure the tunnel device fails.
        """
        self.system.permissions.remove('ioctl')
        self.assertRaises(CannotListenError, self.port.startListening)


    def _stopPort(self, port):
        """
        Verify that the C{stopListening} method of an L{IListeningPort} removes
        that port from the reactor's "readers" set and also that the
        L{Deferred} returned by that method fires with C{None}.

        @param port: The port object to stop.
        @type port: L{IListeningPort} provider
        """
        stopped = port.stopListening()
        self.assertNotIn(port, self.reactor.getReaders())
        # An unfortunate implementation detail
        self.reactor.advance(0)
        self.assertIs(None, self.successResultOf(stopped))


    def test_stopListeningStopsReading(self):
        """
        L{TuntapPort.stopListening} returns a L{Deferred} which fires after the
        port has been removed from the reactor's reader list by passing it to
        the reactor's C{removeReader} method.
        """
        self.port.startListening()
        fileno = self.port.fileno()
        self._stopPort(self.port)

        self.assertNotIn(fileno, self.system._openFiles)


    def test_stopListeningUnsetsConnected(self):
        """
        After the L{Deferred} returned by L{TuntapPort.stopListening} fires,
        the C{connected} attribute of the port object is set to C{False}.
        """
        self.port.startListening()
        self._stopPort(self.port)
        self.assertFalse(self.port.connected)


    def test_stopListeningStopsProtocol(self):
        """
        L{TuntapPort.stopListening} calls C{doStop} on the protocol the port
        was initialized with.
        """
        self.port.startListening()
        self._stopPort(self.port)
        self.assertIs(None, self.protocol.transport)


    def test_stopListeningWhenStopped(self):
        """
        L{TuntapPort.stopListening} returns a L{Deferred} which succeeds
        immediately if it is called when the port is not listening.
        """
        stopped = self.port.stopListening()
        self.assertIs(None, self.successResultOf(stopped))


    def test_multipleStopListening(self):
        """
        It is safe and a no-op to call L{TuntapPort.stopListening} more than
        once with no intervening L{TuntapPort.startListening} call.
        """
        self.port.startListening()
        self.port.stopListening()
        second = self.port.stopListening()
        self.reactor.advance(0)
        self.assertIs(None, self.successResultOf(second))


    def test_loseConnection(self):
        """
        L{TuntapPort.loseConnection} stops the port and is deprecated.
        """
        self.port.startListening()

        self.port.loseConnection()
        # An unfortunate implementation detail
        self.reactor.advance(0)

        self.assertFalse(self.port.connected)
        warnings = self.flushWarnings([self.test_loseConnection])
        self.assertEqual(DeprecationWarning, warnings[0]['category'])
        self.assertEqual(
            "twisted.pair.tuntap.TuntapPort.loseConnection was deprecated "
            "in Twisted 14.0.0; please use twisted.pair.tuntap.TuntapPort."
            "stopListening instead",
            warnings[0]['message'])
        self.assertEqual(1, len(warnings))


    def _stopsReadingTest(self, style):
        """
        Test that L{TuntapPort.doRead} has no side-effects under a certain
        exception condition.

        @param style: An exception instance to arrange for the (python wrapper
            around the) underlying platform I{read} call to fail with.

        @raise C{self.failureException}: If there are any observable
            side-effects.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.nonBlockingExceptionStyle = style
        self.port.doRead()
        self.assertEqual([], self.protocol.received)


    def test_eagainStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EAGAIN} errno from a C{read}
        call, it returns.
        """
        self._stopsReadingTest(Tunnel.EAGAIN_STYLE)


    def test_ewouldblockStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EWOULDBLOCK} errno from a
        C{read} call, it returns.
        """
        self._stopsReadingTest(Tunnel.EWOULDBLOCK_STYLE)


    def test_eintrblockStopsReading(self):
        """
        Once L{TuntapPort.doRead} encounters an I{EINTR} errno from a C{read}
        call, it returns.
        """
        self._stopsReadingTest(Tunnel.EINTR_STYLE)


    def test_unhandledReadError(self):
        """
        If L{Tuntap.doRead} encounters any exception other than one explicitly
        handled by the code, the exception propagates to the caller.
        """
        class UnexpectedException(Exception):
            pass

        self.assertRaises(
            UnexpectedException,
            self._stopsReadingTest, UnexpectedException())


    def test_unhandledEnvironmentReadError(self):
        """
        Just like C{test_unhandledReadError}, but for the case where the
        exception that is not explicitly handled happens to be of type
        C{EnvironmentError} (C{OSError} or C{IOError}).
        """
        self.assertRaises(
            IOError,
            self._stopsReadingTest, IOError(EPERM, "Operation not permitted"))


    def test_doReadSmallDatagram(self):
        """
        L{TuntapPort.doRead} reads a datagram of fewer than
        C{TuntapPort.maxPacketSize} from the port's file descriptor and passes
        it to its protocol's C{datagramReceived} method.
        """
        datagram = b'x' * (self.port.maxPacketSize - 1)
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.append(datagram)
        self.port.doRead()
        self.assertEqual([datagram], self.protocol.received)


    def test_doReadLargeDatagram(self):
        """
        L{TuntapPort.doRead} reads the first part of a datagram of more than
        C{TuntapPort.maxPacketSize} from the port's file descriptor and passes
        the truncated data to its protocol's C{datagramReceived} method.
        """
        datagram = b'x' * self.port.maxPacketSize
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.append(datagram + b'y')
        self.port.doRead()
        self.assertEqual([datagram], self.protocol.received)


    def test_doReadSeveralDatagrams(self):
        """
        L{TuntapPort.doRead} reads several datagrams, of up to
        C{TuntapPort.maxThroughput} bytes total, before returning.
        """
        values = cycle(iterbytes(b'abcdefghijklmnopqrstuvwxyz'))
        total = 0
        datagrams = []
        while total < self.port.maxThroughput:
            datagrams.append(next(values) * self.port.maxPacketSize)
            total += self.port.maxPacketSize

        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.readBuffer.extend(datagrams)
        tunnel.readBuffer.append(b'excessive datagram, not to be read')

        self.port.doRead()
        self.assertEqual(datagrams, self.protocol.received)


    def _datagramReceivedException(self):
        """
        Deliver some data to a L{TuntapPort} hooked up to an application
        protocol that raises an exception from its C{datagramReceived} method.

        @return: Whatever L{AttributeError} exceptions are logged.
        """
        self.port.startListening()
        self.system.getTunnel(self.port).readBuffer.append(b"ping")

        # Break the application logic
        self.protocol.received = None

        self.port.doRead()
        return self.flushLoggedErrors(AttributeError)


    def test_datagramReceivedException(self):
        """
        If the protocol's C{datagramReceived} method raises an exception, the
        exception is logged.
        """
        errors = self._datagramReceivedException()
        self.assertEqual(1, len(errors))


    def test_datagramReceivedExceptionIdentifiesProtocol(self):
        """
        The exception raised by C{datagramReceived} is logged with a message
        identifying the offending protocol.
        """
        messages = []
        addObserver(messages.append)
        self.addCleanup(removeObserver, messages.append)
        self._datagramReceivedException()
        error = next(m for m in messages if m['isError'])
        message = textFromEventDict(error)
        self.assertEqual(
            "Unhandled exception from %s.datagramReceived" % (
                fullyQualifiedName(self.protocol.__class__),),
            message.splitlines()[0])


    def test_write(self):
        """
        L{TuntapPort.write} sends a datagram into the tunnel.
        """
        datagram = b"a b c d e f g"
        self.port.startListening()
        self.port.write(datagram)
        self.assertEqual(
            self.system.getTunnel(self.port).writeBuffer,
            deque([datagram]))


    def test_interruptedWrite(self):
        """
        If the platform write call is interrupted (causing the Python wrapper
        to raise C{IOError} with errno set to C{EINTR}), the write is re-tried.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        tunnel.pendingSignals.append(SIGINT)
        self.port.write(b"hello, world")
        self.assertEqual(deque([b"hello, world"]), tunnel.writeBuffer)


    def test_unhandledWriteError(self):
        """
        Any exception raised by the underlying write call, except for EINTR, is
        propagated to the caller.
        """
        self.port.startListening()
        tunnel = self.system.getTunnel(self.port)
        self.assertRaises(
            IOError,
            self.port.write, b"x" * tunnel.SEND_BUFFER_SIZE + b"y")


    def test_writeSequence(self):
        """
        L{TuntapPort.writeSequence} sends a datagram into the tunnel by
        concatenating the byte strings in the list passed to it.
        """
        datagram = [b"a", b"b", b"c", b"d"]
        self.port.startListening()
        self.port.writeSequence(datagram)
        self.assertEqual(
            self.system.getTunnel(self.port).writeBuffer,
            deque([b"".join(datagram)]))


    def test_getHost(self):
        """
        L{TuntapPort.getHost} returns a L{TunnelAddress} including the tunnel's
        type and name.
        """
        self.port.startListening()
        address = self.port.getHost()
        self.assertEqual(
            TunnelAddress(
                self._tunnelTypeOnly(self.helper.TUNNEL_TYPE),
                self.system.getTunnel(self.port).name),
            address)


    def test_listeningString(self):
        """
        The string representation of a L{TuntapPort} instance includes the
        tunnel type and interface and the protocol associated with the port.
        """
        self.port.startListening()
        expected = "<%s listening on %s/%s>" % (
            fullyQualifiedName(self.protocol.__class__),
            self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name,
            self.system.getTunnel(self.port).name)

        self.assertEqual(expected, str(self.port))


    def test_unlisteningString(self):
        """
        The string representation of a L{TuntapPort} instance includes the
        tunnel type and interface and the protocol associated with the port.
        """
        expected = "<%s not listening on %s/%s>" % (
            fullyQualifiedName(self.protocol.__class__),
            self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name, self.name)

        self.assertEqual(expected, str(self.port))


    def test_logPrefix(self):
        """
        L{TuntapPort.logPrefix} returns a string identifying the application
        protocol and the type of tunnel.
        """
        self.assertEqual(
            "%s (%s)" % (
                self.protocol.__class__.__name__,
                self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name),
            self.port.logPrefix())